Repository: iterative/dvc

Issue:
`dvc exp show`'s table: shifts params values to metrics columns after renaming metrics file
# Bug Report

`dvc exp show -A` or `dvc show -n N` shows values of params in wrong columns.

## Description

**Phase 1.** I had a simple training stage in `pipelines/example/dvc.yaml` without metrics tracking, but with params tracking.

```yaml
dummy_train:
    wdir: ../..
    cmd: python scripts/example/dummy_train.py
    deps:
    - scripts/example/dummy_train.py
    params:
    - pipelines/example/params.yaml:
      - test.value
    - pipelines/example/custom_params.yaml:
      - custom.value
```
**Phase 2.** I ran some experiments and then introduced metrics file `pipelines/example/scores.json`.

```yaml
dummy_train:
    wdir: ../..
    cmd: python scripts/example/dummy_train.py
    deps:
    - scripts/example/dummy_train.py
    params:
    - pipelines/example/params.yaml:
      - test.value
    - pipelines/example/custom_params.yaml:
      - custom.value
    metrics:
    - pipelines/example/scores.json:
        cache: false
```

**Phase 3.** I used this stage to run some more experiments and at some point I removed `pipelines/example/scores.json` and added `pipelines/example/metrics.json` instead and the ran further experiments.

**Results.** Now, when I use `dvc exp show -A` I see values from params' columns in the columns for metrics `pipelines/example/scores.json` beginning from the timepoint when I introduced metrics in experiments (phase 2). Params `test.value` and `custom.value` are shown in columns  ` pipelines/example/scores.json:mean_acc` and ` pipelines/example/scores.json:mean_loss`.

