Repository: iterative/dvc

Issue:
exp show: show only changed parameters
Currently I have tens of parameters in my `params.yaml`. After I `dvc exp run --queue` several experiments and `dvc exp show`, it is hard for me to recognize the parameters I just set. I know I could use `--include-params or --exclude-params`, but it somehow just takes me some efforts to list my previous commands, recall what I have changed in `params.yaml` before queuing. It would be nice to just show the **changed** parameters in `dvc exp show`, e.g., via `dvc exp show --only-changed`
exp show: show only changed parameters
Currently I have tens of parameters in my `params.yaml`. After I `dvc exp run --queue` several experiments and `dvc exp show`, it is hard for me to recognize the parameters I just set. I know I could use `--include-params or --exclude-params`, but it somehow just takes me some efforts to list my previous commands, recall what I have changed in `params.yaml` before queuing. It would be nice to just show the **changed** parameters in `dvc exp show`, e.g., via `dvc exp show --only-changed`


Discussion:
@ciaochiaociao Hello, first thanks for your suggestion.

Currently `dvc exp show` can show all the parameters metrics of all experiments in a table.`dvc exp diff` can compare two experiments' parameters and metrics and only show the different ones. Maybe this command can help you now?


Yes, right now I kind of write a bash script to automate running `dvc exp diff` over all my experiments compared to HEAD. 
I have similar use-case to @ciaochiaociao. I have 10 experiments I want to show the results of in a table. Together they differ over 4 of ~20 parameters that I am tracking. I can include only the 4 parameters, but it seems trivial yet very convenient to provide some `--show-changed-params` to auto-filter out parameters that never change throughout all experiments in the table.
@ciaochiaociao Hello, first thanks for your suggestion.

Currently `dvc exp show` can show all the parameters metrics of all experiments in a table.`dvc exp diff` can compare two experiments' parameters and metrics and only show the different ones. Maybe this command can help you now?


Yes, right now I kind of write a bash script to automate running `dvc exp diff` over all my experiments compared to HEAD. 
I have similar use-case to @ciaochiaociao. I have 10 experiments I want to show the results of in a table. Together they differ over 4 of ~20 parameters that I am tracking. I can include only the 4 parameters, but it seems trivial yet very convenient to provide some `--show-changed-params` to auto-filter out parameters that never change throughout all experiments in the table.

Patch:
diff --git a/dvc/command/experiments.py b/dvc/command/experiments.py
--- a/dvc/command/experiments.py
+++ b/dvc/command/experiments.py
@@ -471,6 +471,9 @@ def show_experiments(
         }
     )
 
+    if kwargs.get("only_changed", False):
+        td.drop_duplicates("cols")
+
     td.render(
         pager=pager,
         borders=True,
@@ -537,6 +540,7 @@ def run(self):
                 pager=not self.args.no_pager,
                 csv=self.args.csv,
                 markdown=self.args.markdown,
+                only_changed=self.args.only_changed,
             )
         return 0
 
@@ -1033,6 +1037,15 @@ def add_parser(subparsers, parent_parser):
         ),
         metavar="<n>",
     )
+    experiments_show_parser.add_argument(
+        "--only-changed",
+        action="store_true",
+        default=False,
+        help=(
+            "Only show metrics/params with values varying "
+            "across the selected experiments."
+        ),
+    )
     experiments_show_parser.set_defaults(func=CmdExperimentsShow)
 
     EXPERIMENTS_APPLY_HELP = (
diff --git a/dvc/compare.py b/dvc/compare.py
--- a/dvc/compare.py
+++ b/dvc/compare.py
@@ -188,7 +188,6 @@ def dropna(self, axis: str = "rows"):
                 f"Invalid 'axis' value {axis}."
                 "Choose one of ['rows', 'cols']"
             )
-
         to_drop: Set = set()
         for n_row, row in enumerate(self):
             for n_col, col in enumerate(row):
@@ -211,6 +210,41 @@ def dropna(self, axis: str = "rows"):
         else:
             self.drop(*to_drop)
 
