Repository: iterative/dvc

Issue:
`exp show`: Include data files.
If an user modifies data files in between `dvc exp run` calls, the change is not reflected in the table generated by `dvc exp show`.

In [Studio](https://studio.iterative.ai) users can view information of the data files associated to each `experiment`. I think it would be nice to include similar information in the table generated by `dvc exp show`.

Ideally, the feature would have the same options available for `metrics` and `params` (i.e. `--include-files`/`--exclude-files`).


Discussion:
+1 on this. I'll be important for VS Code. On a separate note. We haven't touched source files in the Studio yet, but it would be great to have signal in the table from which you can see that there are changes to the source files between experiments.
What should the table actually show? I don't think actually including a list of changed files in the CLI table is feasible due to the size constraints.

So should it just be a true/false column to note that there is a change to data files (so users know they should check the changes with `dvc diff`? Or maybe columns for `added/modified/removed` with counts that would match the summary totals you get from `dvc diff`?
But if we use `added/modified/removed`, there comes another question. These notions only represent the status compare to default/workspace version. Unlike with the numbers it is hard to know the differences between two `modified` exps.
For experiments in the table it would have to be the diff between the experiment and the original baseline git commit (not the diff of one experiment to another experiment)

To get the difference between two experiments you would just have to use `dvc diff exp1 exp2`
> +1 on this. I'll be important for VS Code. On a separate note. We haven't touched source files in the Studio yet, but it would be great to have signal in the table from which you can see that there are changes to the source files between experiments.

Any source files or just source files that are `deps` of a stage?
What is meaningful info to show about data files? Just whether they have changed from the baseline commit? Size? Number of files? Maybe there is some tag/label/metadata that would be helpful?

Also, what files should be included (dependencies, outputs, dvc-tracked files, all files)?

> Ideally, the feature would have the same options available for `metrics` and `params` (i.e. `--include-files`/`--exclude-files`).

If we add data files to the `dvc exp show` table, it will become even more important to provide easy ways to narrow the table. Adding more info is not universally desirable since the table can become too busy. See:

* #6271
* #5966
* #6451
> Any source files or just source files that are deps of a stage?

I would say just a binary signal that some sources have changed is a good start. 

> Also, what files should be included (dependencies, outputs, dvc-tracked files, all files)?

My take (from Studio experience) - inputs (datasets) is the most important part. Les so intermediate, etc. We can start with datasets only if want to keep the table sane.

> What is meaningful info to show about data files? Just whether they have changed from the baseline commit? Size? Number of files? Maybe there is some tag/label/metadata that would be helpful?

Good question. I think some signal that data has changes is enough (we highlight values in Studio with a special dot icon or something like this). Values - we could do size to start. In Studio users can switch between those.
Show the same thing in `dvc status` is the first step.
Another idea here is to encourage use of the `desc` field and show that in the table.
> Another idea here is to encourage use of the `desc` field and show that in the table.

In addition, we could show the `rev` (or other selectable property, line in Studio https://github.com/iterative/studio-support/issues/16)
> Another idea here is to encourage use of the `desc` field and show that in the table.

Related https://github.com/iterative/dvc/issues/6548
> Another idea here is to encourage use of the `desc` field and show that in the table.

But the problem is that as we didn't change the `desc` after the `.dvc` file had been generated, it stays the same in all of the revisions. Cann't be taken any comparison.
I was thinking that users might update the description when they make changes, like `cats` -> `cats and dogs`. Maybe it would be better to track the last modified commit and show the commit message 🤷 .
> I was thinking that users might update the description when they make changes, like `cats` -> `cats and dogs`. Maybe it would be better to track the last modified commit and show the commit message 🤷 .

Only support for `.dvc` files not for pipeline generated ones? That's make sense, Only those `dvc add` generated data need a name, those generated via pipeline ones can be represented by the parameters from stages.

Resolving patch:
diff --git a/dvc/commands/experiments/show.py b/dvc/commands/experiments/show.py
--- a/dvc/commands/experiments/show.py
+++ b/dvc/commands/experiments/show.py
@@ -39,14 +39,17 @@ def _update_names(names, items):
 def _collect_names(all_experiments, **kwargs):
     metric_names = defaultdict(dict)
     param_names = defaultdict(dict)
+    deps_names = set()
 
     for _, experiments in all_experiments.items():
         for exp_data in experiments.values():
             exp = exp_data.get("data", {})
             _update_names(metric_names, exp.get("metrics", {}).items())
             _update_names(param_names, exp.get("params", {}).items())
+            for dep_name in exp.get("deps", {}):
+                deps_names.add(dep_name)
 
-    return metric_names, param_names
+    return metric_names, param_names, deps_names
 
 
 experiment_types = {
@@ -64,6 +67,7 @@ def _collect_rows(
     experiments,
     metric_names,
     param_names,
+    deps_names,
     precision=DEFAULT_PRECISION,
     sort_by=None,
     sort_order=None,
@@ -150,7 +154,11 @@ def _collect_rows(
             precision,
             fill_value=fill_value,
         )
-
+        for dep in deps_names:
+            hash_info = exp.get("deps", {}).get(dep, {}).get("hash")
+            if hash_info is not None:
+                hash_info = hash_info[:7]
+            row.append(hash_info or fill_value)
         yield row
 
 
@@ -245,6 +253,7 @@ def experiments_table(
     metric_names,
     param_headers,
     param_names,
+    deps_names,
     sort_by=None,
     sort_order=None,
     precision=DEFAULT_PRECISION,
@@ -256,7 +265,8 @@ def experiments_table(
     from dvc.compare import TabularData
 
     td = TabularData(
-        lconcat(headers, metric_headers, param_headers), fill_value=fill_value
+        lconcat(headers, metric_headers, param_headers, deps_names),
+        fill_value=fill_value,
     )
     for base_rev, experiments in all_experiments.items():
         rows = _collect_rows(
@@ -264,6 +274,7 @@ def experiments_table(
             experiments,
             metric_names,
             param_names,
+            deps_names,
             sort_by=sort_by,
             sort_order=sort_order,
             precision=precision,
@@ -310,7 +321,7 @@ def show_experiments(
 ):
     from funcy.seqs import flatten as flatten_list
 
-    metric_names, param_names = _collect_names(all_experiments)
+    metric_names, param_names, deps_names = _collect_names(all_experiments)
 
     headers = [
         "Experiment",
@@ -335,6 +346,7 @@ def show_experiments(
         metric_names,
         param_headers,
         param_names,
+        deps_names,
         kwargs.get("sort_by"),
         kwargs.get("sort_order"),
         kwargs.get("precision"),
@@ -359,14 +371,22 @@ def show_experiments(
         )
         td.drop(*merge_headers[1:])
 
-    headers = {"metrics": metric_headers, "params": param_headers}
+    headers = {
+        "metrics": metric_headers,
+        "params": param_headers,
+        "deps": deps_names,
+    }
     styles = {
         "Experiment": {"no_wrap": True, "header_style": "black on grey93"},
         "Created": {"header_style": "black on grey93"},
         "State": {"header_style": "black on grey93"},
         "Executor": {"header_style": "black on grey93"},
     }
-    header_bg_colors = {"metrics": "cornsilk1", "params": "light_cyan1"}
+    header_bg_colors = {
+        "metrics": "cornsilk1",
+        "params": "light_cyan1",
+        "deps": "plum2",
+    }
     styles.update(
         {
             header: {
diff --git a/dvc/repo/experiments/show.py b/dvc/repo/experiments/show.py
--- a/dvc/repo/experiments/show.py
+++ b/dvc/repo/experiments/show.py
@@ -23,6 +23,8 @@ def _collect_experiment_commit(
     running=None,
     onerror: Optional[Callable] = None,
 ):
+    from dvc.dependency import ParamsDependency, RepoDependency
+
     res: Dict[str, Optional[Any]] = defaultdict(dict)
     for rev in repo.brancher(revs=[exp_rev]):
         if rev == "workspace":
@@ -39,6 +41,26 @@ def _collect_experiment_commit(
         if params:
             res["params"] = params
 
+        res["deps"] = {
+            dep.def_path: {
+                "hash": dep.hash_info.value,
+                "size": dep.meta.size,
+                "nfiles": dep.meta.nfiles,
+            }
+            for dep in repo.index.deps
+            if not isinstance(dep, (ParamsDependency, RepoDependency))
+        }
+
+        res["outs"] = {
+            out.def_path: {
+                "hash": out.hash_info.value,
+                "size": out.meta.size,
+                "nfiles": out.meta.nfiles,
+            }
+            for out in repo.index.outs
+            if not (out.is_metric or out.is_plot)
+        }
+
         res["queued"] = stash
         if running is not None and exp_rev in running:
             res["running"] = True
@@ -111,6 +133,7 @@ def show(
     param_deps=False,
     onerror: Optional[Callable] = None,
 ):
+
     if onerror is None:
         onerror = onerror_collect
 


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
@@ -1,6 +1,7 @@
 import logging
 import os
 from datetime import datetime
+from unittest.mock import ANY
 
 import pytest
 from funcy import first, get_in
@@ -38,7 +39,15 @@ def test_show_simple(tmp_dir, scm, dvc, exp_stage):
     assert dvc.experiments.show()["workspace"] == {
         "baseline": {
             "data": {
+                "deps": {
+                    "copy.py": {
+                        "hash": ANY,
+                        "size": ANY,
+                        "nfiles": None,
+                    }
+                },
                 "metrics": {"metrics.yaml": {"data": {"foo": 1}}},
+                "outs": {},
                 "params": {"params.yaml": {"data": {"foo": 1}}},
                 "queued": False,
                 "running": False,
@@ -63,7 +72,15 @@ def test_show_experiment(tmp_dir, scm, dvc, exp_stage, workspace):
 
     expected_baseline = {
         "data": {
+            "deps": {
+                "copy.py": {
+                    "hash": ANY,
+                    "size": ANY,
+                    "nfiles": None,
+                }
+            },
             "metrics": {"metrics.yaml": {"data": {"foo": 1}}},
+            "outs": {},
             "params": {"params.yaml": {"data": {"foo": 1}}},
             "queued": False,
             "running": False,
@@ -318,8 +335,16 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
     assert dvc.experiments.show()["workspace"] == {
         "baseline": {
             "data": {
+                "deps": {
+                    "copy.py": {
+                        "hash": ANY,
+                        "size": ANY,
+                        "nfiles": None,
+                    }
+                },
                 "metrics": {"metrics.yaml": {"data": {"foo": 1}}},
                 "params": {"params.yaml": {"data": {"foo": 1}}},
+                "outs": {},
                 "queued": False,
                 "running": True,
                 "executor": info.location,
@@ -329,7 +354,7 @@ def test_show_running_workspace(tmp_dir, scm, dvc, exp_stage, capsys):
     }
 
     capsys.readouterr()
-    assert main(["exp", "show", "--no-pager"]) == 0
+    assert main(["exp", "show", "--csv"]) == 0
     cap = capsys.readouterr()
     assert "Running" in cap.out
     assert info.location in cap.out
@@ -450,26 +475,26 @@ def _get_rev_isotimestamp(rev):
     capsys.readouterr()
     assert main(["exp", "show", "--csv"]) == 0
     cap = capsys.readouterr()
+    data_dep = first(x for x in dvc.index.deps if "copy.py" in x.fspath)
+    data_hash = data_dep.hash_info.value[:7]
+    assert "Experiment,rev,typ,Created,parent" in cap.out
+    assert "metrics.yaml:foo,params.yaml:foo,copy.py" in cap.out
+    assert f",workspace,baseline,,,3,3,{data_hash}" in cap.out
     assert (
-        "Experiment,rev,typ,Created,parent,metrics.yaml:foo,params.yaml:foo"
-        in cap.out
-    )
-    assert ",workspace,baseline,,,3,3" in cap.out
-    assert (
-        "master,{},baseline,{},,1,1".format(
-            baseline_rev[:7], _get_rev_isotimestamp(baseline_rev)
+        "master,{},baseline,{},,1,1,{}".format(
+            baseline_rev[:7], _get_rev_isotimestamp(baseline_rev), data_hash
         )
         in cap.out
     )
     assert (
-        "{},{},branch_base,{},,2,2".format(
-            ref_info1.name, rev1[:7], _get_rev_isotimestamp(rev1)
+        "{},{},branch_base,{},,2,2,{}".format(
+            ref_info1.name, rev1[:7], _get_rev_isotimestamp(rev1), data_hash
         )
         in cap.out
     )
     assert (
-        "{},{},branch_commit,{},,3,3".format(
-            ref_info2.name, rev2[:7], _get_rev_isotimestamp(rev2)
+        "{},{},branch_commit,{},,3,3,{}".format(
+            ref_info2.name, rev2[:7], _get_rev_isotimestamp(rev2), data_hash
         )
         in cap.out
     )
@@ -596,3 +621,42 @@ def test_show_parallel_coordinates(tmp_dir, dvc, scm, mocker, capsys):
     html_text = (tmp_dir / "dvc_plots" / "index.html").read_text()
     assert '"label": "Created"' not in html_text
     assert '"label": "foobar"' not in html_text
+
+
+def test_show_outs(tmp_dir, dvc, scm):
+    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 && echo out > out",
+        metrics_no_cache=["metrics.yaml"],
+        params=["foo", "bar"],
+        name="copy-file",
+        deps=["copy.py"],
+        outs=["out"],
+    )
+    scm.add(
+        [
+            "dvc.yaml",
+            "dvc.lock",
+            "copy.py",
+            "params.yaml",
+            "metrics.yaml",
+            ".gitignore",
+        ]
+    )
+    scm.commit("init")
+
+    outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"]
+    assert outs == {
+        "out": {
+            "hash": ANY,
+            "size": ANY,
+            "nfiles": None,
+        }
+    }