Repository: EducationalTestingService/rsmtool (python)

Issue:
Update `intermediate_file_paths`notebooks
Clean up the `intermediate_file_paths` notebooks, by (1) providing more descriptive titles for some files (e.g. `'confMatrix' -> 'Confusion Matrix'`), and (2) grouping files into table cells.

Pull request description:
Update intermediate files notebook
This PR updates the intermediate files notebook to be more readable and closes #177. 

### Changes

- Add a new dictionary to `utils/constants.py` that contains custom descriptions for the intermediate files generated by `rsmtool` and `rsmsummarize`. We don't add an entry for `rsmeval` since the files are a strict subset of `rsmtool`'s files. We handle subgroups by setting a "ZZZ" placeholder which we can replace with the subgroup name when processing the description.
- Update `rsmtool.utils.notebook.show_files()` to take a context parameter and use that to look up the custom descriptions added to `rsmtool.utils.constants`.
- Update `rsmtool.utils.notebook.get_files_as_html()` to use the custom descriptions when available but fail over to using the filename components if not. Also, produce a table rather than a bulleted list as before with rows still sorted by filenames.
- Update `rsmtool` and `rsmsummarize` intermediate file notebooks to (a) call `show_files()` with the context as a parameter and (b) update the introductory text to be more useful.
- Update `reporter.py` to pass the context as an environment variable when generating `rsmsummarize` reports.
- Update the summary header notebook to load this environment variable and make it available to other notebooks.
- Update tests for `get_files_as_html()` to use tables as expected output.
- Update pre-commit checks to ignore line length errors (E501) in `rsmtool.utils.constants`.

### To review

- Run a few `rsmtool`, `rsmeval`, and `rsmsummarize` experiments from the test data and check that the intermediate file section in the notebook is using the custom descriptions and that the links work. Make sure to include experiments that have subgroups, use length, have a second human score, use `tsv`/`xlsx` output files, etc. 
- Suggest improvements to the HTML table, if you have any.
- Review the code changes themselves. 


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 24a02e85..2fd6a535 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -28,4 +28,4 @@ repos:
     rev: 'v0.0.285'
     hooks:
       - id: ruff
-        args: [--line-length=100, --select, "D,E,F,I", --ignore, "D212", --per-file-ignores, "tests/test*.py:D100,tests/test*.py:D101,tests/test*.py:D102,tests/test*.py:D103"]
+        args: [--line-length=100, --select, "D,E,F,I", --ignore, "D212", --per-file-ignores, "tests/test*.py:D100,tests/test*.py:D101,tests/test*.py:D102,tests/test*.py:D103,rsmtool/utils/constants.py:E501"]
diff --git a/rsmtool/notebooks/intermediate_file_paths.ipynb b/rsmtool/notebooks/intermediate_file_paths.ipynb
index e733e2c6..78f5990d 100644
--- a/rsmtool/notebooks/intermediate_file_paths.ipynb
+++ b/rsmtool/notebooks/intermediate_file_paths.ipynb
@@ -8,10 +8,31 @@
    ]
   },
   {
-   "cell_type": "markdown",
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# dynamically set the documentation link based on context\n",
+    "if context == \"rsmtool\":\n",
+    "    documentation_link = \"https://rsmtool.readthedocs.io/en/main/usage_rsmtool.html#intermediate-files\"\n",
+    "else:\n",
+    "    # this is rsmeval\n",
+    "    documentation_link = \"https://rsmtool.readthedocs.io/en/main/advanced_usage.html#intermediate-files\"\n",
+    "\n",
+    "markdown_text = (\"The following intermediate files were generated as part of this experiment.\"\n",
+    "\"To examine a file, click on the link. Note that the descriptions provided here are only brief \"\n",
+    "\"summaries. For more detailed descriptions, please refer to the \"\n",
+    "f\"[relevant RSMTool documentation]({documentation_link}).\")"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
    "metadata": {},
+   "outputs": [],
    "source": [
-    "Click on the hyperlinks below to see the intermediate experiment files generated as part of this experiment."
+    "display(Markdown(markdown_text))"
    ]
   },
   {
@@ -29,7 +50,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "show_files(output_dir, experiment_id, file_format)"
+    "show_files(context, output_dir, experiment_id, file_format)"
    ]
   }
  ],
