Repository: iterative/dvc

Issue:
experiments show: allow filtering/sorting specific metrics/params
From #4345

> By default, if the console isn't wide enough to display the full table, params columns will be truncated first, giving priority to display the full experiment SHA and resulting metrics. In the future we should probably have options to only show certain metrics/params columns so that we don't need to truncate anything.

TODO

* [x] include/exclude columns
* [ ] sorting on specific columns


Discussion:

Resolving patch:
diff --git a/dvc/command/experiments.py b/dvc/command/experiments.py
--- a/dvc/command/experiments.py
+++ b/dvc/command/experiments.py
@@ -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]
@@ -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()
         _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):
@@ -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):
@@ -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(
@@ -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
@@ -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 "-"
@@ -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:
@@ -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",


Test patch: