Skip to content
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
67 changes: 57 additions & 10 deletions dvc/command/experiments.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,10 +29,10 @@ def _filter_names(
f" --exclude-{label}"
)

names = {tuple(name.split(".")) for name in names}
names = [tuple(name.split(".")) for name in names]

def _filter(filters, update_func):
filters = {tuple(name.split(".")) for name in filters}
filters = [tuple(name.split(".")) for name in filters]
for length, groups in groupby(filters, len):
for group in groups:
matches = [name for name in names if name[:length] == group]
Expand All @@ -41,18 +41,18 @@ def _filter(filters, update_func):
raise InvalidArgumentError(
f"'{name}' does not match any known {label}"
)
update_func(matches)
update_func({match: None for match in matches})

if include:
ret = set()
ret = OrderedDict()
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use OrderedDict() with None values to get set-like behavior with preserved ordering

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Or, use a dict?

Copy link
Contributor Author

@pmrowla pmrowla Aug 31, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I thought the preserved key ordering for regular dict technically isn't guaranteed before 3.7 (it's a cpython implementation behavior in 3.6 that was added to the formal spec in 3.7), so I thought it would be preferred to just use OrderedDict?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, you are right that it is only on >3.6. My point was that, as CPython and Python are closely tied, no one is going to change that behavior in 3.6, so should be safe to just use dict for convenience. Saying that I have no issues with OrderedDict, it was just a suggestion.

_filter(include, ret.update)
else:
ret = set(names)
ret = OrderedDict({name: None for name in names})

if exclude:
_filter(exclude, ret.difference_update)

return {".".join(name) for name in ret}
return [".".join(name) for name in ret]


def _update_names(names, items):
Expand All @@ -61,7 +61,7 @@ def _update_names(names, items):
item = flatten(item)
names.update(item.keys())
else:
names.add(name)
names[name] = None


def _collect_names(all_experiments, **kwargs):
Expand All @@ -74,19 +74,19 @@ def _collect_names(all_experiments, **kwargs):
_update_names(param_names, exp.get("params", {}).items())

metric_names = _filter_names(
metric_names,
sorted(metric_names),
"metrics",
kwargs.get("include_metrics"),
kwargs.get("exclude_metrics"),
)
param_names = _filter_names(
param_names,
sorted(param_names),
"params",
kwargs.get("include_params"),
kwargs.get("exclude_params"),
)

return sorted(metric_names), sorted(param_names)
return metric_names, param_names


def _collect_rows(
Expand All @@ -96,7 +96,19 @@ def _collect_rows(
param_names,
precision=DEFAULT_PRECISION,
no_timestamp=False,
sort_by=None,
sort_order=None,
):
if sort_by:
if sort_by in metric_names:
sort_type = "metrics"
elif sort_by in param_names:
sort_type = "params"
else:
raise InvalidArgumentError(f"Unknown sort column '{sort_by}'")
reverse = sort_order == "desc"
experiments = _sort_exp(experiments, sort_by, sort_type, reverse)

for i, (rev, exp) in enumerate(experiments.items()):
row = []
style = None
Expand All @@ -122,6 +134,28 @@ def _collect_rows(
yield row, style


def _sort_exp(experiments, sort_by, typ, reverse):
if "baseline" in experiments:
ret = OrderedDict({"baseline": experiments.pop("baseline")})
else:
ret = OrderedDict()

def _sort(item):
_, exp = item
for fname, item in exp.get(typ, {}).items():
if isinstance(item, dict):
item = flatten(item)
else:
item = {fname: item}
if sort_by in item:
val = item[sort_by]
return (val is None, val)
return (True, None)

ret.update(sorted(experiments.items(), key=_sort, reverse=reverse))
return ret


def _format_time(timestamp):
if timestamp is None:
return "-"
Expand Down Expand Up @@ -241,6 +275,8 @@ def run(self):
include_params=self.args.include_params,
exclude_params=self.args.exclude_params,
no_timestamp=self.args.no_timestamp,
sort_by=self.args.sort_by,
sort_order=self.args.sort_order,
)

if not self.args.no_pager:
Expand Down Expand Up @@ -418,6 +454,17 @@ def add_parser(subparsers, parent_parser):
help="Exclude the specified params from output table.",
metavar="<params_list>",
)
experiments_show_parser.add_argument(
"--sort-by",
help="Sort related experiments by the specified metric or param.",
metavar="<metric/param>",
)
experiments_show_parser.add_argument(
"--sort-order",
help="Sort order to use with --sort-by.",
choices=("asc", "desc"),
default="asc",
)
experiments_show_parser.add_argument(
"--no-timestamp",
action="store_true",
Expand Down