diff --git a/rsmtool/notebooks/summary/header.ipynb b/rsmtool/notebooks/summary/header.ipynb
index d04f2034..dbcce492 100644
--- a/rsmtool/notebooks/summary/header.ipynb
+++ b/rsmtool/notebooks/summary/header.ipynb
@@ -153,6 +153,7 @@
    "source": [
     "# NOTE: you will need to set the following manually\n",
     "# if you are using this notebook interactively.\n",
+    "context = environ_config.get('CONTEXT')\n",
     "summary_id = environ_config.get('SUMMARY_ID')\n",
     "description = environ_config.get('DESCRIPTION')\n",
     "jsons = environ_config.get('JSONS')\n",
diff --git a/rsmtool/notebooks/summary/intermediate_file_paths.ipynb b/rsmtool/notebooks/summary/intermediate_file_paths.ipynb
index ce4aaf06..92dbf1d2 100644
--- a/rsmtool/notebooks/summary/intermediate_file_paths.ipynb
+++ b/rsmtool/notebooks/summary/intermediate_file_paths.ipynb
@@ -11,9 +11,9 @@
    "cell_type": "markdown",
    "metadata": {},
    "source": [
-    "Click on the hyperlinks below to see the intermediate experiment files generated as part of this summary. \n",
-    "\n",
-    "**Note**: This only includes the intermediate files generated by `rsmsummarize`. It does not include links to the intermediate files generated by the original experiment(s)."
+    "The following intermediate files were generated as part of this experiment. To examine a file, click on the link. Note that the descriptions provided here are only brief summaries. For more detailed descriptions, please refer to the [relevant RSMTool documentation](https://rsmtool.readthedocs.io/en/main/advanced_usage.html#intermediate-files-rsmsummarize).",
+    "<br/><br/>",
+    "**Note**: This list only includes the intermediate files generated by `rsmsummarize`, *not* the intermediate files generated by the original experiment(s)."
    ]
   },
   {
@@ -31,7 +31,7 @@
    "metadata": {},
    "outputs": [],
    "source": [
-    "show_files(output_dir, summary_id, file_format_summarize)"
+    "show_files(context, output_dir, summary_id, file_format_summarize)"
    ]
   }
  ],
diff --git a/rsmtool/reporter.py b/rsmtool/reporter.py
index d714dc0d..2dfd1fc2 100644
--- a/rsmtool/reporter.py
+++ b/rsmtool/reporter.py
@@ -865,6 +865,7 @@ class Reporter:
             "JSONS": all_experiments,
             "JAVASCRIPT_PATH": javascript_path,
             "OUTPUT_DIR": csvdir,
+            "CONTEXT": "rsmsummarize",
         }
 
         report_name = f"{config['summary_id']}_report"