+    def drop_duplicates(self, axis: str = "rows"):
+        if axis not in ["rows", "cols"]:
+            raise ValueError(
+                f"Invalid 'axis' value {axis}."
+                "Choose one of ['rows', 'cols']"
+            )
+
+        if axis == "cols":
+            cols_to_drop: List[str] = []
+            for n_col, col in enumerate(self.columns):
+                # Cast to str because Text is not hashable error
+                unique_vals = {str(x) for x in col if x != self._fill_value}
+                if len(unique_vals) == 1:
+                    cols_to_drop.append(self.keys()[n_col])
+            self.drop(*cols_to_drop)
+
+        elif axis == "rows":
+            unique_rows = []
+            rows_to_drop: List[int] = []
+            for n_row, row in enumerate(self):
+                tuple_row = tuple(row)
+                if tuple_row in unique_rows:
+                    rows_to_drop.append(n_row)
+                else:
+                    unique_rows.append(tuple_row)
+
+            for name in self.keys():
+                self._columns[name] = Column(
+                    [
+                        x
+                        for n, x in enumerate(self._columns[name])
+                        if n not in rows_to_drop
+                    ]
+                )
+
 
 def _normalize_float(val: float, precision: int):
     return f"{val:.{precision}g}"


Test patch:
diff --git a/tests/func/experiments/test_show.py b/tests/func/experiments/test_show.py
--- a/tests/func/experiments/test_show.py
+++ b/tests/func/experiments/test_show.py
@@ -538,3 +538,47 @@ def _get_rev_isotimestamp(rev):
         )
         in cap.out
     )
+
+
+def test_show_only_changed(tmp_dir, dvc, scm, capsys):
+    tmp_dir.gen("copy.py", COPY_SCRIPT)
+    params_file = tmp_dir / "params.yaml"
+    params_data = {
+        "foo": 1,
+        "bar": 1,
+    }
+    (tmp_dir / params_file).dump(params_data)
+
+    dvc.run(
+        cmd="python copy.py params.yaml metrics.yaml",
+        metrics_no_cache=["metrics.yaml"],
+        params=["foo", "bar"],
+        name="copy-file",
+        deps=["copy.py"],
+    )
+    scm.add(
+        [
+            "dvc.yaml",
+            "dvc.lock",
+            "copy.py",
+            "params.yaml",
+            "metrics.yaml",
+            ".gitignore",
+        ]
+    )
+    scm.commit("init")
+
+    dvc.experiments.run(params=["foo=2"])
+
+    capsys.readouterr()
+    assert main(["exp", "show"]) == 0
+    cap = capsys.readouterr()
+
+    print(cap)
+    assert "bar" in cap.out
+
+    capsys.readouterr()
+    assert main(["exp", "show", "--only-changed"]) == 0
+    cap = capsys.readouterr()
+
+    assert "bar" not in cap.out
diff --git a/tests/unit/test_tabular_data.py b/tests/unit/test_tabular_data.py
--- a/tests/unit/test_tabular_data.py
+++ b/tests/unit/test_tabular_data.py
@@ -205,8 +205,47 @@ def test_dropna(axis, expected):
     assert list(td) == expected
 
 
+@pytest.mark.parametrize(
+    "axis,expected",
+    [
+        (
+            "rows",
+            [
+                ["foo", "", ""],
+                ["foo", "foo", ""],
+                ["foo", "bar", "foobar"],
+            ],
+        ),
+        ("cols", [[""], ["foo"], ["foo"], ["bar"]]),
+    ],
+)
+def test_drop_duplicates(axis, expected):
+    td = TabularData(["col-1", "col-2", "col-3"])
+    td.extend(
+        [["foo"], ["foo", "foo"], ["foo", "foo"], ["foo", "bar", "foobar"]]
+    )
+
+    assert list(td) == [
+        ["foo", "", ""],
+        ["foo", "foo", ""],
+        ["foo", "foo", ""],
+        ["foo", "bar", "foobar"],
+    ]
+
+    td.drop_duplicates(axis)
+
+    assert list(td) == expected
+
+
 def test_dropna_invalid_axis():
     td = TabularData(["col-1", "col-2", "col-3"])
 
     with pytest.raises(ValueError, match="Invalid 'axis' value foo."):
         td.dropna("foo")
+
+
+def test_drop_duplicates_invalid_axis():
+    td = TabularData(["col-1", "col-2", "col-3"])
+
+    with pytest.raises(ValueError, match="Invalid 'axis' value foo."):
+        td.drop_duplicates("foo")