![Screenshot from 2021-12-01 12-01-24](https://user-images.githubusercontent.com/45446581/144224925-b5835dc4-1a5a-41af-b5ea-874b85b53d48.png)

This happens only if the list of experiments is "long enough". As soon as I use, e.g `dvc exp show -n 8` everything is fine. This matches only the experiments from phase 3, where the obsolete `pipelines/example/scores.json` was already removed from the stage and Git repository.

![Screenshot from 2021-12-01 12-07-09](https://user-images.githubusercontent.com/45446581/144226839-5749d6e6-a596-44fe-a512-ccba5c078002.png)


### Reproduce

1. Create  a stage in `pipelines/example/dvc.yaml` as described in phase 2.
2. `dvc repro pipelines/example/dvc.yaml`
3. `git add pipelines/example/` and `git commit`
4. Amend `dvc.yaml` as said in phase 3 by renaming `scores.json` to `metrics.json`
5. `dvc repro pipelines/example/dvc.yaml`
6. `git rm pipelines/example/scores.json`
7. `git add pipelines/example/`
8. `dvc exp  show  -A`

### Expected

Expected to see values of params in columns for params as show in the 2nd image above.

### Environment information

**Output of `dvc doctor`:**

```console
DVC version: 2.8.3 (pip)
---------------------------------
Platform: Python 3.8.10 on Linux-5.8.0-63-generic-x86_64-with-glibc2.32
Supports:
	hdfs (fsspec = 2021.11.0, pyarrow = 5.0.0),
	webhdfs (fsspec = 2021.11.0),
	http (aiohttp = 3.7.4.post0, aiohttp-retry = 2.4.6),
	https (aiohttp = 3.7.4.post0, aiohttp-retry = 2.4.6),
	ssh (sshfs = 2021.11.1)
Cache types: hardlink, symlink
Cache directory: ext4 on /dev/nvme0n1p3
Caches: local
Remotes: ssh, ssh
Workspace directory: ext4 on /dev/nvme0n1p3
Repo: dvc, git
```




Discussion:
I have now experienced that the described shift in the table does not only happen when renaming files, but even when introducing  new metrics. For runs with less metrics, there will be a shift in column values, making it completely unreadable.
Hi @agrizzli ! I'm going to try to reproduce and get back to you
Able to reproduce. The bug also affects a few other scenarios

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
@@ -65,6 +65,9 @@ def _collect_names(all_experiments, **kwargs):
 def _collect_rows(
     base_rev,
     experiments,
+    all_headers,
+    metric_headers,
+    param_headers,
     metric_names,
     param_names,
     deps_names,
@@ -87,14 +90,18 @@ def _collect_rows(
 
     new_checkpoint = True
     for i, (rev, results) in enumerate(experiments.items()):
+        fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
+        row_dict = {k: fill_value for k in all_headers}
+
         exp = results.get("data", {})
+
         if exp.get("running"):
             state = "Running"
         elif exp.get("queued"):
             state = "Queued"
         else:
             state = fill_value
-        executor = exp.get("executor", fill_value)
+
         is_baseline = rev == "baseline"
 
         if is_baseline:
@@ -102,9 +109,7 @@ def _collect_rows(
         else:
             name_rev = rev[:7]
 
-        exp_name = exp.get("name", "")
         tip = exp.get("checkpoint_tip")
-
         parent_rev = exp.get("checkpoint_parent", "")
         parent_exp = experiments.get(parent_rev, {}).get("data", {})
         parent_tip = parent_exp.get("checkpoint_tip")
@@ -130,26 +135,28 @@ def _collect_rows(
         if not is_baseline:
             new_checkpoint = not (tip and tip == parent_tip)
 
-        row = [
-            exp_name,
-            name_rev,
-            typ,
-            _format_time(exp.get("timestamp"), fill_value, iso),
-            parent,
-            state,
-            executor,
-        ]
-        fill_value = FILL_VALUE_ERRORED if results.get("error") else fill_value
+        row_dict["Experiment"] = exp.get("name", "")
+        row_dict["rev"] = name_rev
+        row_dict["typ"] = typ
+        row_dict["Created"] = _format_time(
+            exp.get("timestamp"), fill_value, iso
+        )
+        row_dict["parent"] = parent
+        row_dict["State"] = state
+        row_dict["Executor"] = exp.get("executor", fill_value)
+
         _extend_row(
-            row,
+            row_dict,
             metric_names,
+            metric_headers,
             exp.get("metrics", {}).items(),
             precision,
             fill_value=fill_value,
         )
         _extend_row(
-            row,
+            row_dict,
             param_names,
+            param_headers,
             exp.get("params", {}).items(),
             precision,
             fill_value=fill_value,
@@ -158,8 +165,8 @@ def _collect_rows(
             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
+            row_dict[dep] = hash_info
+        yield list(row_dict.values())
 
 
 def _sort_column(sort_by, metric_names, param_names):
@@ -225,13 +232,9 @@ def _format_time(datetime_obj, fill_value=FILL_VALUE, iso=False):
     return datetime_obj.strftime(fmt)
 
 
-def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
+def _extend_row(row, names, headers, items, precision, fill_value=FILL_VALUE):
     from dvc.compare import _format_field, with_value
 
-    if not items:
-        row.extend(fill_value for keys in names.values() for _ in keys)
-        return
-
     for fname, data in items:
         item = data.get("data", {})
         item = flatten(item) if isinstance(item, dict) else {fname: item}
@@ -243,7 +246,11 @@ def _extend_row(row, names, items, precision, fill_value=FILL_VALUE):
             # wrap field data in ui.rich_text, otherwise rich may
             # interpret unescaped braces from list/dict types as rich
             # markup tags
-            row.append(ui.rich_text(str(_format_field(value, precision))))
+            value = ui.rich_text(str(_format_field(value, precision)))
+            if name in headers:
+                row[name] = value
+            else:
+                row[f"{fname}:{name}"] = value
 
 
 def experiments_table(
@@ -264,14 +271,15 @@ def experiments_table(
 
     from dvc.compare import TabularData
 
-    td = TabularData(
-        lconcat(headers, metric_headers, param_headers, deps_names),
-        fill_value=fill_value,
-    )
+    all_headers = lconcat(headers, metric_headers, param_headers, deps_names)
+    td = TabularData(all_headers, fill_value=fill_value)
     for base_rev, experiments in all_experiments.items():
         rows = _collect_rows(
             base_rev,
             experiments,
+            all_headers,
+            metric_headers,
+            param_headers,
             metric_names,
             param_names,
             deps_names,


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
@@ -640,6 +640,34 @@ def test_show_outs(tmp_dir, dvc, scm):
         deps=["copy.py"],
         outs=["out"],
     )
+
+    scm.commit("init")
+
+    outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"]
+    assert outs == {
+        "out": {
+            "hash": ANY,
+            "size": ANY,
+            "nfiles": None,
+        }
+    }
+
+
+def test_metrics_renaming(tmp_dir, dvc, scm, capsys):
+    tmp_dir.gen("copy.py", COPY_SCRIPT)
+    params_file = tmp_dir / "params.yaml"
+    params_data = {
+        "foo": 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"],
+        name="copy-file",
+        deps=["copy.py"],
+    )
     scm.add(
         [
             "dvc.yaml",
@@ -650,16 +678,49 @@ def test_show_outs(tmp_dir, dvc, scm):
             ".gitignore",
         ]
     )
-    scm.commit("init")
 
-    outs = dvc.experiments.show()["workspace"]["baseline"]["data"]["outs"]
-    assert outs == {
-        "out": {
-            "hash": ANY,
-            "size": ANY,
-            "nfiles": None,
-        }
-    }
+    scm.commit("metrics.yaml")
+    metrics_rev = scm.get_rev()
+
+    dvc.run(
+        cmd="python copy.py params.yaml scores.yaml",
+        metrics_no_cache=["scores.yaml"],
+        params=["foo"],
+        name="copy-file",
+        deps=["copy.py"],
+    )
+    scm.add(
+        [
+            "dvc.yaml",
+            "dvc.lock",
+            "params.yaml",
+            "scores.yaml",
+        ]
+    )
+    scm.commit("scores.yaml")
+    scores_rev = scm.get_rev()
+
+    capsys.readouterr()
+    assert main(["exp", "show", "--csv", "-A"]) == 0
+    cap = capsys.readouterr()
+
+    def _get_rev_isotimestamp(rev):
+        return datetime.fromtimestamp(
+            scm.gitpython.repo.rev_parse(rev).committed_date
+        ).isoformat()
+
+    assert (
+        "master,{},baseline,{},,1,,1".format(
+            scores_rev[:7], _get_rev_isotimestamp(scores_rev)
+        )
+        in cap.out
+    )
+    assert (
+        ",{},baseline,{},,,1,1".format(
+            metrics_rev[:7], _get_rev_isotimestamp(metrics_rev)
+        )
+        in cap.out
+    )
 
 
 def test_show_sorted_deps(tmp_dir, dvc, scm, capsys):