diff --git a/rsmtool/utils/constants.py b/rsmtool/utils/constants.py
index aaef70c2..4f406c97 100644
--- a/rsmtool/utils/constants.py
+++ b/rsmtool/utils/constants.py
@@ -443,3 +443,82 @@ INTERACTIVE_MODE_METADATA = {
 
 # regular expression used to parse rsmexplain range values
 RSMEXPLAIN_RANGE_REGEXP = re.compile(r"^(?P<start>[0-9]+)\-(?P<end>[0-9]+)$")
+
+# replacement dictionary for intermediate file names to descriptive names
+INTERMEDIATE_FILES_TO_DESCRIPTIONS = {
+    "rsmtool": {
+        "betas": "Standardized & relative regression coefficients",
+        "coefficients": "Feature coefficients (including intercept)",
+        "coefficients_scaled": "Scaled feature coefficients (including intercept)",
+        "confMatrix": "Confusion matrix between human scores and machine scores",
+        "confMatrix_h1h2": "Confusion matrix between human-human scores",
+        "consistency": "Human-human agreement metrics",
+        "consistency_by_ZZZ": "Human-human agreement by ZZZ subgroup",
+        "cors_orig": "Correlations between raw features & between features and human score",
+        "cors_processed": "Correlations between pre-processed features & between features and human score",
+        "data_composition": "Data composition statistics",
+        "data_composition_by_ZZZ": "Data composition statistics by ZZZ subgroup",
+        "degradation": "Degradation in H-M performance compared to H-H",
+        "disattenuated_correlations": "Disattenuated correlations between various scores",
+        "disattenuated_correlations_by_ZZZ": "Disattenuated correlations by ZZZ subgroup",
+        "estimates_csd_by_ZZZ": "Conditional score differences by ZZZ subgroup",
+        "estimates_osa_by_ZZZ": "Overall score accuracy by ZZZ subgroup",
+        "estimates_osd_by_ZZZ": "Overall score differences by ZZZ subgroup",
+        "eval": "Full set of evaluation metrics",
+        "eval_by_ZZZ": "Evaluation metrics by ZZZ subgroup",
+        "eval_short": "Subset of more commonly used evaluation metrics",
+        "fairness_metrics_by_ZZZ": "R^2 and p-values for all fairness models by ZZZ subgroup",
+        "feature": "Feature values, signs, and transforms used in the final model",
+        "feature_descriptives": "Main Descriptive statistics for raw feature values",
+        "feature_descriptivesExtra": "Additional descriptive statistics for raw feature values",
+        "feature_outliers": "Number and percentage of outliers trunctated during feature pre-processing",
+        "margcor_length_all_data": "Marginal correlations between each pre-processed feature and length",
+        "margcor_length_by_ZZZ": "Marginal feature-length correlations by ZZZ subgroup",
+        "margcor_score_all_data": "Marginal correlations between each pre-processed feature and human score",
+        "margcor_score_by_ZZZ": "Marginal feature-score correlations by ZZZ subgroup",
+        "model_fit": "R^2 and adjusted R^2 computed on the training set",
+        "pca": "Results of principal components analysis on the pre-processed feature values",
+        "pcavar": "Eigenvalues and variance explained by each principal component",
+        "pcor_length_all_data": "Partial correlations between each pre-processed feature and length, controlling for other features",
+        "pcor_length_by_ZZZ": "Partial feature-length correlations by ZZZ subgroup, controlling for other features",
+        "pcor_score_all_data": "Partial correlations between each pre-processed feature & human score, controlling for other features",
+        "pcor_score_no_length_all_data": "Partial feature-score correlations, also controlling for length",
+        "pcor_score_by_ZZZ": "Partial feature-score correlations by ZZZ subgroup, controlling for other features",
+        "pcor_score_no_length_by_ZZZ": "Partial feature-score correlations by ZZZ subgroup, also controlling for length",
+        "postprocessing_params": "Parameters used for trimming and scaling predicted scores",
+        "pred_processed": "Predicted scores for the evaluation set",
+        "pred_train": "Predicted scores for the training set",
+        "score_dist": "Distributions of human and machine scores",
+        "test_excluded_composition": "Composition of evaluation set responses excluded from analysis",
+        "test_excluded_responses": "Actual evaluation set responses excluded from analysis",
+        "test_features": "Raw feature values for the evaluation set",
+        "test_human_scores": "Human scores for the evaluation set",
+        "test_metadata": "Metadata columns for the evaluation set",
+        "test_other_columns": "Unused columns for the evaluation set",
+        "test_preprocessed_features": "Pre-processed feature values for the evaluation set",
+        "test_responses_with_excluded_flags": "Evaluation set responses filtered out via `flag_column`",
+        "train_excluded_composition": "Composition of training set responses excluded from analysis",
+        "train_excluded_responses": "Actual training set responses excluded from analysis",
+        "train_features": "Raw feature values for the training set",
+        "train_metadata": "Metadata columns for the training set",
+        "train_missing_feature_values": "Number of non-numeric values for each feature in training set",
+        "train_other_columns": "Unused columns for the training set",
+        "train_preprocessed_features": "Pre-processed feature values for the training set",
+        "train_response_lengths": "Values of `length` column for training set",
+        "train_responses_with_excluded_flags": "Training set responses filtered out via `flag_column`",
+        "true_score_eval": "Metrics evaluating how well system scores predict true scores",
+    },
+    "rsmsummarize": {
+        "betas": "Standardized coefficients for each experiment",
+        "eval_short": "More commonly used evaluation metrics for each experiment",
+        "margcor_score_all_data": "Marginal correlations between pre-processed features and human score for each experiment",
+        "model_fit": "R^2 and adjusted R^2 computed on the training set for each experiment",
+        "model_summary": "Summary of models used for each experiment",
+        "pcor_score_all_data": "Partial feature-score correlations features for each experiment, controlling for other features",
+        "pcor_score_no_length_all_data": "Partial feature-score correlations features for each experiment, also controlling for length",
+        "true_score_eval": "Metrics evaluating how well system scores predict true scores for each experiment",
+    },
+}
+
+# rsmeval has the same intermediate files as rsmtool
+INTERMEDIATE_FILES_TO_DESCRIPTIONS["rsmeval"] = INTERMEDIATE_FILES_TO_DESCRIPTIONS["rsmtool"]
diff --git a/rsmtool/utils/notebook.py b/rsmtool/utils/notebook.py
index 235ab930..e3106890 100644
--- a/rsmtool/utils/notebook.py
+++ b/rsmtool/utils/notebook.py
@@ -8,6 +8,7 @@ Utility functions for use in RSMTool sections/notebooks.
 :organization: ETS
 """
 
+import re
 from math import ceil
 from os.path import exists, isabs, relpath
 from pathlib import Path
@@ -16,7 +17,16 @@ from textwrap import wrap
 
 from IPython.display import HTML, display
 
-HTML_STRING = """<li><b>{}</b>: <a href="{}" download>{}</a></li>"""
+from .constants import INTERMEDIATE_FILES_TO_DESCRIPTIONS
+
+INTERMEDIATE_TABLE_ROW_STRING = """
+<tr>
+    <td style="text-align:left;">
+        <a href="{}" style="text-decoration: none;" download>{}</a>
+    </td>
+    <td style="text-align:left;">{}</td>
+</tr>
+"""
 
 
 def float_format_func(num, prec=3, scientific=False):
@@ -362,47 +372,84 @@ def get_files_as_html(output_dir, experiment_id, file_format, replace_dict={}):
     """
     output_dir = Path(output_dir)
     parent_dir = output_dir.parent
-    files = output_dir.glob(f"*.{file_format}")
-    html_string = ""
-    for file in sorted(files):
+
+    # get the list of intermediate files generated for this experiment
+    files = list(output_dir.glob(f"*.{file_format}"))
+
+    # get the bare filenames for each file since we will need those for sorting
+    filenames = [file.stem.replace(f"{experiment_id}_", "") for file in files]
+
+    # initialize list to hold the generated HTML strings
+    html_strings = [
+        """
+        <tr>
+            <th style="text-align:left;">Filename</th>
+            <th style="text-align:left;">Description</th>
+        </tr>
+        """
+    ]
+
+    # iterate over the files and generate the HTML strings
+    for filename, file in sorted(zip(filenames, files)):
+        # get the path of the file relative to the parent of the "output"
+        # directory; these will be the targets of our links
         relative_file = ".." / file.relative_to(parent_dir)
+
+        # get the relative names of the file without the experiment id prefix
+        # these are needed in case we do not have a pre-defined replacement
         relative_name = relative_file.stem.replace(f"{experiment_id}_", "")
 
-        # check if relative name is in the replacement dictionary and,
-        # if it is, use the more descriptive name in the replacement
-        # dictionary. Otherwise, normalize the file name and use that
-        # as the description instead.
-        if relative_name in replace_dict:
-            descriptive_name = replace_dict[relative_name]
-        else:
+        # if the name of the file contains "_by_<subgroup>", replace
+        # the subgroup with "ZZZ" to get the replacement key; otherwise,
+        # use the filename as the key; if neither form of the  key is
+        # found in the dictionary, use the filename itself as the description
+        try:
+            if m := re.search(r"_by_([^\._]+)", filename):
+                subgroup = m.group(1)
+                replacement_key = filename.replace(f"_by_{subgroup}", "_by_ZZZ")
+                # replace the "ZZZ" with the actual subgroup in the description
+                descriptive_name = replace_dict[replacement_key].replace("ZZZ", subgroup)
+            else:
+                replacement_key = filename
+                descriptive_name = replace_dict[filename]
+        except KeyError:
             descriptive_name_components = relative_name.split("_")
             descriptive_name = " ".join(descriptive_name_components).title()
 
-        html_string += HTML_STRING.format(descriptive_name, relative_file, file_format)
+        # generate a table row string for this file
+        html_strings.append(
+            INTERMEDIATE_TABLE_ROW_STRING.format(relative_file, filename, descriptive_name)
+        )
 
-    return """<ul>""" + html_string + """</ul>"""
+    # combine the HTML table row strings into a single HTML table
+    table_rows = "\n".join(html_strings)
+    return f"<table>{table_rows}</table>"
 
 
-def show_files(output_dir, experiment_id, file_format, replace_dict={}):
+def show_files(context, output_dir, experiment_id, file_format):
     """
-    Show files in a given directory as an HTML list in a Jupyter notebook.
+    Show files for given context and directory as a table in a Jupyter notebook.
 
     Parameters
     ----------
+    context: str
+        The tool context: one of {"rsmtool", "rsmeval", "rsmsummarize"}.
     output_dir : str
         The output directory.
     experiment_id : str
         The experiment ID.
     file_format : str
         The format of the output files.
-    replace_dict : dict, optional
-        A dictionary which makes file names to descriptions.
-        Defaults to ``{}``.
 
     Displays
     --------
     display : IPython.core.display.HTML
         The HTML file descriptions and links.
     """
-    html_string = get_files_as_html(output_dir, experiment_id, file_format, replace_dict)
+    html_string = get_files_as_html(
+        output_dir,
+        experiment_id,
+        file_format,
+        replace_dict=INTERMEDIATE_FILES_TO_DESCRIPTIONS[context],
+    )
     display(HTML(html_string))


Test patch:
diff --git a/tests/data/output/lr_data_composition_by_L1.csv b/tests/data/output/lr_data_composition_by_L1.csv
new file mode 100644
index 00000000..f5e273fa
--- /dev/null
+++ b/tests/data/output/lr_data_composition_by_L1.csv
@@ -0,0 +1,5 @@
+L1,Training set,Evaluation set
+Esperanto,127,42
+Klingon,134,46
+Navi,112,50
+Vulcan,127,62
diff --git a/tests/test_utils.py b/tests/test_utils.py
index e03a4037..2218022a 100644
--- a/tests/test_utils.py
+++ b/tests/test_utils.py
@@ -35,7 +35,12 @@ from rsmtool.utils.commandline import (
     InteractiveField,
     setup_rsmcmd_parser,
 )
-from rsmtool.utils.constants import CHECK_FIELDS, DEFAULTS, INTERACTIVE_MODE_METADATA
+from rsmtool.utils.constants import (
+    CHECK_FIELDS,
+    DEFAULTS,
+    INTERACTIVE_MODE_METADATA,
+    INTERMEDIATE_FILES_TO_DESCRIPTIONS,
+)
 from rsmtool.utils.conversion import convert_to_float, int_to_float, parse_range
 from rsmtool.utils.cross_validation import (
     combine_fold_prediction_files,
@@ -63,6 +68,7 @@ from rsmtool.utils.notebook import (
     get_files_as_html,
     get_thumbnail_as_html,
     int_or_float_format_func,
+    show_files,
 )
 from rsmtool.writer import DataWriter
 
@@ -858,44 +864,128 @@ class TestCrossValidation(unittest.TestCase):
 
 
 class TestIntermediateFiles(unittest.TestCase):
-    def get_files(self, file_format="csv"):
+    def check_get_files_as_html(self, expected_html_string, file_format="csv", replace_dict={}):
         directory = join(rsmtool_test_dir, "data", "output")
         files = sorted([f for f in listdir(directory) if f.endswith(file_format)])
-        return files, directory
-
-    def test_get_files_as_html(self):
-        files, directory = self.get_files()
-        html_string = (
-            """<li><b>Betas</b>: <a href="{}" download>csv</a></li>"""
-            """<li><b>Eval</b>: <a href="{}" download>csv</a></li>"""
-        )
-
-        html_expected = html_string.format(
-            join("..", "output", files[0]), join("..", "output", files[1])
+        html_expected = expected_html_string.format(
+            join("..", "output", files[0]),
+            join("..", "output", files[1]),
+            join("..", "output", files[2]),
         )
         html_expected = "".join(html_expected.strip().split())
-        html_expected = """<ul>""" + html_expected + """</ul>"""
-        html_result = get_files_as_html(directory, "lr", "csv")
-        html_result = "".join(html_result.strip().split())
-        self.assertEqual(html_expected, html_result)
-
-    def test_get_files_as_html_replace_dict(self):
-        files, directory = self.get_files()
-        html_string = (
-            """<li><b>THESE BETAS</b>: <a href="{}" download>csv</a></li>"""
-            """<li><b>THESE EVALS</b>: <a href="{}" download>csv</a></li>"""
-        )
+        html_expected = """<table>""" + html_expected + """</table>"""
+        html_computed = get_files_as_html(directory, "lr", "csv", replace_dict=replace_dict)
+        html_computed = "".join(html_computed.strip().split())
+        self.assertEqual(html_expected, html_computed)
 
-        replace_dict = {"betas": "THESE BETAS", "eval": "THESE EVALS"}
-        html_expected = html_string.format(
-            join("..", "output", files[0]), join("..", "output", files[1])
+    def test_get_files_as_html(self):
+        html_string = """
+            <tr>
+                <th style="text-align:left;">Filename</th>
+                <th style="text-align:left;">Description</th>
+            </tr>
+            <tr>
+                <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>betas</a>
+               </td>
+               <td style="text-align:left;">Betas</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>data_composition_by_L1</a>
+               </td>
+               <td style="text-align:left;">Data Composition By L1</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>eval</a>
+               </td>
+               <td style="text-align:left;">Eval</td>
+            </tr>
+            """
+        self.check_get_files_as_html(html_string)
+
+    def test_get_files_as_html_replace_dict_default(self):
+        html_string = """
+            <tr>
+                <th style="text-align:left;">Filename</th>
+                <th style="text-align:left;">Description</th>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>betas</a>
+               </td>
+               <td style="text-align:left;">Standardized & relative regression coefficients</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>data_composition_by_L1</a>
+               </td>
+               <td style="text-align:left;">Data composition statistics by L1 subgroup</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>eval</a>
+               </td>
+               <td style="text-align:left;">Full set of evaluation metrics</td>
+            </tr>
+            """
+
+        self.check_get_files_as_html(
+            html_string, replace_dict=INTERMEDIATE_FILES_TO_DESCRIPTIONS["rsmtool"]
         )
-        html_expected = "".join(html_expected.strip().split())
-        html_expected = """<ul>""" + html_expected + """</ul>"""
-        html_result = get_files_as_html(directory, "lr", "csv", replace_dict)
-        html_result = "".join(item for item in html_result)
-        html_result = "".join(html_result.strip().split())
-        self.assertEqual(html_expected, html_result)
+
+    def test_get_files_as_html_replace_dict_custom(self):
+        html_string = """
+            <tr>
+                <th style="text-align:left;">Filename</th>
+                <th style="text-align:left;">Description</th>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>betas</a>
+               </td>
+               <td style="text-align:left;">These are betas</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>data_composition_by_L1</a>
+               </td>
+               <td style="text-align:left;">These are data composition by L1</td>
+            </tr>
+            <tr>
+               <td style="text-align:left;">
+                   <a href="{}" style="text-decoration: none;" download>eval</a>
+               </td>
+               <td style="text-align:left;">These are eval metrics</td>
+            </tr>
+            """
+
+        replace_dict = {
+            "betas": "These are betas",
+            "data_composition_by_ZZZ": "These are data composition by ZZZ",
+            "eval": "These are eval metrics",
+        }
+        self.check_get_files_as_html(html_string, replace_dict=replace_dict)
+
+    def test_show_files_rsmeval(self):
+        # mock the `get_files_as_html()` function
+        with patch("rsmtool.utils.notebook.get_files_as_html", return_value="") as mock_get_files:
+            # call `show_files()` which will call `get_files_as_html()`
+            directory = join(rsmtool_test_dir, "data", "output")
+            _ = show_files("rsmeval", directory, "lr", "csv")
+
+            # check that the function was called with the expected arguments
+            # including the fact that replacement dictionary context is set
+            # to "rsmtool" when "rsmeval" is passed as expected
+            mock_get_files.assert_called_once()
+            self.assertEqual(mock_get_files.call_args[0][0], directory)
+            self.assertEqual(mock_get_files.call_args[0][1], "lr")
+            self.assertEqual(mock_get_files.call_args[0][2], "csv")
+            self.assertEqual(
+                mock_get_files.call_args[1]["replace_dict"],
+                INTERMEDIATE_FILES_TO_DESCRIPTIONS["rsmeval"],
+            )
 
 
 class TestThumbnail(unittest.TestCase):