Repository: EducationalTestingService/rsmtool
Pull request #681 (2024-02-05 17:05:56)

Add type hints (Part 3)
### Changes

- Add `statsmodels` intersphinx mapping for documentation.
- add type hints to `fairness_utils.py`.
- add type hints to `modeler.py`.
- add type hints to `preprocessor.py`.
- add type hints to `reader.py`
- Remove incorrect exceptions in some tests based on changes in `reader.py`. 

Note that in this MR, I also did some refactoring:
- Removed unused `id_column` argument in `FeaturePreprocessor.filter_on_column()`. 
- Changed the behavior of `DataReader.locate_files()` to always return list of string instead of either a string or a list of strings + `None`s which was confusing and also caused issues when adding type hints. 
- I also had to change a couple of tests to check for the raising of the correct exception type based on the changes in `DataReader`.

### How to review
- Please check that I have not left any parameters/arguments/return values without type hints.
- Make sure that I didn't screw up anything when doing the refactoring. 
- Here's the [RTD build for this branch](https://rsmtool.readthedocs.io/en/326-type-hints-3). Check the "API Documentation" section (only for the files affected by this MR) to confirm that all types are linked to relevant pages from external documentations (e.g., Python, Numpy, SKLL, stats models etc.)
- Check for typos and/or weird issues in docstrings.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) Report
Attention: `3 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`8ee9dbf`)](https://app.codecov.io/gh/EducationalTestingService/rsmtool/commit/8ee9dbfdf472620ac0215e8ff5ea9a651bce2c9b?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) 96.14% compared to head [(`4e11936`)](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) 96.10%.

| [Files](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) | Patch % | Lines |
|---|---|---|
| [rsmtool/modeler.py](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService#diff-cnNtdG9vbC9tb2RlbGVyLnB5) | 96.29% | [2 Missing :warning: ](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) |
| [rsmtool/reader.py](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService#diff-cnNtdG9vbC9yZWFkZXIucHk=) | 95.00% | [1 Missing :warning: ](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##             main     #681      +/-   ##
==========================================
- Coverage   96.14%   96.10%   -0.04%     
==========================================
  Files          32       32              
  Lines        4483     4490       +7     
==========================================
+ Hits         4310     4315       +5     
- Misses        173      175       +2     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/EducationalTestingService/rsmtool/pull/681?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=EducationalTestingService).

desilinguist: We can ignore the coverage failure. 

Patch:
diff --git a/doc/conf.py b/doc/conf.py
index 2718b01b..bd4cc8e9 100644
--- a/doc/conf.py
+++ b/doc/conf.py
@@ -52,6 +52,7 @@ intersphinx_mapping = {
     "skll": ("https://skll.readthedocs.io/en/latest", None),
     "pandas": ("https://pandas.pydata.org/pandas-docs/stable/", None),
     "numpy": ("https://docs.scipy.org/doc/numpy/", None),
+    "statsmodels": ("https://www.statsmodels.org/stable/", None),
 }
 
 # some settings for sphinx_autodoc_typehints
diff --git a/rsmtool/analyzer.py b/rsmtool/analyzer.py
index 2ae9c436..57431fd1 100644
--- a/rsmtool/analyzer.py
+++ b/rsmtool/analyzer.py
@@ -854,7 +854,7 @@ class Analyzer:
 
         Returns
         -------
-        df_correlations: pandas DataFrame
+        df_correlations: pandas.DataFrame
             Data frame containing the human-system correlations, human-human
             correlations, and disattenuated correlations.
         """
diff --git a/rsmtool/comparer.py b/rsmtool/comparer.py
index 155bf8cf..7b493542 100644
--- a/rsmtool/comparer.py
+++ b/rsmtool/comparer.py
@@ -219,7 +219,7 @@ class Comparer:
 
         Returns
         -------
-        df_correlations: pandas DataFrame
+        df_correlations: pandas.DataFrame
             Data frame with a row for each feature and the following columns:
                 - "N": total number of responses
                 - "human_old": correlation with human score in the old frame
diff --git a/rsmtool/configuration_parser.py b/rsmtool/configuration_parser.py
index bf50a2d4..cc0bb403 100644
--- a/rsmtool/configuration_parser.py
+++ b/rsmtool/configuration_parser.py
@@ -551,7 +551,9 @@ class Configuration:
                     )
         return new_filter_dict
 
-    def get_trim_min_max_tolerance(self) -> Tuple[float, float, float]:
+    def get_trim_min_max_tolerance(
+        self,
+    ) -> Tuple[Optional[float], Optional[float], Optional[float]]:
         """
         Get trim min, trim max, and tolerance values.
 
@@ -560,11 +562,11 @@ class Configuration:
 
         Returns
         -------
-        spec_trim_min : float
+        spec_trim_min : Optional[float]
             Specified trim min value.
-        spec_trim_max : float
+        spec_trim_max : Optional[float]
             Specified trim max value.
-        spec_trim_tolerance: float
+        spec_trim_tolerance: Optional[float]
             Specified trim tolerance value.
         """
         config = self._config
diff --git a/rsmtool/fairness_utils.py b/rsmtool/fairness_utils.py
index d8fe99a8..ebd4cb37 100644
--- a/rsmtool/fairness_utils.py
+++ b/rsmtool/fairness_utils.py
@@ -11,16 +11,20 @@ Classes and functions related to computing fairness evaluations.
 import pickle
 import warnings
 from os.path import join
+from typing import Dict, Optional, Tuple
 
 import pandas as pd
 import statsmodels.formula.api as smf
+from statsmodels.regression.linear_model import RegressionResults
 from statsmodels.stats.anova import anova_lm
 
-from rsmtool.container import DataContainer
+from rsmtool.container import DataContainer, DatasetDict
 from rsmtool.writer import DataWriter
 
 
-def convert_to_ordered_category(group_values, base_group=None):
+def convert_to_ordered_category(
+    group_values: pd.Series, base_group: Optional[str] = None
+) -> pd.Series:
     """
     Convert given series to an ordered category.
 
@@ -30,17 +34,16 @@ def convert_to_ordered_category(group_values, base_group=None):
 
     Parameters
     ----------
-    group_values : pandas Series
+    group_values : pandas.Series
         A series indicating group membership
 
-    base_group : str, optional
-        The group to use as the first category.
-        This overrides the default ordering.
+    base_group : Optional[str]
+        The group to use as the first category which overrides the default ordering.
         Defaults to ``None``.
 
     Returns
     -------
-    group_ordered_category : pandas Series
+    group_ordered_category : pandas.Series
         The ordered category.
 
     Raises
@@ -79,7 +82,7 @@ def convert_to_ordered_category(group_values, base_group=None):
     return group_ordered_category
 
 
-def get_coefficients(fit, base_category):
+def get_coefficients(fit: RegressionResults, base_category: str) -> pd.DataFrame:
     """
     Extract estimates, significance, and confidence intervals for a given group.
 
@@ -87,18 +90,17 @@ def get_coefficients(fit, base_category):
     prefix added by ``statmodels``. The name of the base category
     is added in parenthesis to the Intercept.
 
-    Parameters:
-    -----------
-    fit: pandas.stats.ols.OLS
+    Parameters
+    ----------
+    fit: statsmodels.regression.linear_model.RegressionResults
         Linear regression object fitted using ``statsmodels``.
 
     base_category: str
-        Name of the group used as reference category when
-        fitting the model.
+        Name of the group used as reference category when fitting the model.
 
-    Returns:
+    Returns
     -------
-    df_results: pandas DataFrame
+    df_results: pandas.DataFrame
         A dataframe with rows for each category and the following columns:
         - "estimate"
         - "P>[t]"
@@ -125,16 +127,21 @@ def get_coefficients(fit, base_category):
 
 
 def write_fairness_results(
-    fit_dictionary, fairness_container, group, output_dir, experiment_id, file_format
-):
+    fit_dictionary: Dict[str, RegressionResults],
+    fairness_container: DataContainer,
+    group: str,
+    output_dir: str,
+    experiment_id: str,
+    file_format: str,
+) -> None:
     """
     Save the results of fairness analysis to disk.
 
     Parameters
     ----------
-    fit_dictionary: dict
+    fit_dictionary: Dict[str, RegressionResults]
         A dictionary of fitted models generated by ``get_fairness_analyses()``.
-    fairness_container: container.DataContainer
+    fairness_container: DataContainer
         A data container with the results of fairness analysis generated by
         ``get_fairness_analyses()``.
     group: str
@@ -142,7 +149,7 @@ def write_fairness_results(
     output_dir: str
         The directory where the results will be saved.
     experiment_id: str
-        Experiment ID.
+        The experiment ID.
     file_format: str
         File format to use for data files.
     """
@@ -164,8 +171,12 @@ def write_fairness_results(
 
 
 def get_fairness_analyses(
-    df, group, system_score_column, human_score_column="sc1", base_group=None
-):
+    df: pd.DataFrame,
+    group: str,
+    system_score_column: str,
+    human_score_column: str = "sc1",
+    base_group: Optional[str] = None,
+) -> Tuple[Dict[str, RegressionResults], DataContainer]:
     """
     Compute analyses from `Loukina et al. 2019 <https://aclanthology.org/W19-4401/>`_.
 
@@ -176,7 +187,7 @@ def get_fairness_analyses(
 
     Parameters
     ----------
-    df: pandas DataFrame
+    df: pandas.DataFrame
         A dataframe containing columns with numeric human scores,
         columns with numeric system scores and a column with
         group membership.
@@ -186,24 +197,25 @@ def get_fairness_analyses(
         Name of the column containing system scores.
     human_score_column: str
         Name of the column containing human scores.
-    base_group: str, optional
-        Name of the group to use as the reference category.
-        Defaults to ``None`` in which case the group with the
-        largest number of cases will be used as the reference
+        Dedaults to ``"sc1"``.
+    base_group: Optional[str]
+        Name of the group to use as the reference category. If ``None``, the
+        group with the largest number of cases will be used as the reference
         category. Ties are broken alphabetically.
+        Defaults to ``None``.
 
     Returns
     -------
-    model_dict: dictionary
-        A dictionary with different proposed metrics as keys
-        and fitted models as values.
+    model_dict: Dict[str, RegressionResults]
+        A dictionary with different proposed metrics as keys and fitted models
+        as values.
     fairness_container: DataContainer
         A datacontainer with the following datasets:
 
-         - "estimates_<METRIC>_by_<GROUP>" where "<GROUP>" corresponds to
-           the given group and "<METRIC>" can be "osa", "osd" and "csd" estimates
-           for each group computed by the respective models.
-         - "fairness_metrics_by_<GROUP>" - a summary of model fits (R2 and
+         - ``"estimates_<METRIC>_by_<GROUP>"`` where ``<GROUP>`` corresponds
+           to the given group and ``<METRIC>`` can be ``osa``, ``osd`` and ``csd``
+           estimates for each group computed by the respective models.
+         - ``"fairness_metrics_by_<GROUP>"`` - a summary of model fits (R^2 and
            p values).
     """
     # compute error and squared error
@@ -212,9 +224,8 @@ def get_fairness_analyses(
 
     # convert group values to category and reorder them using
     # the largest category as reference
-
     df["group"] = convert_to_ordered_category(df[group], base_group=base_group)
-    base_group = df["group"].cat.categories[0]
+    base_category = df["group"].cat.categories[0]
 
     df["sc1_cat"] = convert_to_ordered_category(df[human_score_column])
 
@@ -230,7 +241,7 @@ def get_fairness_analyses(
 
     osa_results = pd.Series(osa_dict, name="Overall score accuracy")
 
-    df_coefficients_osa = get_coefficients(osa_fit, base_group)
+    df_coefficients_osa = get_coefficients(osa_fit, base_category)
 
     # Overall score difference (OSD)
     # variance in signed residuals (raw error) explained by L1
@@ -244,7 +255,7 @@ def get_fairness_analyses(
 
     osd_results = pd.Series(osd_dict, name="Overall score difference")
 
-    df_coefficients_osd = get_coefficients(osd_fit, base_group)
+    df_coefficients_osd = get_coefficients(osd_fit, base_category)
 
     # conditional score difference CSD
     # Variance in score difference conditioned on Native language
@@ -273,20 +284,20 @@ def get_fairness_analyses(
     }
     csd_results = pd.Series(csd_dict, name="Conditional score difference")
 
-    df_coefficients_csd = get_coefficients(csd_fit, base_group)
+    df_coefficients_csd = get_coefficients(csd_fit, base_category)
 
     # create a summary table
 
     df_r2_all = pd.concat([osa_results, osd_results, csd_results], axis=1, sort=True)
-    df_r2_all["base_category"] = base_group
+    df_r2_all["base_category"] = base_category
 
     # assemble all datasets into a DataContainer
 
     datasets = [
-        {"name": f"estimates_osa_by_{group}", "frame": df_coefficients_osa},
-        {"name": f"estimates_osd_by_{group}", "frame": df_coefficients_osd},
-        {"name": f"estimates_csd_by_{group}", "frame": df_coefficients_csd},
-        {"name": f"fairness_metrics_by_{group}", "frame": df_r2_all},
+        DatasetDict({"name": f"estimates_osa_by_{group}", "frame": df_coefficients_osa}),
+        DatasetDict({"name": f"estimates_osd_by_{group}", "frame": df_coefficients_osd}),
+        DatasetDict({"name": f"estimates_csd_by_{group}", "frame": df_coefficients_csd}),
+        DatasetDict({"name": f"fairness_metrics_by_{group}", "frame": df_r2_all}),
     ]
 
     # assemble all models into a dictionary
diff --git a/rsmtool/modeler.py b/rsmtool/modeler.py
index a81e92e4..3be9265c 100644
--- a/rsmtool/modeler.py
+++ b/rsmtool/modeler.py
@@ -12,6 +12,7 @@ import logging
 import pickle
 from math import log10, sqrt
 from os.path import join
+from typing import Any, Dict, List, Optional, Tuple
 
 import joblib
 import numpy as np
@@ -22,9 +23,11 @@ from scipy.optimize import nnls
 from sklearn.linear_model import LassoCV
 from skll.data import FeatureSet
 from skll.learner import Learner
+from statsmodels.regression.linear_model import RegressionResults
 
 from .analyzer import Analyzer
-from .container import DataContainer
+from .configuration_parser import Configuration
+from .container import DataContainer, DatasetDict
 from .preprocessor import FeaturePreprocessor
 from .utils.metrics import compute_expected_scores_from_model
 from .utils.models import is_skll_model
@@ -32,27 +35,40 @@ from .writer import DataWriter
 
 
 class Modeler:
-    """Class to train model and generate predictions with built-in or SKLL models."""
+    """
+    Class to train model and generate predictions with built-in or SKLL models.
 
-    def __init__(self, logger=None):
+    Note
+    ----
+    The learner and scaling-/trimming-related attributes are set to ``None``.
+    """
+
+    def __init__(self, logger: Optional[logging.Logger] = None) -> None:
         """
-        Initialize empty ``Modeler`` object.
+        Initialize empty Modeler object.
 
-        The learner and the scaling-/trimming-related attributes are
-        set to ``None``.
+        Parameters
+        ----------
+        logger : Optional[logging.Logger]
+            Logger object to use for logging messages. If ``None``, a new logger
+            instance will be created.
+            Defaults to ``None``.
         """
-        self.feature_info = None
-        self.learner = None
-        self.trim_min = None
-        self.trim_max = None
-        self.trim_tolerance = None
-        self.train_predictions_mean = None
-        self.train_predictions_sd = None
-        self.h1_mean = None
-        self.h1_sd = None
-        self.logger = logger if logger else logging.getLogger(__name__)
-
-    def save(self, model_path):
+        self.feature_info: Optional[pd.DataFrame] = None
+        self.learner: Optional[Learner] = None
+        self.trim_min: Optional[float] = None
+        self.trim_max: Optional[float] = None
+        self.trim_tolerance: Optional[float] = None
+        self.train_predictions_mean: Optional[float] = None
+        self.train_predictions_sd: Optional[float] = None
+        self.h1_mean: Optional[float] = None
+        self.h1_sd: Optional[float] = None
+        if logger is None:
+            self.logger = logging.getLogger(__name__)
+        else:
+            self.logger = logger
+
+    def save(self, model_path: str) -> None:
         """
         Save an instance of this class to disk.
 
@@ -64,18 +80,18 @@ class Modeler:
         joblib.dump(self, model_path)
 
     @classmethod
-    def load_from_file(cls, path):
+    def load_from_file(cls, path: str) -> "Modeler":
         """
-        Load a modeler object from a file on disk.
+        Load a ``Modeler`` object from a file on disk.
 
-        The file must contain either ``Modeler`` or a SKLL ``Learner``,
+        The file must contain either a ``Modeler`` or a SKLL ``Learner``,
         in which case a ``Modeler`` object will be created from the
         ``Learner``.
 
         Parameters
         ----------
         path : str
-            File path.
+            File path from which to load the modeler object.
 
         Returns
         -------
@@ -105,9 +121,9 @@ class Modeler:
         return cls.load_from_learner(learner)
 
     @classmethod
-    def load_from_learner(cls, learner):
+    def load_from_learner(cls, learner: Learner) -> "Modeler":
         """
-        Create a new ``Modeler`` instance with a pre-populated learner.
+        Create a new ``Modeler`` object with a pre-populated learner.
 
         Parameters
         ----------
@@ -117,7 +133,7 @@ class Modeler:
         Returns
         -------
         modeler : Modeler
-            A ``Modeler`` object.
+            The newly created ``Modeler`` object.
 
         Raises
         ------
@@ -136,19 +152,19 @@ class Modeler:
         return modeler
 
     @staticmethod
-    def model_fit_to_dataframe(fit):
+    def model_fit_to_dataframe(fit: RegressionResults) -> pd.DataFrame:
         """
         Extract fit metrics from a ``statsmodels`` fit object into a data frame.
 
         Parameters
         ----------
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             Model fit object obtained from a linear model trained using
             ``statsmodels.OLS``.
 
         Returns
         -------
-        df_fit : pandas DataFrame
+        df_fit : pandas.DataFrame
             The output data frame with the main model fit metrics.
         """
         df_fit = pd.DataFrame({"N responses": [int(fit.nobs)]})
@@ -158,20 +174,20 @@ class Modeler:
         return df_fit
 
     @staticmethod
-    def ols_coefficients_to_dataframe(coefs):
+    def ols_coefficients_to_dataframe(coefs: pd.Series) -> pd.DataFrame:
         """
         Convert series containing OLS coefficients to a data frame.
 
         Parameters
         ----------
-        coefs : pandas Series
+        coefs : pandas.Series
             Series with feature names in the index and the coefficient
             values as the data, obtained from a linear model trained
             using ``statsmodels.OLS``.
 
         Returns
         -------
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame with two columns: the feature name and
             the coefficient value.
 
@@ -201,7 +217,7 @@ class Modeler:
         return df_coef
 
     @staticmethod
-    def skll_learner_params_to_dataframe(learner):
+    def skll_learner_params_to_dataframe(learner: Learner) -> pd.DataFrame:
         """
         Extract parameters from the given SKLL learner into a data frame.
 
@@ -212,9 +228,9 @@ class Modeler:
 
         Returns
         -------
-        df_coef : pandas DataFrame
-            The data frame containing the model parameters
-            from the given SKLL learner object.
+        df_coef : pandas.DataFrame
+            The data frame containing the model parameters from the given SKLL
+            Learner object.
 
         Note
         ----
@@ -245,15 +261,15 @@ class Modeler:
 
         return df_coef
 
-    def create_fake_skll_learner(self, df_coefficients):
+    def create_fake_skll_learner(self, df_coefficients: pd.DataFrame) -> Learner:
         """
         Create a fake SKLL linear regression learner from given coefficients.
 
         Parameters
         ----------
-        df_coefficients : pandas DataFrame
-            The data frame containing the linear coefficients
-            we want to create the fake SKLL model with.
+        df_coefficients : pandas.DataFrame
+            The data frame containing the linear coefficients we want to create
+            the fake SKLL model with.
 
         Returns
         -------
@@ -293,7 +309,9 @@ class Modeler:
         learner.model.intercept_ = intercept
         return learner
 
-    def train_linear_regression(self, df_train, feature_columns):
+    def train_linear_regression(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train a "LinearRegression" model.
 
@@ -301,21 +319,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statmodels.regression.linear_model.RegressionResults
             A ``statsmodels`` regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # get the feature columns
@@ -334,7 +351,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_equal_weights_lr(self, df_train, feature_columns):
+    def train_equal_weights_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train an "EqualWeightsLR" model.
 
@@ -342,21 +361,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A ``statsmodels`` regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # we first compute a single feature that is simply the sum of all features
@@ -388,7 +406,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_rebalanced_lr(self, df_train, feature_columns):
+    def train_rebalanced_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train a "RebalancedLR" model.
 
@@ -398,21 +418,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A ``statsmodels`` regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
              Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a plain Linear Regression model
@@ -461,7 +480,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_lasso_fixed_lambda_then_lr(self, df_train, feature_columns):
+    def train_lasso_fixed_lambda_then_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train a "LassoFixedLambdaThenLR" model.
 
@@ -471,21 +492,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A ``statsmodels`` regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             The model coefficients in a data_frame
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a Lasso Regression model with this featureset with a preset lambda
@@ -521,7 +541,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_positive_lasso_cv_then_lr(self, df_train, feature_columns):
+    def train_positive_lasso_cv_then_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train a "PositiveLassoCVThenLR" model.
 
@@ -531,21 +553,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A statsmodels regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a LassoCV outside of SKLL since it's not exposed there
@@ -576,7 +597,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_non_negative_lr(self, df_train, feature_columns):
+    def train_non_negative_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train an "NNLR" model.
 
@@ -590,21 +613,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A statsmodels regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
              Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # add an intercept to the features manually
@@ -614,7 +636,7 @@ class Modeler:
         y = df_train["sc1"].values
 
         # fit an NNLS model on this data
-        coefs, rnorm = nnls(X_plus_intercept, y)
+        coefs, _ = nnls(X_plus_intercept, y)
 
         # check whether the intercept is set to 0 and if so then we need
         # to flip the sign and refit the model to ensure that it is always
@@ -622,7 +644,7 @@ class Modeler:
         if coefs[0] == 0:
             intercepts = -1 * np.ones((len(df_train), 1))
             X_plus_intercept = np.concatenate([intercepts, X], axis=1)
-            coefs, rnorm = nnls(X_plus_intercept, y)
+            coefs, _ = nnls(X_plus_intercept, y)
 
         # separate the intercept and feature coefficients
         # intercept = coefs[0]
@@ -650,7 +672,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_non_negative_lr_iterative(self, df_train, feature_columns):
+    def train_non_negative_lr_iterative(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train an "NNLR_iterative" model.
 
@@ -664,21 +688,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A statsmodels regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         X = df_train[feature_columns]
@@ -715,7 +738,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_lasso_fixed_lambda_then_non_negative_lr(self, df_train, feature_columns):
+    def train_lasso_fixed_lambda_then_non_negative_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train an "LassoFixedLambdaThenNNLR" model.
 
@@ -724,21 +749,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object.
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A statsmodels regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a Lasso Regression model with a preset lambda
@@ -766,7 +790,7 @@ class Modeler:
         y = df_train["sc1"].values
 
         # fit an NNLS model on this data
-        coefs, rnorm = nnls(X_plus_intercept, y)
+        coefs, _ = nnls(X_plus_intercept, y)
 
         # check whether the intercept is set to 0 and if so then we need
         # to flip the sign and refit the model to ensure that it is always
@@ -774,7 +798,7 @@ class Modeler:
         if coefs[0] == 0:
             intercepts = -1 * np.ones((len(df_train), 1))
             X_plus_intercept = np.concatenate([intercepts, X], axis=1)
-            coefs, rnorm = nnls(X_plus_intercept, y)
+            coefs, _ = nnls(X_plus_intercept, y)
 
         # separate the intercept and feature coefficients
         # even though we do not use intercept in the code
@@ -804,7 +828,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_lasso_fixed_lambda(self, df_train, feature_columns):
+    def train_lasso_fixed_lambda(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, None, pd.DataFrame, List[str]]:
         """
         Train a "LassoFixedLambda" model.
 
@@ -812,10 +838,9 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
@@ -823,11 +848,10 @@ class Modeler:
         learner : skll.learner.Learner
             The SKLL learner object
         fit : ``None``
-            This is always ``None`` since there is no OLS
-            model fitted in this case.
-        df_coef : pandas DataFrame
+            This is always ``None`` since there is no OLS model fitted in this case.
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a Lasso Regression model with a preset lambda
@@ -855,7 +879,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_positive_lasso_cv(self, df_train, feature_columns):
+    def train_positive_lasso_cv(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, None, pd.DataFrame, List[str]]:
         """
         Train a "PositiveLassoCV" model.
 
@@ -865,10 +891,9 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
@@ -876,11 +901,10 @@ class Modeler:
         learner : skll.learner.Learner
             The SKLL learner object
         fit : ``None``
-            This is always ``None`` since there is no OLS
-            model fitted in this case.
-        df_coef : pandas DataFrame
+            This is always ``None`` since there is no OLS model fitted in this case.
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train a LassoCV outside of SKLL since it's not exposed there
@@ -915,7 +939,9 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_score_weighted_lr(self, df_train, feature_columns):
+    def train_score_weighted_lr(
+        self, df_train: pd.DataFrame, feature_columns: List[str]
+    ) -> Tuple[Learner, RegressionResults, pd.DataFrame, List[str]]:
         """
         Train a "ScoreWeightedLR" model.
 
@@ -923,21 +949,20 @@ class Modeler:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        feature_columns : list of str
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        feature_columns : List[str]
             A list of feature columns to use in training the model.
 
         Returns
         -------
         learner : skll.learner.Learner
             The SKLL learner object
-        fit : statsmodels.RegressionResults
+        fit : statsmodels.regression.linear_model.RegressionResults
             A statsmodels regression results object.
-        df_coef : pandas DataFrame
+        df_coef : pandas.DataFrame
             Data frame containing the model coefficients.
-        used_features : list of str
+        used_features : List[str]
             A list of features used in the final model.
         """
         # train weighted least squares regression
@@ -967,7 +992,14 @@ class Modeler:
 
         return learner, fit, df_coef, used_features
 
-    def train_builtin_model(self, model_name, df_train, experiment_id, filedir, file_format="csv"):
+    def train_builtin_model(
+        self,
+        model_name: str,
+        df_train: pd.DataFrame,
+        experiment_id: str,
+        filedir: str,
+        file_format: str = "csv",
+    ) -> Learner:
         """
         Train one of the :ref:`built-in linear regression models <builtin_models>`.
 
@@ -975,7 +1007,7 @@ class Modeler:
         ----------
         model_name : str
             Name of the built-in model to train.
-        df_train : pandas DataFrame
+        df_train : pandas.DataFrame
             Data frame containing the features on which to train
             the model. The data frame must contain the ID column
             named "spkitemid" and the numeric label column named
@@ -984,9 +1016,10 @@ class Modeler:
             The experiment ID.
         filedir : str
             Path to the `output` experiment output directory.
-        file_format : str, optional
-            The format in which to save files. One of {"csv", "tsv", "xlsx"}.
-            Defaults to "csv".
+        file_format : str
+            The format in which to save files. One of {``"csv"``, ``"tsv"``,
+            ``"xlsx"``}.
+            Defaults to ``"csv"``.
 
         Returns
         -------
@@ -1042,13 +1075,13 @@ class Modeler:
             result = self.train_score_weighted_lr(df_train, feature_columns)
 
         writer = DataWriter(experiment_id)
-        frames = []
+        datasets = []
 
         # unpack all results
         learner, fit, df_coef, used_features = result
 
         # add raw coefficients to frame list
-        frames.append({"name": "coefficients", "frame": df_coef})
+        datasets.append(DatasetDict({"name": "coefficients", "frame": df_coef}))
 
         # compute the standardized and relative coefficients (betas) for the
         # non-intercept features and save to a file
@@ -1061,7 +1094,7 @@ class Modeler:
         df_betas.reset_index(inplace=True)
 
         # add betas to frame list
-        frames.append({"name": "betas", "frame": df_betas})
+        datasets.append(DatasetDict({"name": "betas", "frame": df_betas}))
 
         # save the OLS fit object and its summary to files
         if fit:
@@ -1075,9 +1108,9 @@ class Modeler:
             df_model_fit = self.model_fit_to_dataframe(fit)
 
             # add model_fit to frame list
-            frames.append({"name": "model_fit", "frame": df_model_fit})
+            datasets.append(DatasetDict({"name": "model_fit", "frame": df_model_fit}))
 
-        container = DataContainer(frames)
+        container = DataContainer(datasets=datasets)
         writer.write_experiment_output(filedir, container, file_format=file_format)
 
         self.learner = learner
@@ -1086,13 +1119,13 @@ class Modeler:
 
     def train_skll_model(
         self,
-        model_name,
-        df_train,
-        custom_fixed_parameters=None,
-        custom_objective=None,
-        predict_expected_scores=False,
-        skll_grid_search_jobs=1,
-    ):
+        model_name: str,
+        df_train: pd.DataFrame,
+        custom_fixed_parameters: Optional[Dict[str, Any]] = None,
+        custom_objective: Optional[str] = None,
+        predict_expected_scores: bool = False,
+        skll_grid_search_jobs: int = 1,
+    ) -> Tuple[Learner, str]:
         """
         Train a SKLL classification or regression model.
 
@@ -1100,29 +1133,28 @@ class Modeler:
         ----------
         model_name : str
             Name of the SKLL model to train.
-        df_train : pandas DataFrame
-            Data frame containing the features on which
-            to train the model.
-        custom_fixed_parameters : dict, optional
-            A dictionary containing any fixed parameters for the SKLL
-            model.
+        df_train : pandas.DataFrame
+            Data frame containing the features on which to train the model.
+        custom_fixed_parameters : Optional[Dict[str, Any]]
+            A dictionary containing any fixed parameters for the SKLL model.
             Defaults to ``None``.
-        custom_objective : str, optional
+        custom_objective : Optional[str]
             Name of custom user-specified objective. If not specified
             or ``None``, "neg_mean_squared_error" is used as the objective.
             Defaults to ``None``.
-        predict_expected_scores : bool, optional
+        predict_expected_scores : bool
             Whether we want the trained classifiers to predict expected scores.
             Defaults to ``False``.
         skll_grid_search_jobs : int
-            Number of folds to run in parallel when using ``skll`` grid search.
+            Number of folds to run in parallel when using SKLL grid search.
             Defaults to 1.
 
         Returns
         -------
-        learner_and_objective : tuple
-            A 2-tuple containing a SKLL Learner object of the appropriate type
-            and the chosen tuning objective.
+        learner: skll.learner.Learner
+            A SKLL Learner object of the appropriate type.
+        objective: str
+            The chosen tuning objective.
         """
         # Instantiate the given SKLL learner and set its probability value
         # and fixed parameters appropriately
@@ -1154,13 +1186,18 @@ class Modeler:
         )
 
         # TODO: compute betas for linear SKLL models?
-
         self.learner = learner
 
         # return the SKLL learner object and the chosen objective
         return learner, objective
 
-    def train(self, configuration, data_container, filedir, figdir, file_format="csv"):
+    def train(
+        self,
+        configuration: Configuration,
+        data_container: DataContainer,
+        filedir: str,
+        file_format: str = "csv",
+    ):
         """
         Train the given model on the given data and save the results.
 
@@ -1170,23 +1207,23 @@ class Modeler:
 
         Parameters
         ----------
-        configuration : configuration_parser.Configuration
+        configuration : Configuration
             A configuration object containing "experiment_id" and "model_name"
             parameters.
-        data_container : container.DataContainer
+        data_container : DataContainer
             A data container object containing "train_preprocessed_features"
             data set.
         filedir : str
             Path to the "output" experiment output directory.
-        figdir : str
-            Path to the "figure" experiment output directory.
-        file_format : str, optional
-            The format in which to save files. One of {"csv", "tsv", "xlsx"}.
-            Defaults to "csv".
+        file_format : str
+            The format in which to save files. One of {``"csv"``, ``"tsv"``,
+            ``"xlsx"``}.
+            Defaults to ``"csv"``.
 
         Returns
         -------
-        name : SKLL Learner object
+        model : skll.learner.Learner
+            The trained SKLL Learner object.
         """
         Analyzer.check_param_names(configuration, ["model_name", "experiment_id"])
         Analyzer.check_frame_names(data_container, ["train_preprocessed_features"])
@@ -1217,27 +1254,33 @@ class Modeler:
 
         return model
 
-    def predict(self, df, min_score=None, max_score=None, predict_expected=False):
+    def predict(
+        self,
+        df: pd.DataFrame,
+        min_score: Optional[float] = None,
+        max_score: Optional[float] = None,
+        predict_expected: bool = False,
+    ) -> pd.DataFrame:
         """
         Get raw predictions from given SKLL model on data in given data frame.
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             Data frame containing features on which to make the predictions.
             The data must contain pre-processed feature values, an ID column
             named "spkitemid", and a label column named "sc1".
-        min_score : int, optional
+        min_score : Optional[float]
             Minimum score level to be used if computing expected scores.
             If ``None``, trying to compute expected scores will raise an
             exception.
             Defaults to ``None``.
-        max_score : int, optional
+        max_score : Optional[float]
             Maximum score level to be used if computing expected scores.
             If ``None``, trying to compute expected scores will raise an
             exception.
             Defaults to ``None``.
-        predict_expected : bool, optional
+        predict_expected : bool
             Predict expected scores for classifiers that return probability
             distributions over score. This will be ignored with a warning
             if the specified model does not support probability distributions.
@@ -1248,12 +1291,14 @@ class Modeler:
 
         Returns
         -------
-        df_predictions : pandas DataFrame
-            Data frame containing the raw predictions, the IDs, and the
-            human scores.
+        df_predictions : pandas.DataFrame
+            Data frame containing the raw predictions, the IDs, and the human
+            scores.
 
         Raises
         ------
+        ValueError
+            If no model has been trained yet.
         ValueError
             If the model cannot predict probability distributions and
             ``predict_expected`` is set to ``True``.
@@ -1263,14 +1308,13 @@ class Modeler:
             distribution.
         ValueError
             If ``predict_expected`` is ``True`` but ``min_score`` and
-            ``max_score`` are not specified.
+            ``max_score`` are ``None``.
         """
-        # expected scores require specifying ``trim_min`` and ``trim_max``
-        if predict_expected and not (min_score and max_score):
-            raise ValueError("Must specify 'min_score' and 'max_score' for expected scores.")
-
         model = self.learner
 
+        if model is None:
+            raise ValueError("No model has been trained yet.")
+
         feature_columns = [c for c in df.columns if c not in ["spkitemid", "sc1"]]
         features = df[feature_columns].to_dict(orient="records")
         ids = df["spkitemid"].tolist()
@@ -1282,11 +1326,15 @@ class Modeler:
 
         fs = FeatureSet("data", ids=ids, labels=labels, features=features)
         # if we are predicting expected scores, then call a different function
-        predictions = (
-            compute_expected_scores_from_model(model, fs, min_score, max_score)
-            if predict_expected
-            else model.predict(fs)
-        )
+        if predict_expected:
+            if min_score is None or max_score is None:
+                raise ValueError("Must specify 'min_score' and 'max_score' for expected scores.")
+            else:
+                predictions = compute_expected_scores_from_model(
+                    model, fs, int(min_score), int(max_score)
+                )
+        else:
+            predictions = model.predict(fs)
 
         df_predictions = pd.DataFrame()
         df_predictions["spkitemid"] = ids
@@ -1298,26 +1346,30 @@ class Modeler:
 
         return df_predictions
 
-    def predict_train_and_test(self, df_train, df_test, configuration):
+    def predict_train_and_test(
+        self, df_train: pd.DataFrame, df_test: pd.DataFrame, configuration: Configuration
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Generate raw, scaled, and trimmed predictions on given data.
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the pre-processed training
-            set features.
-        df_test : pandas DataFrame
-            Data frame containing the pre-processed test
-            set features.
-        configuration : configuration_parser.Configuration
-            A configuration object containing "trim_max"
-            and "trim_min" keys.
+        df_train : pandas.DataFrame
+            Data frame containing the pre-processed training set features.
+        df_test : pandas.DataFrame
+            Data frame containing the pre-processed test set features.
+        configuration : Configuration
+            A configuration object containing "trim_max" and "trim_min" keys.
 
         Returns
         -------
-        List of data frames containing predictions and other
-        information.
+        configuration : Configuration
+            A copy of the given configuration object also containing the
+            "train_predictions_mean", "train_predictions_sd", "human_labels_mean",
+            "human_labels_sd", "trim_min", and "trim_max" parameters.
+        data_container : DataContainer
+            A data container object containing the "pred_train", "pred_test",
+            and "postprocessing_params" data sets.
         """
         Analyzer.check_param_names(configuration, ["trim_max", "trim_min", "trim_tolerance"])
 
@@ -1327,18 +1379,22 @@ class Modeler:
             trim_tolerance,
         ) = configuration.get_trim_min_max_tolerance()
 
+        # At this point, `trim_min` and `trim_max` are guaranteed to be
+        # valid numeric values but let's confirm to satisfy mypy
+        assert trim_min is not None and trim_max is not None and trim_tolerance is not None
+
         predict_expected_scores = configuration["predict_expected_scores"]
 
         df_train_predictions = self.predict(
             df_train,
-            min_score=int(trim_min),
-            max_score=int(trim_max),
+            min_score=trim_min,
+            max_score=trim_max,
             predict_expected=predict_expected_scores,
         )
         df_test_predictions = self.predict(
             df_test,
-            min_score=int(trim_min),
-            max_score=int(trim_max),
+            min_score=trim_min,
+            max_score=trim_max,
             predict_expected=predict_expected_scores,
         )
 
@@ -1390,9 +1446,9 @@ class Modeler:
         )
 
         datasets = [
-            {"name": "pred_train", "frame": df_train_predictions},
-            {"name": "pred_test", "frame": df_test_predictions},
-            {"name": "postprocessing_params", "frame": df_postproc_params},
+            DatasetDict({"name": "pred_train", "frame": df_train_predictions}),
+            DatasetDict({"name": "pred_test", "frame": df_test_predictions}),
+            DatasetDict({"name": "postprocessing_params", "frame": df_postproc_params}),
         ]
 
         # configuration options that are entirely for internal use
@@ -1409,46 +1465,46 @@ class Modeler:
 
         return new_configuration, DataContainer(datasets=datasets)
 
-    def get_feature_names(self):
+    def get_feature_names(self) -> Optional[List[str]]:
         """
         Get the feature names, if available.
 
         Returns
         -------
-        feature_names : list or ``None``
+        feature_names : Optional[List[str]]
             A list of feature names, or None if no learner was trained.
         """
         if self.learner is not None:
             return self.learner.feat_vectorizer.get_feature_names_out().tolist()
         return None
 
-    def get_intercept(self):
+    def get_intercept(self) -> Optional[float]:
         """
         Get the intercept of the model, if available.
 
         Returns
         -------
-        intercept : float or ``None``
+        intercept : Optional[float]
            The intercept of the model.
         """
         if self.learner is not None:
             return self.learner.model.intercept_
         return None
 
-    def get_coefficients(self):
+    def get_coefficients(self) -> Optional[np.ndarray]:
         """
         Get the coefficients of the model, if available.
 
         Returns
         -------
-        coefficients : np.array or ``None``
+        coefficients : Optional[np.ndarray]
            The coefficients of the model, if available.
         """
         if self.learner is not None:
             return self.learner.model.coef_
         return None
 
-    def scale_coefficients(self, configuration):
+    def scale_coefficients(self, configuration: Configuration) -> DataContainer:
         """
         Scale coefficients using human scores & training set predictions.
 
@@ -1457,13 +1513,13 @@ class Modeler:
 
         Parameters
         ----------
-        configuration : configuration_parser.Configuration
+        configuration : Configuration
             A configuration object containing the "train_predictions_mean",
             "train_predictions_sd", and "human_labels_sd" parameters.
 
         Returns
         -------
-        data_container : container.DataContainer
+        data_container : DataContainer
             A container object containing the "coefficients_scaled"
             dataset. The frame for this dataset contains the scaled
             coefficients and the feature names, along with the
@@ -1502,7 +1558,10 @@ class Modeler:
         new_intercept = intercept * (h1_sd / train_predictions_sd)
         new_intercept += train_predictions_mean * (1 - h1_sd / train_predictions_sd)
 
-        intercept_and_feature_names = ["Intercept"] + feature_names
+        intercept_and_feature_names = (
+            ["Intercept"] + feature_names if feature_names else ["Intercept"]
+        )
+
         intercept_and_feature_values = [new_intercept] + list(scaled_coefficients)
 
         # create a data frame with new values
@@ -1514,6 +1573,8 @@ class Modeler:
             columns=["feature", "coefficient"],
         )
 
-        scaled_dataset = [{"name": "coefficients_scaled", "frame": df_scaled_coefficients}]
+        scaled_dataset = [
+            DatasetDict({"name": "coefficients_scaled", "frame": df_scaled_coefficients})
+        ]
 
         return DataContainer(datasets=scaled_dataset)
diff --git a/rsmtool/preprocessor.py b/rsmtool/preprocessor.py
index cb47babf..02cc973a 100644
--- a/rsmtool/preprocessor.py
+++ b/rsmtool/preprocessor.py
@@ -11,12 +11,15 @@ Classes for preprocessing input data in various contexts.
 import logging
 import re
 from sys import version_info
+from typing import Any, Dict, List, Optional, Tuple, Union
 
 import numpy as np
 import pandas as pd
 from numpy.random import RandomState
 
-from .container import DataContainer
+from rsmtool.configuration_parser import Configuration
+
+from .container import DataContainer, DatasetDict
 from .reader import DataReader
 from .reporter import Reporter
 from .transformer import FeatureTransformer
@@ -27,26 +30,28 @@ from .utils.models import is_built_in_model, is_skll_model
 class FeatureSubsetProcessor:
     """Class to encapsulate feature sub-setting methods."""
 
-    def __init__(self, logger=None):
+    def __init__(self, logger: Optional[logging.Logger] = None):
         """Initialize the FeatureSubsetProcessor object."""
         self.logger = logger if logger else logging.getLogger(__name__)
 
-    def select_by_subset(self, feature_columns, feature_subset_specs, subset):
+    def select_by_subset(
+        self, feature_columns: List[str], feature_subset_specs: pd.DataFrame, subset: str
+    ) -> List[str]:
         """
         Select feature columns using feature subset specifications.
 
         Parameters
         ----------
-        feature_columns : list of str
+        feature_columns : List[str]
             A list of feature columns
-        feature_subset_specs : pandas DataFrame
+        feature_subset_specs : pandas.DataFrame
             The feature subset specification data frame.
         subset : str
             The column to subset.
 
         Returns
         -------
-        feature_names : list of str
+        feature_names : List[str]
             A list of feature names to include.
         """
         feature_subset = feature_subset_specs[feature_subset_specs[subset] == 1]["Feature"]
@@ -72,21 +77,22 @@ class FeatureSubsetProcessor:
                 )
         return feature_names
 
-    def check_feature_subset_file(self, df, subset=None, sign=None):
+    def check_feature_subset_file(
+        self, df: pd.DataFrame, subset: Optional[str] = None, sign: Optional[str] = None
+    ) -> None:
         """
         Check that feature subset file is complete and in the correct format.
 
-        Raises an exception if it finds any errors but otherwise
-        returns nothing.
+        Raises an exception if it finds any errors but otherwise returns nothing.
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame containing the feature subset file.
-        subset : str, optional
+        subset : Optional[str]
             Name of a pre-defined feature subset.
             Defaults to ``None``.
-        sign : str, optional
+        sign : Optional[str]
             Value of the sign.
             Defaults to ``None``.
 
@@ -137,11 +143,11 @@ class FeatureSubsetProcessor:
 class FeatureSpecsProcessor:
     """Encapsulate feature file processing methods."""
 
-    def __init__(self, logger=None):
+    def __init__(self, logger: Optional[logging.Logger] = None):
         """Initialize the FeatureSpecsProcessor object."""
         self.logger = logger if logger else logging.getLogger(__name__)
 
-    def generate_default_specs(self, feature_names):
+    def generate_default_specs(self, feature_names: List[str]) -> pd.DataFrame:
         """
         Generate default feature "specifications" for given feature names.
 
@@ -150,12 +156,12 @@ class FeatureSpecsProcessor:
 
         Parameters
         ----------
-        feature_names: list of str
+        feature_names: List[str]
             List of feature names for which to generate specifications.
 
         Returns
         -------
-        feature_specs: pandas DataFrame
+        feature_specs: pandas.DataFrame
             A dataframe with feature specifications that can be saved as a
             :ref:`feature list file <example_feature_csv>`.
 
@@ -170,7 +176,7 @@ class FeatureSpecsProcessor:
         df_feature_specs["sign"] = 1.0
         return df_feature_specs
 
-    def find_feature_sign(self, feature, sign_dict):
+    def find_feature_sign(self, feature: str, sign_dict: Dict[str, str]) -> float:
         """
         Get the feature sign from the feature CSV file.
 
@@ -198,7 +204,9 @@ class FeatureSpecsProcessor:
             feature_sign_numeric = -1.0 if feature_sign_string == "-" else 1.0
         return feature_sign_numeric
 
-    def validate_feature_specs(self, df, use_truncations=False):
+    def validate_feature_specs(
+        self, df: pd.DataFrame, use_truncations: bool = False
+    ) -> pd.DataFrame:
         """
         Validate given feature specifications.
 
@@ -208,17 +216,16 @@ class FeatureSpecsProcessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The feature specification DataFrame to validate.
-        use_truncations : bool, optional
-            Whether to use truncation values. If this is
-            ``True`` and truncation values are not specified,
-            an exception is raised.
+        use_truncations : bool
+            Whether to use truncation values. If this is ``True`` and truncation
+            values are not specified, an exception is raised.
             Defaults to ``False``.
 
         Returns
         -------
-        df_specs_new : pandas DataFrame
+        df_specs_new : pandas.DataFrame
             The output data frame with normalized values.
 
         Raises
@@ -287,8 +294,13 @@ class FeatureSpecsProcessor:
         return df_specs_new
 
     def generate_specs(
-        self, df, feature_names, train_label, feature_subset=None, feature_sign=None
-    ):
+        self,
+        df: pd.DataFrame,
+        feature_names: List[str],
+        train_label: str,
+        feature_subset: Optional[pd.DataFrame] = None,
+        feature_sign: Optional[int] = None,
+    ) -> pd.DataFrame:
         """
         Generate feature specifications using the feature CSV file.
 
@@ -297,26 +309,26 @@ class FeatureSpecsProcessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The input data frame from which to generate the specifications.
-        feature_names : list of str
+        feature_names : List[str]
             A list of feature names.
         train_label : str
             The label column for the training data
-        feature_subset : pandas DataFrame, optional
-            A data frame containing the feature subset specifications.
+        feature_subset : Optional[pandas.DataFrame]
+            A data frame containing the feature subset specifications, if any.
             Defaults to ``None``.
-        feature_sign : int, optional
+        feature_sign : Optional[int]
             The sign of the feature.
             Defaults to ``None``.
 
         Returns
         -------
-        df_feature_specs : pandas DataFrame
+        df_feature_specs : pandas.DataFrame
             The output data frame containing the feature specifications.
         """
         # get feature sign information, if available
-        if feature_sign:
+        if feature_sign and feature_subset is not None:
             # Convert to dictionary {feature:sign}
             sign_dict = dict(zip(feature_subset.Feature, feature_subset[f"Sign_{feature_sign}"]))
         # else create an empty dictionary
@@ -324,7 +336,7 @@ class FeatureSpecsProcessor:
             sign_dict = {}
 
         feature_specs = []
-        feature_dict = {}
+        feature_dict: Dict[str, Any] = {}
         for feature in feature_names:
             feature_dict["feature"] = feature
             feature_dict["transform"] = FeatureTransformer().find_feature_transform(
@@ -347,11 +359,11 @@ class FeatureSpecsProcessor:
 class FeaturePreprocessor:
     """Class to preprocess features in training and testing sets."""
 
-    def __init__(self, logger=None):
+    def __init__(self, logger: Optional[logging.Logger] = None):
         """Initialize the FeaturePreprocessor object."""
         self.logger = logger if logger else logging.getLogger(__name__)
 
-    def check_model_name(self, model_name):
+    def check_model_name(self, model_name: str) -> str:
         """
         Check that the given model name is valid and determine its type.
 
@@ -381,7 +393,13 @@ class FeaturePreprocessor:
 
         return model_type
 
-    def trim(self, values, trim_min, trim_max, tolerance=0.4998):
+    def trim(
+        self,
+        values: Union[List[float], np.ndarray],
+        trim_min: float,
+        trim_max: float,
+        tolerance: float = 0.4998,
+    ) -> np.ndarray:
         """
         Trim values in given numpy array.
 
@@ -390,7 +408,7 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        values : list or np.array
+        values : Union[List[float], numpy.ndarray]
             The values to trim.
         trim_min : float
             The lowest score on the score point, used for
@@ -398,19 +416,27 @@ class FeaturePreprocessor:
         trim_max : float
             The highest score on the score point, used for
             trimming the raw regression predictions.
-        tolerance : float, optional
+        tolerance : float
             The tolerance that will be used to compute the
             trim interval.
             Defaults to ``0.4998``.
 
         Returns
         -------
-        trimmed_values : np.array
-            Trimmed values.
+        trimmed_values : numpy.ndarray
+            Numpy array containing the trimmed values."
+
+        Raises
+        ------
+        ValueError
+            If ``trim_min``, ``trim_max``, or ``tolerance`` are ``None``.
         """
         if isinstance(values, list):
             values = np.array(values)
 
+        if trim_min is None or trim_max is None or tolerance is None:
+            raise ValueError("trim_min, trim_max, and tolerance must be numeric values.")
+
         new_max = trim_max + tolerance
         new_min = trim_min - tolerance
         trimmed_values = values.copy()
@@ -419,7 +445,12 @@ class FeaturePreprocessor:
         return trimmed_values
 
     @staticmethod
-    def remove_outliers(values, mean=None, sd=None, sd_multiplier=4):  # noqa: D301
+    def remove_outliers(
+        values: np.ndarray,
+        mean: Optional[float] = None,
+        sd: Optional[float] = None,
+        sd_multiplier: int = 4,
+    ) -> np.ndarray:
         r"""
         Remove outliers from given array of values by clamping them.
 
@@ -434,34 +465,31 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        values : np.array
-            The values from which to remove outliers.
-        mean : int or float, optional
-            Use the given mean value when computing outliers
-            instead of the mean from the data.
+        values : numpy.ndarray
+            The values from which to remove outliers, usually corresponding to
+            a given feature.
+        mean : Optional[float]
+            Use the given mean value when computing outliers instead of the mean
+            from the data.
             Defaults to ``None``.
-        sd : None, optional
-            Use the given std. dev. value when computing
-            outliers instead of the std. dev. from the
-            data.
+        sd : Optional[float]
+            Use the given std. dev. value when computing outliers instead of the
+            std. dev. from the data.
             Defaults to ``None``.
-        sd_multiplier : int, optional
-            Use the given multipler for the std. dev. when
-            computing the outliers. Defaults to 4.
+        sd_multiplier : int
+            Use the given multipler for the std. dev. when computing the outliers.
             Defaults to 4.
 
         Returns
         -------
-        new_values : np.array
+        new_values : numpy.ndarray
             Numpy array with the outliers clamped.
         """
         # convert data to a numpy float array before doing any clamping
         new_values = np.array(values, dtype=np.float64)
 
-        if not mean:
-            mean = new_values.mean()
-        if not sd:
-            sd = new_values.std()
+        mean = mean if mean else new_values.mean()
+        sd = sd if sd else new_values.std()
 
         floor = mean - sd_multiplier * sd
         ceiling = mean + sd_multiplier * sd
@@ -470,27 +498,30 @@ class FeaturePreprocessor:
         new_values[new_values < floor] = floor
         return new_values
 
-    def remove_outliers_using_truncations(self, values, feature_name, truncations):
+    def remove_outliers_using_truncations(
+        self, values: np.ndarray, feature_name: str, truncations: pd.DataFrame
+    ) -> np.ndarray:
         """
         Remove outliers using pre-specified truncation groups.
 
         This is different from ``remove_outliers()`` which calculates the
-        outliers based on the training set.
+        outliers based on the training set rather than looking up the truncation
+        values from a pre-specified data frame.
 
         Parameters
         ----------
-        values : np.array
-            The values from which to remove outliers.
+        values : numpy.ndarray
+            The values from which to remove outliers, usually corresponding
+            to a given feature.
         feature_name : str
-            Name of the feature whose outliers are
-            being clamped.
-        truncations : pandas DataFrame
-            A data frame with truncation values. The
-            features should be set as the index.
+            Name of the feature whose outliers are being clamped.
+        truncations : pandas.DataFrame
+            A data frame with truncation values. The features should be set as
+            the index.
 
         Returns
         -------
-        new_values : numpy array
+        new_values : numpy.ndarray
             Numpy array with the outliers clamped.
         """
         # convert data to a numpy float array before doing any clamping
@@ -503,26 +534,28 @@ class FeaturePreprocessor:
         new_values[new_values < minimum] = minimum
         return new_values
 
-    def select_candidates(self, df, N, candidate_col="candidate"):
+    def select_candidates(
+        self, df: pd.DataFrame, N: int, candidate_col: str = "candidate"
+    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
         """
         Select candidates which have responses to N or more items.
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame from which to select candidates with N or more items.
         N: int
             Minimal number of items per candidate
-        candidate_col : str, optional
+        candidate_col : str
             Name of the column which contains candidate ids.
             Defaults to "candidate".
 
         Returns
         -------
-        df_included: pandas DataFrame
+        df_included: pandas.DataFrame
             Data frame with responses from candidates with responses to N
             or more items.
-        df_excluded: pandas DataFrame
+        df_excluded: pandas.DataFrame
             Data frame with responses from candidates with responses to
             less than N items.
         """
@@ -540,7 +573,7 @@ class FeaturePreprocessor:
 
         return (df_included, df_excluded)
 
-    def check_subgroups(self, df, subgroups):
+    def check_subgroups(self, df: pd.DataFrame, subgroups: List[str]) -> pd.DataFrame:
         """
         Validate subgroup names in the given data.
 
@@ -552,15 +585,15 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             Input data frame with subgroups to check.
-        subgroups : list of str
+        subgroups : List[str]
             List of column names that contain grouping information.
 
         Returns
         -------
-        df : pandas DataFrame
-             Modified input data frame with NaNs replaced.
+        df : pandas.DataFrame
+            Modified input data frame with NaNs replaced.
 
         Raises
         ------
@@ -585,15 +618,15 @@ class FeaturePreprocessor:
 
     def rename_default_columns(
         self,
-        df,
-        requested_feature_names,
-        id_column,
-        first_human_score_column,
-        second_human_score_column,
-        length_column,
-        system_score_column,
-        candidate_column,
-    ):
+        df: pd.DataFrame,
+        requested_feature_names: List[str],
+        id_column: str,
+        first_human_score_column: Union[str, None],
+        second_human_score_column: Union[str, None],
+        length_column: Union[str, None],
+        system_score_column: Union[str, None],
+        candidate_column: Union[str, None],
+    ) -> pd.DataFrame:
         """
         Standardize column names and rename columns with reserved column names.
 
@@ -605,33 +638,32 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame containing the columns to rename.
-        requested_feature_names : list of str
-            List of feature column names that we want
-            to include in the scoring model.
+        requested_feature_names : List[str]
+            List of feature column names that we want to include in the
+            scoring model.
         id_column : str
             Column name containing the response IDs.
-        first_human_score_column : str or ``None``.
-            Column name containing the H1 scores.
-            Should be ``None`` if no H1 scores are available.
-        second_human_score_column : str or ``None``
-            Column name containing the H2 scores.
-            Should be ``None`` if no H2 scores are available.
-        length_column : str or ``None``
-            Column name containing response lengths.
-            Should be ``None`` if lengths are not available.
-        system_score_column : str
-            Column name containing the score predicted
-            by the system. This is only used for rsmeval.
-        candidate_column : str or ``None``
-            Column name containing identifying information
-            at the candidate level. Should be None if such
-            information is not available.
+        first_human_score_column : Union[str, None]
+            Column name containing the H1 scores. Should be ``None`` if no H1
+            scores are available.
+        second_human_score_column : Union[str, None]
+            Column name containing the H2 scores. Should be ``None`` if no H2
+            scores are available.
+        length_column : Union[str, None]
+            Column name containing response lengths. Should be ``None`` if
+            lengths are not available.
+        system_score_column : Union[str, None]
+            Column name containing the score predicted by the system. This is
+            only used for rsmeval.
+        candidate_column : Union[str, None]
+            Column name containing identifying information at the candidate level.
+            Should be ``None`` if such information is not available.
 
         Returns
         -------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             Modified input data frame with all the approximate
             re-namings.
         """
@@ -692,8 +724,12 @@ class FeaturePreprocessor:
         return df
 
     def filter_on_column(
-        self, df, column, id_column, exclude_zeros=False, exclude_zero_sd=False
-    ):  # noqa: D301
+        self,
+        df: pd.DataFrame,
+        column: str,
+        exclude_zeros: bool = False,
+        exclude_zero_sd: bool = False,
+    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
         r"""
         Filter out rows containing non-numeric values.
 
@@ -704,26 +740,23 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame containing the data to be filtered.
         column : str
             Name of the column from which to filter out values.
-        id_column : str
-            Name of the column containing the unique response IDs.
-        exclude_zeros : bool, optional
-            Whether to exclude responses containing zeros
-            in the specified column.
+        exclude_zeros : bool
+            Whether to exclude responses containing zeros in the specified column.
             Defaults to ``False``.
-        exclude_zero_sd : bool, optional
-            Whether to perform the additional filtering step of removing
-            columns that have :math:`\\sigma = 0`.
+        exclude_zero_sd : bool
+            Whether to perform the additional filtering step of removing columns
+            that have :math:`\\sigma = 0`.
             Defaults to ``False``.
 
         Returns
         -------
-        df_filtered : pandas DataFrame
+        df_filtered : pandas.DataFrame
             Data frame containing the responses that were *not* filtered out.
-        df_excluded : pandas DataFrame
+        df_excluded : pandas.DataFrame
             Data frame containing the non-numeric or zero responses that
             were filtered out.
 
@@ -807,48 +840,44 @@ class FeaturePreprocessor:
 
     def process_predictions(
         self,
-        df_test_predictions,
-        train_predictions_mean,
-        train_predictions_sd,
-        human_labels_mean,
-        human_labels_sd,
-        trim_min,
-        trim_max,
-        trim_tolerance=0.4998,
-    ):
+        df_test_predictions: pd.DataFrame,
+        train_predictions_mean: float,
+        train_predictions_sd: float,
+        human_labels_mean: float,
+        human_labels_sd: float,
+        trim_min: float,
+        trim_max: float,
+        trim_tolerance: float = 0.4998,
+    ) -> pd.DataFrame:
         """
         Process predictions to create scaled, trimmed and rounded predictions.
 
         Parameters
         ----------
-        df_test_predictions : pandas DataFrame
+        df_test_predictions : pandas.DataFrame
             Data frame containing the test set predictions.
         train_predictions_mean : float
             The mean of the predictions on the training set.
         train_predictions_sd : float
-            The std. dev. of the predictions on the training
-            set.
+            The std. dev. of the predictions on the training set.
         human_labels_mean : float
-            The mean of the human scores used to train the
-            model.
+            The mean of the human scores used to train the model.
         human_labels_sd : float
-            The std. dev. of the human scores used to train
-            the model.
+            The std. dev. of the human scores used to train the model.
         trim_min : float
-            The lowest score on the score point, used for
-            trimming the raw regression predictions.
+            The lowest score on the score point, used for trimming the raw
+            regression predictions.
         trim_max : float
-            The highest score on the score point, used for
-            trimming the raw regression predictions.
+            The highest score on the score point, used for trimming the raw
+            regression predictions.
         trim_tolerance: float
-            Tolerance to be added to trim_max and substracted from
-            trim_min. Defaults to 0.4998.
+            Tolerance to be added to trim_max and substracted from trim_min.
+            Defaults to 0.4998.
 
         Returns
         -------
-        df_pred_processed : pandas DataFrame
-            Data frame containing the various trimmed
-            and rounded predictions.
+        df_pred_processed : pandas.DataFrame
+            Data frame containing the various trimmed and rounded predictions.
         """
         # rescale the test set predictions by boosting
         # them to match the human mean and SD
@@ -877,7 +906,9 @@ class FeaturePreprocessor:
 
         return df_pred_process
 
-    def filter_on_flag_columns(self, df, flag_column_dict):
+    def filter_on_flag_columns(
+        self, df: pd.DataFrame, flag_column_dict: Dict[str, Any]
+    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
         """
         Filter based on specific flag columns.
 
@@ -888,29 +919,28 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The DataFrame to filter on.
-        flag_column_dict : dict
+        flag_column_dict : Dict[str, Any]
             Dictionary containing the flag column information.
 
         Returns
         -------
-        df_responses_with_requested_flags : pandas DataFrame
-            Data frame containing the responses remaining
-            after filtering using the specified flag
-            columns.
-        df_responses_with_excluded_flags : pandas DataFrame
-            Data frame containing the responses filtered
-            out using the specified flag columns.
+        df_responses_with_requested_flags : pandas.DataFrame
+            Data frame containing the responses remaining after filtering using
+            the specified flag columns.
+        df_responses_with_excluded_flags : pandas.DataFrame
+            Data frame containing the responses filtered out using the specified
+            flag columns.
 
         Raises
         ------
         KeyError
-            If the columns listed in the dictionary are
-            not actually present in the data frame.
+            If the columns listed in the dictionary are not actually present in
+            the data frame.
         ValueError
-            If no responses remain after filtering based
-            on the flag column information.
+            If no responses remain after filtering based on the flag column
+            information.
         """
         df = df.copy()
 
@@ -977,8 +1007,12 @@ class FeaturePreprocessor:
             return (df_responses_with_requested_flags, df_responses_with_excluded_flags)
 
     def generate_feature_names(
-        self, df, reserved_column_names, feature_subset_specs, feature_subset
-    ):
+        self,
+        df: pd.DataFrame,
+        reserved_column_names: List[str],
+        feature_subset_specs: pd.DataFrame,
+        feature_subset: Optional[str],
+    ) -> List[str]:
         """
         Generate feature names from column names in data frame.
 
@@ -986,19 +1020,19 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame from which to generate feature names.
-        reserved_column_names : list of str
+        reserved_column_names : List[str]
             Names of reserved columns.
-        feature_subset_specs : pandas DataFrame
+        feature_subset_specs : pandas.DataFrame
             Feature subset specifications.
-        feature_subset : str
+        feature_subset : Optional[str]
             Feature subset column.
 
         Returns
         -------
-        feature_names : list of str
-            A list of features names.
+        feature_names : List[str]
+            List of generated features names.
         """
         df = df.copy()
 
@@ -1020,16 +1054,16 @@ class FeaturePreprocessor:
 
     def preprocess_feature(
         self,
-        values,
-        feature_name,
-        feature_transform,
-        feature_mean,
-        feature_sd,
-        exclude_zero_sd=False,
-        raise_error=True,
-        truncations=None,
-        truncate_outliers=True,
-    ):
+        values: np.ndarray,
+        feature_name: str,
+        feature_transform: str,
+        feature_mean: float,
+        feature_sd: float,
+        exclude_zero_sd: bool = False,
+        raise_error: bool = True,
+        truncations: Optional[pd.DataFrame] = None,
+        truncate_outliers: bool = True,
+    ) -> np.ndarray:
         """
         Remove outliers and transform the values in given numpy array.
 
@@ -1037,35 +1071,35 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        values : np.array
+        values : numpy.ndarray
             The feature values to preprocess.
         feature_name : str
             Name of the feature being pre-processed.
         feature_transform : str
             Name of the transformation function to apply.
         feature_mean : float
-            Mean value to use for outlier detection instead
-            of the mean of the given feature values.
+            Mean value to use for outlier detection instead of the mean of the
+            given feature values.
         feature_sd : float
-            Std. dev. value to use for outlier detection instead
-            of the std. dev. of the given feature values.
-        exclude_zero_sd : bool, optional
+            Std. dev. value to use for outlier detection instead of the std.
+            dev. of the given feature values.
+        exclude_zero_sd : bool
             Exclude the feature if it has zero standard deviation.
             Defaults to ``False``.
-        raise_error : bool, optional
+        raise_error : bool
             Raise an error if any of the transformations lead to "inf" values
             or may change the ranking of feature values.
             Defaults to ``True``.
-        truncations : pandas DataFrame, optional
-            A set of pre-defined truncation values.
+        truncations : Optional[pandas.DataFrame]
+            Set of pre-defined truncation values.
             Defaults to ``None``.
-        truncate_outliers : bool, optional
+        truncate_outliers : bool
             Whether to truncate outlier values.
             Defaults to ``True``.
 
         Returns
         -------
-        transformed_feature : np.array
+        transformed_feature : numpy.ndarray
             Numpy array containing the transformed and clamped
             feature values.
 
@@ -1117,13 +1151,13 @@ class FeaturePreprocessor:
 
     def preprocess_features(
         self,
-        df_train,
-        df_test,
-        df_feature_specs,
-        standardize_features=True,
-        use_truncations=False,
-        truncate_outliers=True,
-    ):
+        df_train: pd.DataFrame,
+        df_test: pd.DataFrame,
+        df_feature_specs: pd.DataFrame,
+        standardize_features: bool = True,
+        use_truncations: bool = False,
+        truncate_outliers: bool = True,
+    ) -> Tuple[pd.DataFrame, pd.DataFrame, pd.DataFrame]:
         """
         Preprocess features in given data using corresponding specifications.
 
@@ -1134,34 +1168,29 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df_train : pandas DataFrame
-            Data frame containing the raw feature values
-            for the training set.
-        df_test : pandas DataFrame
-            Data frame containing the raw feature values
-            for the test set.
-        df_feature_specs : pandas DataFrame
-            Data frame containing the various specifications
-            from the feature file.
-        standardize_features : bool, optional
+        df_train : pandas.DataFrame
+            Data frame containing the raw feature values for the training set.
+        df_test : pandas.DataFrame
+            Data frame containing the raw feature values for the test set.
+        df_feature_specs : pandas.DataFrame
+            Data frame containing the various specifications from the feature file.
+        standardize_features : bool
             Whether to standardize the features.
             Defaults to ``True``.
-        truncate_outliers : bool, optional
-            Truncate outlier values if set in the config file
+        truncate_outliers : bool
+            Truncate outlier values if set in the configuration file.
             Defaults to ``True``.
-        use_truncations : bool, optional
-            Whether we should use the truncation set
-            for removing outliers.
+        use_truncations : bool
+            Whether we should use the truncation set for removing outliers.
             Defaults to ``False``.
 
-
         Returns
         -------
-        df_train_preprocessed : pandas DataFrame
+        df_train_preprocessed : pandas.DataFrame
             Data frame with preprocessed training data.
-        df_test_preprocessed : pandas DataFrame
+        df_test_preprocessed : pandas.DataFrame
             Data frame with preprocessed test data.
-        df_feature_info : pandas DataFrame
+        df_feature_info : pandas.DataFrame
             Data frame with feature information.
         """
         # keep the original data frames and make copies
@@ -1257,25 +1286,36 @@ class FeaturePreprocessor:
 
     def filter_data(
         self,
-        df,
-        label_column,
-        id_column,
-        length_column,
-        second_human_score_column,
-        candidate_column,
-        requested_feature_names,
-        reserved_column_names,
-        given_trim_min,
-        given_trim_max,
-        flag_column_dict,
-        subgroups,
-        exclude_zero_scores=True,
-        exclude_zero_sd=False,
-        feature_subset_specs=None,
-        feature_subset=None,
-        min_candidate_items=None,
-        use_fake_labels=False,
-    ):
+        df: pd.DataFrame,
+        label_column: str,
+        id_column: str,
+        length_column: Optional[str],
+        second_human_score_column: Optional[str],
+        candidate_column: str,
+        requested_feature_names: List[str],
+        reserved_column_names: List[str],
+        given_trim_min: Optional[float],
+        given_trim_max: Optional[float],
+        flag_column_dict: Dict[str, Any],
+        subgroups: List[str],
+        exclude_zero_scores: bool = True,
+        exclude_zero_sd: bool = False,
+        feature_subset_specs: Optional[pd.DataFrame] = None,
+        feature_subset: Optional[str] = None,
+        min_candidate_items: Optional[int] = None,
+        use_fake_labels: bool = False,
+    ) -> Tuple[
+        pd.DataFrame,
+        pd.DataFrame,
+        pd.DataFrame,
+        pd.DataFrame,
+        pd.DataFrame,
+        pd.DataFrame,
+        pd.DataFrame,
+        float,
+        float,
+        List[str],
+    ]:
         """
         Filter rows with zero/non-numeric values for ``label_column``.
 
@@ -1297,70 +1337,70 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             The data frame to filter.
         label_column : str
             The label column in the data.
         id_column : str
             The ID column in the data.
-        length_column : str
+        length_column : Optional[str]
             The length column in the data.
-        second_human_score_column : str
+        second_human_score_column : Optional[str]
             The second human score column in the data.
         candidate_column : str
             The candidate column in the data.
-        requested_feature_names : list
+        requested_feature_names : List[str]
             A list of requested feature names.
-        reserved_column_names : list
+        reserved_column_names : List[str]
             A list of reserved column names.
-        given_trim_min : float
+        given_trim_min : Optional[float]
             The minimum trim value.
-        given_trim_max : float
+        given_trim_max : Optional[float]
             The maximum trim value.
-        flag_column_dict : dict
+        flag_column_dict : Dict[str, Any]
             A dictionary of flag columns.
-        subgroups : list, optional
-            A list of subgroups, if any.
+        subgroups : List[str]
+            List containing subgroup names.
         exclude_zero_scores : bool
             Whether to exclude zero scores.
             Defaults to ``True``.
-        exclude_zero_sd : bool, optional
+        exclude_zero_sd : bool
             Whether to exclude zero standard deviation.
             Defaults to ``False``.
-        feature_subset_specs : pandas DataFrame, optional
+        feature_subset_specs : Optional[pandas.DataFrame]
             The data frame containing the feature subset specifications.
             Defaults to ``None``.
-        feature_subset : str, optional
+        feature_subset : Optional[str]
             The feature subset group (e.g. 'A').
             Defaults to ``None``.
-        min_candidate_items : int, optional
+        min_candidate_items : Optional[int]
             The minimum number of items needed to include candidate.
             Defaults to ``None``.
-        use_fake_labels : bool, optional
+        use_fake_labels : bool
             Whether to use fake labels.
             Defaults to ``False``.
 
         Returns
         -------
-        df_filtered_features : pandas DataFrame
+        df_filtered_features : pandas.DataFrame
             Data frame with filtered features.
-        df_filtered_metadata : pandas DataFrame
+        df_filtered_metadata : pandas.DataFrame
             Data frame with filtered metadata.
-        df_filtered_other_columns : pandas DataFrame
+        df_filtered_other_columns : pandas.DataFrame
             Data frame with other columns filtered.
-        df_excluded : pandas DataFrame
+        df_excluded : pandas.DataFrame
             Data frame with excluded records.
-        df_filtered_length : pandas DataFrame
+        df_filtered_length : pandas.DataFrame
             Data frame with length column(s) filtered.
-        df_filtered_human_scores : pandas DataFrame
+        df_filtered_human_scores : pandas.DataFrame
             Data frame with human scores filtered.
-        df_responses_with_excluded_flags : pandas DataFrame
+        df_responses_with_excluded_flags : pandas.DataFrame
             Data frame containing responses with excluded flags.
         trim_min : float
             The maximum trim value.
         trim_max : float
             The minimum trim value.
-        feature_names  : list
+        feature_names : List[str]
             A list of feature names.
         """
         # make sure that the columns specified in the
@@ -1451,7 +1491,6 @@ class FeaturePreprocessor:
             (df_filtered, df_excluded) = self.filter_on_column(
                 df_responses_with_requested_flags,
                 "sc1",
-                "spkitemid",
                 exclude_zeros=exclude_zero_scores,
             )
 
@@ -1486,7 +1525,6 @@ class FeaturePreprocessor:
                 newdf, newdf_excluded = self.filter_on_column(
                     df_filtered,
                     feat,
-                    "spkitemid",
                     exclude_zeros=False,
                     exclude_zero_sd=exclude_zero_sd,
                 )
@@ -1625,7 +1663,9 @@ class FeaturePreprocessor:
             feature_names,
         )
 
-    def process_data_rsmtool(self, config_obj, data_container_obj):
+    def process_data_rsmtool(
+        self, config_obj: Configuration, data_container_obj: DataContainer
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Set up rsmtool experiment by loading & preprocessing train/test data.
 
@@ -1635,17 +1675,17 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        config_obj : configuration_parser.Configuration
-            A configuration object.
-        data_container_obj : container.DataContainer
-            A data container object.
+        config_obj : Configuration
+            The configuration object.
+        data_container_obj : DataContainer
+            The data container object.
 
         Returns
         -------
-        config_obj : configuration_parser.Configuration
-            A new configuration object.
-        data_container : container.DataContainer
-            A new data container object.
+        config_obj : Configuration
+            New configuration object containing the updated rsmtool configuration.
+        data_container : DataContainer
+            New data container object containing the preprocessed data.
 
         Raises
         ------
@@ -1662,8 +1702,8 @@ class FeaturePreprocessor:
             If "use_truncations" was specified in the configuration,
             but no feature CSV file was found.
         """
-        train = data_container_obj.train
-        test = data_container_obj.test
+        train = data_container_obj["train"]
+        test = data_container_obj["test"]
         feature_specs = data_container_obj.get_frame("feature_specs")
         feature_subset = data_container_obj.get_frame("feature_subset_specs")
 
@@ -1679,7 +1719,7 @@ class FeaturePreprocessor:
         feature_subset_file = config_obj["feature_subset_file"]
 
         if feature_subset_file is not None:
-            feature_subset_file = DataReader.locate_files(feature_subset_file, configdir)
+            feature_subset_file = DataReader.locate_files(feature_subset_file, configdir)[0]
 
         # get the column name for the labels for the training and testing data
         train_label_column = config_obj["train_label_column"]
@@ -1693,7 +1733,7 @@ class FeaturePreprocessor:
         (
             spec_trim_min,
             spec_trim_max,
-            spec_trim_tolerance,
+            _,
         ) = config_obj.get_trim_min_max_tolerance()
 
         # get the name of the optional column that
@@ -2019,36 +2059,42 @@ class FeaturePreprocessor:
         for key, value in internal_options_dict.items():
             new_config_obj[key] = value
 
-        new_container = [
-            {"name": "train_features", "frame": df_train_features},
-            {"name": "test_features", "frame": df_test_features},
-            {
-                "name": "train_preprocessed_features",
-                "frame": df_train_preprocessed_features,
-            },
-            {
-                "name": "test_preprocessed_features",
-                "frame": df_test_preprocessed_features,
-            },
-            {"name": "train_metadata", "frame": df_train_metadata},
-            {"name": "test_metadata", "frame": df_test_metadata},
-            {"name": "train_other_columns", "frame": df_train_other_columns},
-            {"name": "test_other_columns", "frame": df_test_other_columns},
-            {"name": "train_excluded", "frame": df_train_excluded},
-            {"name": "test_excluded", "frame": df_test_excluded},
-            {"name": "train_length", "frame": df_train_length},
-            {"name": "test_human_scores", "frame": df_test_human_scores},
-            {"name": "train_flagged", "frame": df_train_flagged_responses},
-            {"name": "test_flagged", "frame": df_test_flagged_responses},
-            {"name": "feature_specs", "frame": feature_specs},
-            {"name": "feature_info", "frame": df_feature_info},
+        new_container_datasets = [
+            DatasetDict({"name": "train_features", "frame": df_train_features}),
+            DatasetDict({"name": "test_features", "frame": df_test_features}),
+            DatasetDict(
+                {
+                    "name": "train_preprocessed_features",
+                    "frame": df_train_preprocessed_features,
+                }
+            ),
+            DatasetDict(
+                {
+                    "name": "test_preprocessed_features",
+                    "frame": df_test_preprocessed_features,
+                }
+            ),
+            DatasetDict({"name": "train_metadata", "frame": df_train_metadata}),
+            DatasetDict({"name": "test_metadata", "frame": df_test_metadata}),
+            DatasetDict({"name": "train_other_columns", "frame": df_train_other_columns}),
+            DatasetDict({"name": "test_other_columns", "frame": df_test_other_columns}),
+            DatasetDict({"name": "train_excluded", "frame": df_train_excluded}),
+            DatasetDict({"name": "test_excluded", "frame": df_test_excluded}),
+            DatasetDict({"name": "train_length", "frame": df_train_length}),
+            DatasetDict({"name": "test_human_scores", "frame": df_test_human_scores}),
+            DatasetDict({"name": "train_flagged", "frame": df_train_flagged_responses}),
+            DatasetDict({"name": "test_flagged", "frame": df_test_flagged_responses}),
+            DatasetDict({"name": "feature_specs", "frame": feature_specs}),
+            DatasetDict({"name": "feature_info", "frame": df_feature_info}),
         ]
 
-        new_container = DataContainer(new_container)
+        new_container = DataContainer(datasets=new_container_datasets)
 
         return new_config_obj, new_container
 
-    def process_data_rsmeval(self, config_obj, data_container_obj):
+    def process_data_rsmeval(
+        self, config_obj: Configuration, data_container_obj: DataContainer
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Set up rsmeval experiment by loading & preprocessing evaluation data.
 
@@ -2058,17 +2104,17 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        config_obj : configuration_parser.Configuration
-            A configuration object.
-        data_container_obj : container.DataContainer
-            A data container object.
+        config_obj : Configuration
+            The configuration object.
+        data_container_obj : DataContainer
+            The data container object.
 
         Returns
         -------
-        config_obj : configuration_parser.Configuration
-            A new configuration object.
-        data_container : container.DataContainer
-            A new data container object.
+        config_obj : Configuration
+            New configuration object containing the updated rsmeval configuration.
+        data_container : DataContainer
+            New data container object containing the preprocessed data.
 
         Raises
         ------
@@ -2091,7 +2137,7 @@ class FeaturePreprocessor:
         """
         # get the directory where the config file lives
         configpath = config_obj.configdir
-        pred_file_location = DataReader.locate_files(config_obj["predictions_file"], configpath)
+        pred_file_location = DataReader.locate_files(config_obj["predictions_file"], configpath)[0]
 
         # get the column name for the labels for the training and testing data
         human_score_column = config_obj["human_score_column"]
@@ -2122,13 +2168,22 @@ class FeaturePreprocessor:
         id_column = config_obj["id_column"]
 
         # get the specified trim min and max, if any
-        # and make sure they are numeric
+        # and make sure they are valid numbers and not None
         (
             spec_trim_min,
             spec_trim_max,
             spec_trim_tolerance,
         ) = config_obj.get_trim_min_max_tolerance()
 
+        # this should never happen since `trim_min` and `trim_max` are required
+        # fields for rsmeval and `trim_tolerance` always has a default value
+        # but we need to check it here to satisfy mypy
+        assert (
+            spec_trim_min is not None
+            and spec_trim_max is not None
+            and spec_trim_tolerance is not None
+        )
+
         # get the subgroups if any
         subgroups = config_obj.get("subgroups")
 
@@ -2203,7 +2258,7 @@ class FeaturePreprocessor:
 
         self.logger.info(message)
 
-        df_pred = data_container_obj.predictions
+        df_pred = data_container_obj["predictions"]
 
         # make sure that the columns specified in the config file actually exist
         columns_to_check = [id_column, human_score_column, system_score_column]
@@ -2251,7 +2306,6 @@ class FeaturePreprocessor:
         df_filtered, df_excluded = self.filter_on_column(
             df_responses_with_requested_flags,
             "sc1",
-            "spkitemid",
             exclude_zeros=exclude_zero_scores,
         )
 
@@ -2272,9 +2326,7 @@ class FeaturePreprocessor:
         df_excluded["raw"] = pd.to_numeric(df_excluded["raw"], errors="coerce").astype(float)
 
         # filter out the non-numeric machine scores from the rest of the data
-        newdf, newdf_excluded = self.filter_on_column(
-            df_filtered, "raw", "spkitemid", exclude_zeros=False
-        )
+        newdf, newdf_excluded = self.filter_on_column(df_filtered, "raw", exclude_zeros=False)
 
         del df_filtered
         df_filtered_pred = newdf
@@ -2322,8 +2374,8 @@ class FeaturePreprocessor:
 
         if data_container_obj.get_frame("scale") is not None:
             if (
-                "sc1" not in data_container_obj.scale.columns
-                and "prediction" not in data_container_obj.scale.columns
+                "sc1" not in data_container_obj["scale"].columns
+                and "prediction" not in data_container_obj["scale"].columns
             ):
                 raise KeyError(
                     "The CSV file specified for scaling ",
@@ -2331,12 +2383,12 @@ class FeaturePreprocessor:
                 )
             else:
                 scale_pred_mean, scale_pred_sd = (
-                    data_container_obj.scale["prediction"].mean(),
-                    data_container_obj.scale["prediction"].std(),
+                    data_container_obj["scale"]["prediction"].mean(),
+                    data_container_obj["scale"]["prediction"].std(),
                 )
                 scale_human_mean, scale_human_sd = (
-                    data_container_obj.scale["sc1"].mean(),
-                    data_container_obj.scale["sc1"].std(),
+                    data_container_obj["scale"]["sc1"].mean(),
+                    data_container_obj["scale"]["sc1"].std(),
                 )
 
         self.logger.info("Processing predictions")
@@ -2438,13 +2490,17 @@ class FeaturePreprocessor:
             "test_responses_with_excluded_flags",
         ]
 
-        new_container = [{"name": name, "frame": frame} for frame, name in zip(frames, names)]
+        new_container_datasets = [
+            DatasetDict({"name": name, "frame": frame}) for frame, name in zip(frames, names)
+        ]
 
-        new_container = DataContainer(new_container)
+        new_container = DataContainer(datasets=new_container_datasets)
 
         return new_config_obj, new_container
 
-    def process_data_rsmpredict(self, config_obj, data_container_obj):
+    def process_data_rsmpredict(
+        self, config_obj: Configuration, data_container_obj: DataContainer
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Process data for rsmpredict experiments.
 
@@ -2454,17 +2510,17 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        config_obj : configuration_parser.Configuration
-            A configuration object.
-        data_container_obj : container.DataContainer
-            A data container object.
+        config_obj : Configuration
+            The configuration object.
+        data_container_obj : DataContainer
+            The data container object.
 
         Returns
         -------
-        config_obj : configuration_parser.Configuration
-            A new configuration object.
-        data_congtainer : container.DataContainer
-            A new data container object.
+        config_obj : Configuration
+            New configuration object containing the updated rsmpredict configuration.
+        data_congtainer : DataContainer
+            New data container object containing the preprocessed data.
 
         Raises
         ------
@@ -2473,9 +2529,9 @@ class FeaturePreprocessor:
         ValueError
             If data contains duplicate response IDs.
         """
-        df_input = data_container_obj.input_features
-        df_feature_info = data_container_obj.feature_info
-        df_postproc_params = data_container_obj.postprocessing_params
+        df_input = data_container_obj["input_features"]
+        df_feature_info = data_container_obj["feature_info"]
+        df_postproc_params = data_container_obj["postprocessing_params"]
 
         # get the column name that will hold the ID
         id_column = config_obj["id_column"]
@@ -2628,18 +2684,22 @@ class FeaturePreprocessor:
         ]
 
         datasets = [
-            {"name": "features_processed", "frame": df_features_preprocessed},
-            {"name": "excluded", "frame": df_excluded},
-            {
-                "name": "predictions_with_metadata",
-                "frame": df_predictions_with_metadata,
-            },
-            {"name": "predictions", "frame": df_predictions},
+            DatasetDict({"name": "features_processed", "frame": df_features_preprocessed}),
+            DatasetDict({"name": "excluded", "frame": df_excluded}),
+            DatasetDict(
+                {
+                    "name": "predictions_with_metadata",
+                    "frame": df_predictions_with_metadata,
+                }
+            ),
+            DatasetDict({"name": "predictions", "frame": df_predictions}),
         ]
 
-        return config_obj, DataContainer(datasets)
+        return config_obj, DataContainer(datasets=datasets)
 
-    def process_data_rsmexplain(self, config_obj, data_container_obj):
+    def process_data_rsmexplain(
+        self, config_obj: Configuration, data_container_obj: DataContainer
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Process data for rsmexplain experiments.
 
@@ -2649,26 +2709,26 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        config_obj : configuration_parser.Configuration
-            A configuration object.
-        data_container_obj : container.DataContainer
-            A data container object.
+        config_obj : Configuration
+            The configuration object.
+        data_container_obj : DataContainer
+            The data container object.
 
         Returns
         -------
-        config_obj : configuration_parser.Configuration
-            A new configuration object.
-        data_congtainer : container.DataContainer
-            A new data container object.
+        config_obj : Configuration
+            New configuration object containing the updated rsmexplain configuration.
+        data_congtainer : DataContainer
+            New data container object containing the preprocessed data.
 
         Raises
         ------
         ValueError
             If data contains duplicate response IDs.
         """
-        df_background_features = data_container_obj.background_features
-        df_explain_features = data_container_obj.explain_features
-        df_feature_info = data_container_obj.feature_info
+        df_background_features = data_container_obj["background_features"]
+        df_explain_features = data_container_obj["explain_features"]
+        df_feature_info = data_container_obj["feature_info"]
 
         # get the column name that holds the IDs
         id_column = config_obj["id_column"]
@@ -2741,32 +2801,38 @@ class FeaturePreprocessor:
 
         # return the container with the pre-processed features
         datasets = [
-            {"name": "background_features_preprocessed", "frame": df_background_preprocessed},
-            {"name": "explain_features_preprocessed", "frame": df_explain_preprocessed},
+            DatasetDict(
+                {"name": "background_features_preprocessed", "frame": df_background_preprocessed}
+            ),
+            DatasetDict(
+                {"name": "explain_features_preprocessed", "frame": df_explain_preprocessed}
+            ),
         ]
 
-        return config_obj, DataContainer(datasets)
+        return config_obj, DataContainer(datasets=datasets)
 
-    def process_data(self, config_obj, data_container_obj, context="rsmtool"):
+    def process_data(
+        self, config_obj: Configuration, data_container_obj: DataContainer, context: str = "rsmtool"
+    ) -> Tuple[Configuration, DataContainer]:
         """
         Process and setup the data for an experiment in the given context.
 
         Parameters
         ----------
-        config_obj : configuration_parser.Configuration
-            A configuration object.
-        data_container_obj : container.DataContainer
-            A data container object.
+        config_obj : Configuration
+            The configuration object.
+        data_container_obj : DataContainer
+            The data container object.
         context : str
             The tool context: one of {"rsmtool", "rsmeval", "rsmpredict"}.
             Defaults to "rsmtool".
 
         Returns
         -------
-        config_obj : configuration_parser.Configuration
-            A new configuration object.
-        data_container : container.DataContainer
-            A new data container object.
+        config_obj : Configuration
+            New configuration object containing the updated configuration.
+        data_container : DataContainer
+            new data container object containing the preprocessed data.
 
         Raises
         ------
@@ -2788,8 +2854,12 @@ class FeaturePreprocessor:
             )
 
     def preprocess_new_data(
-        self, df_input, df_feature_info, standardize_features=True, truncate_outliers=True
-    ):
+        self,
+        df_input: pd.DataFrame,
+        df_feature_info: pd.DataFrame,
+        standardize_features: bool = True,
+        truncate_outliers: bool = True,
+    ) -> Tuple[pd.DataFrame, pd.DataFrame]:
         """
         Preprocess feature values using the parameters in ``df_feature_info``.
 
@@ -2798,12 +2868,12 @@ class FeaturePreprocessor:
 
         Parameters
         ----------
-        df_input : pandas DataFrame
+        df_input : pandas.DataFrame
             Data frame with raw feature values that will be used to generate
             the scores. Each feature is stored in a separate column. Each row
             corresponds to one response. There should also be a column named
             "spkitemid" containing a unique ID for each response.
-        df_feature_info : pandas DataFrame
+        df_feature_info : pandas.DataFrame
             Data frame with preprocessing parameters in the following columns:
 
             - "feature" : the name of the feature; should match the feature names
@@ -2817,19 +2887,18 @@ class FeaturePreprocessor:
             - "train_transformed_mean", "train_transformed_sd" : mean and standard
               deviation for computing z-scores.
 
-        standardize_features : bool, optional
+        standardize_features : bool
             Whether the features should be standardized prior to prediction.
             Defaults to ``True``.
-
-        truncate_outliers : bool, optional
+        truncate_outliers : bool
             Whether outlier should be truncated prior to prediction.
             Defaults to ``True``.
 
         Returns
         -------
-        df_features_preprocessed : pandas DataFrame
+        df_features_preprocessed : pandas.DataFrame
             Data frame with processed feature values.
-        df_excluded: pandas DataFrame
+        df_excluded: pandas.DataFrame
             Data frame with responses excluded from further analysis
             due to non-numeric feature values in the original file
             or after applying transformations. This data frame always
@@ -2886,7 +2955,6 @@ class FeaturePreprocessor:
             newdf, newdf_excluded = self.filter_on_column(
                 df_filtered,
                 feature_name,
-                "spkitemid",
                 exclude_zeros=False,
                 exclude_zero_sd=False,
             )
@@ -2941,7 +3009,6 @@ class FeaturePreprocessor:
                     (newdf, newdf_excluded) = self.filter_on_column(
                         df_features_preprocess,
                         feature_name,
-                        "spkitemid",
                         exclude_zeros=False,
                         exclude_zero_sd=False,
                     )
diff --git a/rsmtool/reader.py b/rsmtool/reader.py
index d1ab2946..ab62bc29 100644
--- a/rsmtool/reader.py
+++ b/rsmtool/reader.py
@@ -11,10 +11,11 @@ Classes for reading data files (or dictionaries) into DataContainer objects.
 import warnings
 from functools import partial
 from os.path import abspath, exists, join, splitext
+from typing import Any, Dict, List, Optional, Union
 
 import pandas as pd
 
-from .container import DataContainer
+from .container import DataContainer, DatasetDict
 
 # allow older versions of pandas to work
 try:
@@ -23,7 +24,7 @@ except ImportError:
     from pandas.errors import DtypeWarning
 
 
-def read_jsonlines(filename, converters=None):
+def read_jsonlines(filename: str, converters: Optional[Dict[str, Any]] = None) -> pd.DataFrame:
     """
     Read a data file in .jsonlines format into a data frame.
 
@@ -33,7 +34,7 @@ def read_jsonlines(filename, converters=None):
     ----------
     filename: str
         Name of file to read.
-    converters : dict, optional
+    converters : Optional[Dict[str, Any]]
         A dictionary specifying how the types of the columns
         in the file should be converted. Specified in the same
         format as for ``pandas.read_csv()``.
@@ -41,7 +42,7 @@ def read_jsonlines(filename, converters=None):
 
     Returns
     -------
-    df : pandas DataFrame
+    df : pandas.DataFrame
          Data frame containing the data in the given file.
     """
     try:
@@ -78,7 +79,13 @@ def read_jsonlines(filename, converters=None):
     return df
 
 
-def try_to_load_file(filename, converters=None, raise_error=False, raise_warning=False, **kwargs):
+def try_to_load_file(
+    filename: str,
+    converters: Optional[Dict[str, Any]] = None,
+    raise_error: bool = False,
+    raise_warning: bool = False,
+    **kwargs,
+) -> Optional[pd.DataFrame]:
     """
     Read a single file, if it exists.
 
@@ -89,21 +96,21 @@ def try_to_load_file(filename, converters=None, raise_error=False, raise_warning
     ----------
     filename : str
         Name of file to read.
-    converters : dict, optional
+    converters : Optional[Dict[str, Any]]
         A dictionary specifying how the types of the columns
         in the file should be converted. Specified in the same
         format as for ``pandas.read_csv()``.
         Defaults to ``None``.
-    raise_error : bool, optional
+    raise_error : bool
         Raise an error if the file cannot be located.
         Defaults to ``False``.
-    raise_warning : bool, optional
+    raise_warning : bool
         Raise a warning if the file cannot be located.
         Defaults to ``False``.
 
     Returns
     -------
-    df : pandas DataFrame or ``None``
+    df : Optional[pandas.DataFrame]
         DataFrame containing the data in the given file,
         or ``None`` if the file does not exist.
 
@@ -112,31 +119,37 @@ def try_to_load_file(filename, converters=None, raise_error=False, raise_warning
     FileNotFoundError
         If ``raise_error`` is ``True`` and the file cannot be located.
     """
-    if exists(filename):
+    if not exists(filename):
+        message = f"The file '{filename}' could not be located."
+        if raise_error:
+            raise FileNotFoundError(message)
+        if raise_warning:
+            warnings.warn(message)
+        return None
+    else:
         return DataReader.read_from_file(filename, converters, **kwargs)
 
-    message = f"The file '{filename}' could not be located."
-    if raise_error:
-        raise FileNotFoundError(message)
-
-    if raise_warning:
-        warnings.warn(message)
-
 
 class DataReader:
     """Class to generate DataContainer objects."""
 
-    def __init__(self, filepaths, framenames, file_converters=None):
+    def __init__(
+        self,
+        filepaths: List[str],
+        framenames: List[str],
+        file_converters: Optional[Dict[str, Dict[str, Any]]] = None,
+    ):
         """
         Initialize a DataReader object.
 
         Parameters
         ----------
-        filepaths : list of str
-            A list of paths to files that are to be read in.
-        framenames : list of str
+        filepaths : List[str]
+            A list of paths to files that are to be read in. Some of the paths
+            can be empty strings.
+        framenames : List[str]
             A list of names for the data sets to be included in the container.
-        file_converters : dict of dicts, optional
+        file_converters : Optional[Dict[str, Dict[str, Any]]]
             A dictionary of file converter dicts. The keys are the data set
             names and the values are the converter dictionaries to be applied
             to the corresponding data set.
@@ -155,17 +168,14 @@ class DataReader:
             If any of the specified file paths is ``None``.
         """
         # Default datasets list
-        self.datasets = []
+        self.datasets: List[DatasetDict] = []
 
         # Make sure filepaths length matches frame names length
         assert len(filepaths) == len(framenames)
 
-        # Make sure that there are no Nones in the filepaths
-        if None in filepaths:
-            frames_with_no_path = [
-                framenames[i] for i in range(len(framenames)) if filepaths[i] is None
-            ]
-
+        # Make sure that there are no empty strings in the filepaths
+        frames_with_no_path = [framenames[i] for i in range(len(framenames)) if filepaths[i] == ""]
+        if frames_with_no_path:
             raise ValueError(f"No path specified for {' ,'.join(frames_with_no_path)}")
 
         # Assign names and paths lists
@@ -202,7 +212,7 @@ class DataReader:
         self.file_converters = {} if file_converters is None else file_converters
 
     @staticmethod
-    def read_from_file(filename, converters=None, **kwargs):
+    def read_from_file(filename: str, converters: Optional[Dict[str, Any]] = None, **kwargs):
         """
         Read a CSV/TSV/XLSX/JSONLINES/SAS7BDAT file and return a data frame.
 
@@ -210,7 +220,7 @@ class DataReader:
         ----------
         filename : str
             Name of file to read.
-        converters : dict, optional
+        converters : Optional[Dict[str, Any]]
             A dictionary specifying how the types of the columns
             in the file should be converted. Specified in the same
             format as for `pandas.read_csv()``.
@@ -218,7 +228,7 @@ class DataReader:
 
         Returns
         -------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             Data frame containing the data in the given file.
 
         Raises
@@ -269,17 +279,17 @@ class DataReader:
         return df
 
     @staticmethod
-    def locate_files(filepaths, configdir):
+    def locate_files(filepaths: Union[str, List[str]], configdir: str) -> List[str]:
         """
         Locate an experiment file, or a list of experiment files.
 
         If the given path doesn't exist, then maybe the path is relative
-        to the path of the config file. If neither exists, then return
-        ``None``.
+        to the path of the config file. If neither exists, then return an
+        empty string.
 
         Parameters
         ----------
-        filepaths : str or list
+        filepaths : Union[str, List[str]]
             Name(s) of the experiment file we want to locate.
         configdir : str
             Path to the reference configuration directory
@@ -287,11 +297,10 @@ class DataReader:
 
         Returns
         -------
-        retval :  str or list
-            Absolute path to the experiment file or ``None``
-            if the file could not be located. If ``filepaths``
-            was a string, this method will return a string.
-            Otherwise, it will return a list.
+        retval : List[str]
+            List of absolute paths to the located files. If a file
+            does not exist, the corresponding element in the list
+            is an empty string.
 
         Raises
         ------
@@ -307,13 +316,10 @@ class DataReader:
 
         if isinstance(filepaths, str):
             filepaths = [filepaths]
-            return_string = True
-        else:
-            return_string = False
 
         located_paths = []
         for filepath in filepaths:
-            retval = None
+            retval = ""
             alternate_path = abspath(join(configdir, filepath))
 
             # if the given path exists as is, convert
@@ -328,25 +334,25 @@ class DataReader:
 
             located_paths.append(retval)
 
-        if return_string:
-            return located_paths[0]
-
         return located_paths
 
-    def read(self, kwargs_dict=None):
+    def read(self, kwargs_dict: Optional[Dict[str, Dict[str, Any]]] = None) -> DataContainer:
         """
         Read all files contained in ``self.dataset_paths``.
 
         Parameters
         ----------
-        kwargs_dict : dict of dicts, optional
-            Any additional keyword arguments to pass to a particular DataFrame.
-            These arguments will be passed to the pandas IO reader function.
+        kwargs_dict : Optional[Dict[str, Dict[str, Any]]]
+            Dictionary with the names of the datasets as keys and
+            dictionaries of keyword arguments to pass to the pandas
+            reader for each dataset as values. The keys in those
+            dictionaries are the names of the keyword arguments and
+            the values are the values of the keyword arguments.
             Defaults to ``None``.
 
         Returns
         -------
-        datacontainer : container.DataContainer
+        datacontainer : DataContainer
             A data container object.
 
         Raises
@@ -358,7 +364,7 @@ class DataReader:
             name = self.dataset_names[idx]
             converter = self.file_converters.get(name, None)
 
-            if not exists(set_path):
+            if not set_path or not exists(set_path):
                 raise FileNotFoundError(f"The file {set_path} does not exist")
 
             if kwargs_dict is not None:
diff --git a/rsmtool/reporter.py b/rsmtool/reporter.py
index 80f6a638..30154641 100644
--- a/rsmtool/reporter.py
+++ b/rsmtool/reporter.py
@@ -152,7 +152,7 @@ class Reporter:
         """
         custom_report_sections = []
         for cs_path in custom_report_section_paths:
-            cs_location = DataReader.locate_files(cs_path, configdir)
+            cs_location = DataReader.locate_files(cs_path, configdir)[0]
             if not cs_location:
                 raise FileNotFoundError(f"Error: custom section not found at {cs_path}.")
             else:
diff --git a/rsmtool/rsmcompare.py b/rsmtool/rsmcompare.py
index 4e7a410f..a0493682 100644
--- a/rsmtool/rsmcompare.py
+++ b/rsmtool/rsmcompare.py
@@ -100,7 +100,7 @@ def run_comparison(
     experiment_id_old = configuration["experiment_id_old"]
     experiment_dir_old = DataReader.locate_files(
         configuration["experiment_dir_old"], configuration.configdir
-    )
+    )[0]
     if not experiment_dir_old:
         raise FileNotFoundError(
             f"The directory {configuration['experiment_dir_old']} " f"does not exist."
@@ -120,7 +120,7 @@ def run_comparison(
     experiment_id_new = configuration["experiment_id_new"]
     experiment_dir_new = DataReader.locate_files(
         configuration["experiment_dir_new"], configuration.configdir
-    )
+    )[0]
     if not experiment_dir_new:
         raise FileNotFoundError(
             f"The directory {configuration['experiment_dir_new']} " f"does not exist."
diff --git a/rsmtool/rsmeval.py b/rsmtool/rsmeval.py
index 6882a919..c4699a77 100644
--- a/rsmtool/rsmeval.py
+++ b/rsmtool/rsmeval.py
@@ -125,7 +125,10 @@ def run_evaluation(
     writer = DataWriter(configuration["experiment_id"], configuration.context, wandb_run)
 
     # Make sure prediction file can be located
-    if not DataReader.locate_files(configuration["predictions_file"], configuration.configdir):
+    pred_file_location = DataReader.locate_files(
+        configuration["predictions_file"], configuration.configdir
+    )[0]
+    if not pred_file_location:
         raise FileNotFoundError(
             f"Error: Predictions file {configuration['predictions_file']} " f"not found."
         )
@@ -153,7 +156,7 @@ def run_evaluation(
     # If we want to do scaling, get the scale file
     if do_scaling:
         # Make sure scale file can be located
-        scale_file_location = DataReader.locate_files(scale_with, configuration.configdir)
+        scale_file_location = DataReader.locate_files(scale_with, configuration.configdir)[0]
         if not scale_file_location:
             raise FileNotFoundError(f"Could not find scaling file {scale_file_location}.")
 
diff --git a/rsmtool/rsmexplain.py b/rsmtool/rsmexplain.py
index 522fd45c..42e8cfb8 100644
--- a/rsmtool/rsmexplain.py
+++ b/rsmtool/rsmexplain.py
@@ -247,7 +247,7 @@ def generate_explanation(
     # find the rsmtool experiment directory
     experiment_dir = DataReader.locate_files(
         configuration["experiment_dir"], configuration.configdir
-    )
+    )[0]
     if not experiment_dir:
         raise FileNotFoundError(f"The directory {configuration['experiment_dir']} does not exist.")
     else:
@@ -334,7 +334,7 @@ def generate_explanation(
     container = reader.read(kwargs_dict={"feature_info": {"index_col": 0}})
 
     # ensure that the background data is large enough for meaningful explanations
-    background_data_size = len(container.background_features)
+    background_data_size = len(container["background_features"])
     if background_data_size < 300:
         logger.error(
             f"The background data {background_data_path} contains only "
@@ -353,15 +353,19 @@ def generate_explanation(
 
     # create featuresets from pre-processed background and explain features
     background_fs = FeatureSet.from_data_frame(
-        processed_container.background_features_preprocessed, "background"
+        processed_container["background_features_preprocessed"], "background"
     )
     explain_fs = FeatureSet.from_data_frame(
-        processed_container.explain_features_preprocessed, "explain"
+        processed_container["explain_features_preprocessed"], "explain"
     )
 
     # get the SKLL learner object for the rsmtool experiment and its feature names
     modeler = Modeler.load_from_file(join(experiment_output_dir, f"{experiment_id}.model"))
     learner = modeler.learner
+
+    # at this point learner should be a valid SKLL learner but let's confirm
+    # to satisfy mypy
+    assert learner is not None
     feature_names = list(learner.get_feature_names_out())
 
     # compute the background kmeans distribution
diff --git a/rsmtool/rsmpredict.py b/rsmtool/rsmpredict.py
index 76e5beea..b412bc4b 100644
--- a/rsmtool/rsmpredict.py
+++ b/rsmtool/rsmpredict.py
@@ -37,8 +37,8 @@ def fast_predict(
     modeler: Modeler,
     df_feature_info: Optional[pd.DataFrame] = None,
     trim: bool = False,
-    trim_min: Optional[int] = None,
-    trim_max: Optional[int] = None,
+    trim_min: Optional[float] = None,
+    trim_max: Optional[float] = None,
     trim_tolerance: Optional[float] = None,
     scale: bool = False,
     train_predictions_mean: Optional[float] = None,
@@ -103,14 +103,14 @@ def fast_predict(
         ``trim_max`` must be specified or be available as attributes of
         the ``modeler``.
         Defaults to ``False``.
-    trim_min : Optional[int]
-        The lowest possible integer score that the machine should predict.
+    trim_min : Optional[float]
+        The lowest possible score that the machine should predict.
         If ``None``, this function will try to extract this value from
         ``modeler``. If ``None``, no such attribute exists, and
         ``trim=True``, a ``ValueError`` will be raised.
         Defaults to ``None``.
-    trim_max : Optional[int]
-        The highest possible integer score that the machine should predict.
+    trim_max : Optional[float]
+        The highest possible score that the machine should predict.
         If ``None``, this function will try to extract this value from
         ``modeler``. If ``None``, no such attribute exists, and
         ``trim=True``, a ``ValueError`` will be raised.
@@ -266,23 +266,34 @@ def fast_predict(
             "'trim_min' and 'trim_max' modeler attributes must be present and not "
             "``None``."
         )
+
+        # get the value of the trim tolerance if not provided
         default_trim_tolerance = 0.4998
         if trim_tolerance is None:
             try:
                 trim_tolerance = modeler.trim_tolerance
-                if trim_tolerance is None:
-                    trim_tolerance = default_trim_tolerance
+            # if the attribute is not present, use default value
             except AttributeError:
                 trim_tolerance = default_trim_tolerance
+            # if it is present but has a value of None, also use default value
+            else:
+                if trim_tolerance is None:
+                    trim_tolerance = default_trim_tolerance
+
+        # get the values of the trim min and max if not provided
         try:
             if trim_min is None:
                 trim_min = modeler.trim_min
             if trim_max is None:
                 trim_max = modeler.trim_max
-            if any(arg is None for arg in [trim_tolerance, trim_min, trim_max]):
-                raise ValueError(trim_args_error_message) from None
+        # raise an error if the attributes are not present
         except AttributeError:
             raise ValueError(trim_args_error_message) from None
+        # if they are present but have a value of None, also raise an error
+        else:
+            if trim_min is None or trim_max is None:
+                raise ValueError(trim_args_error_message) from None
+
         for column in df_predictions.columns:
             df_predictions[f"{column}_trim"] = preprocessor.trim(
                 df_predictions[column], trim_min, trim_max, trim_tolerance
@@ -375,13 +386,13 @@ def compute_and_save_predictions(
     # for which we want to generate the predictions
     input_features_file = DataReader.locate_files(
         configuration["input_features_file"], configuration.configdir
-    )
+    )[0]
     if not input_features_file:
         raise FileNotFoundError(f"Input file {configuration['input_features_file']} does not exist")
 
     experiment_dir = DataReader.locate_files(
         configuration["experiment_dir"], configuration.configdir
-    )
+    )[0]
     if not experiment_dir:
         raise FileNotFoundError(f"The directory {configuration['experiment_dir']} does not exist.")
     else:
@@ -494,7 +505,7 @@ def compute_and_save_predictions(
     )
 
     # save excluded responses to disk
-    if not processed_container.excluded.empty:
+    if not processed_container["excluded"].empty:
         # save the predictions to disk
         logger.info(
             f"Saving excluded responses to "
diff --git a/rsmtool/rsmsummarize.py b/rsmtool/rsmsummarize.py
index cac36e96..87d8c3b3 100644
--- a/rsmtool/rsmsummarize.py
+++ b/rsmtool/rsmsummarize.py
@@ -60,7 +60,7 @@ def check_experiment_dir(
         If ``experiment_dir`` contains several JSON configuration
         files instead of just one.
     """
-    full_path_experiment_dir = DataReader.locate_files(experiment_dir, configpath)
+    full_path_experiment_dir = DataReader.locate_files(experiment_dir, configpath)[0]
     if not full_path_experiment_dir:
         raise FileNotFoundError(f"The directory {experiment_dir} does not exist.")
     else:
diff --git a/rsmtool/rsmtool.py b/rsmtool/rsmtool.py
index 546f22d0..643fbe14 100644
--- a/rsmtool/rsmtool.py
+++ b/rsmtool/rsmtool.py
@@ -19,6 +19,8 @@ from typing import Any, Dict, List, Optional, Union
 
 from wandb.wandb_run import Run
 
+from rsmtool.container import DatasetDict
+
 from .analyzer import Analyzer
 from .configuration_parser import Configuration, configure
 from .modeler import Modeler
@@ -222,7 +224,7 @@ def run_experiment(
     # Initialize modeler
     modeler = Modeler(logger=logger)
 
-    modeler.train(processed_config, processed_container, csvdir, figdir, file_format)
+    modeler.train(processed_config, processed_container, csvdir, file_format)
 
     # Identify the features used by the model
     selected_features = modeler.get_feature_names()
@@ -238,12 +240,14 @@ def run_experiment(
     features_data_container = processed_container.copy()
 
     # Get selected feature info, and write out to file
-    df_feature_info = features_data_container.feature_info.copy()
+    df_feature_info = features_data_container["feature_info"].copy()
     df_selected_feature_info = df_feature_info[df_feature_info["feature"].isin(selected_features)]
-    selected_feature_dataset_dict = {
-        "name": "selected_feature_info",
-        "frame": df_selected_feature_info,
-    }
+    selected_feature_dataset_dict = DatasetDict(
+        {
+            "name": "selected_feature_info",
+            "frame": df_selected_feature_info,
+        }
+    )
 
     features_data_container.add_dataset(selected_feature_dataset_dict, update=True)
 
@@ -267,9 +271,13 @@ def run_experiment(
     )
 
     # Use only selected features for predictions
-    columns_for_prediction = ["spkitemid", "sc1"] + selected_features
-    train_for_prediction = processed_container.train_preprocessed_features[columns_for_prediction]
-    test_for_prediction = processed_container.test_preprocessed_features[columns_for_prediction]
+    columns_for_prediction = (
+        ["spkitemid", "sc1"] + selected_features if selected_features else ["spkitemid", "sc1"]
+    )
+    train_for_prediction = processed_container["train_preprocessed_features"][
+        columns_for_prediction
+    ]
+    test_for_prediction = processed_container["test_preprocessed_features"][columns_for_prediction]
 
     logged_str = "Generating training and test set predictions"
     logged_str += " (expected scores)." if configuration["predict_expected_scores"] else "."
@@ -279,13 +287,16 @@ def run_experiment(
     )
 
     # Save modeler instance
-    modeler.feature_info = processed_container.feature_info.copy()
+    modeler.feature_info = processed_container["feature_info"].copy()
     modeler.feature_info.set_index("feature", inplace=True)
-    (
-        modeler.trim_min,
-        modeler.trim_max,
-        modeler.trim_tolerance,
-    ) = configuration.get_trim_min_max_tolerance()
+
+    spec_trim_min, spec_trim_max, spec_trim_tolerance = configuration.get_trim_min_max_tolerance()
+    if spec_trim_min:
+        modeler.trim_min = spec_trim_min
+    if spec_trim_max:
+        modeler.trim_max = spec_trim_max
+    if spec_trim_tolerance:
+        modeler.trim_tolerance = spec_trim_tolerance
     pred_config_dict = pred_config.to_dict()
     for key, attr_name in [
         ("train_predictions_mean", "train_predictions_mean"),
diff --git a/rsmtool/utils/cross_validation.py b/rsmtool/utils/cross_validation.py
index 19b9de48..bba97828 100644
--- a/rsmtool/utils/cross_validation.py
+++ b/rsmtool/utils/cross_validation.py
@@ -5,6 +5,7 @@ Various utility functions used for cross-validation.
 
 :organization: ETS
 """
+
 import logging
 from pathlib import Path
 from shutil import copyfile
@@ -35,7 +36,7 @@ def create_xval_files(configuration, output_dir, logger=None):
 
     Returns
     -------
-    df_train : pandas DataFrame
+    df_train : pandas.DataFrame
         DataFrame containing the raw training data file.
     folds : int
         The number of folds for which the files were created.
@@ -61,7 +62,7 @@ def create_xval_files(configuration, output_dir, logger=None):
     located_filepaths = {}
     located_filepaths["train"] = DataReader.locate_files(
         configuration.get("train_file"), configuration.configdir
-    )
+    )[0]
     if not located_filepaths["train"]:
         raise FileNotFoundError(
             f"The training data file was not found: " f"{repr(configuration.get('train_file'))}"
@@ -76,7 +77,7 @@ def create_xval_files(configuration, output_dir, logger=None):
         if additional_file := configuration.get(additional_filename):
             located_filepaths[additional_filename] = DataReader.locate_files(
                 additional_file, configuration.configdir
-            )
+            )[0]
 
     # read the training file into a dataframe making sure that the specified
     # ID column is read as a string
@@ -223,7 +224,7 @@ def combine_fold_prediction_files(foldsdir, file_format="csv"):
 
     Returns
     -------
-    df_all_predictions : pandas DataFrame
+    df_all_predictions : pandas.DataFrame
         Data frame containing the combined predictions for all folds.
     """
     # initialize empty list to hold each prediction data frame
diff --git a/rsmtool/utils/metrics.py b/rsmtool/utils/metrics.py
index b0c740d8..47142b43 100644
--- a/rsmtool/utils/metrics.py
+++ b/rsmtool/utils/metrics.py
@@ -26,9 +26,9 @@ def compute_expected_scores_from_model(model, featureset, min_score, max_score):
         The SKLL learner object to use for computing the expected scores.
     featureset : skll.data.FeatureSet
         The SKLL featureset object for which predictions are to be made.
-    min_score : int
+    min_score : float
         Minimum score level to be used for computing expected scores.
-    max_score : int
+    max_score : float
         Maximum score level to be used for computing expected scores.
 
     Returns
@@ -439,7 +439,7 @@ def difference_of_standardized_means(
 
 
 def quadratic_weighted_kappa(y_true_observed, y_pred, ddof=0):  # noqa: D301
-    """
+    r"""
     Calculate quadratic-weighted kappa for both discrete and continuous values.
 
     The formula to compute quadratic-weighted kappa for continuous values
diff --git a/rsmtool/utils/prmse.py b/rsmtool/utils/prmse.py
index 5378f4b3..54f7c470 100644
--- a/rsmtool/utils/prmse.py
+++ b/rsmtool/utils/prmse.py
@@ -269,7 +269,7 @@ def get_true_score_evaluations(
 
     Parameters
     ----------
-    df: pandas DataFrame
+    df: pandas.DataFrame
         Input data frame. Must contain columns listed in
         ``system_score_columns`` and ``human_score_columns``.
     system_score_columns: str or list
@@ -290,7 +290,7 @@ def get_true_score_evaluations(
 
     Returns
     -------
-    prmse_metrics: pandas DataFrame
+    prmse_metrics: pandas.DataFrame
         DataFrame containing different evaluation metrics related to the evaluation
         of system scores against true scores. The column names are:
 
diff --git a/rsmtool/writer.py b/rsmtool/writer.py
index ffb5b711..f939d35b 100644
--- a/rsmtool/writer.py
+++ b/rsmtool/writer.py
@@ -7,6 +7,7 @@ Class for writing DataContainer frames to disk.
 
 :organization: ETS
 """
+
 from os import makedirs
 from os.path import join
 
@@ -43,7 +44,7 @@ class DataWriter:
 
         Parameters
         ----------
-        df : pandas DataFrame
+        df : pandas.DataFrame
             Data frame to write to disk
         name_prefix : str
             The complete prefix for the file to be written to disk.


Test patch:
diff --git a/tests/test_experiment_rsmtool_3.py b/tests/test_experiment_rsmtool_3.py
index 35636da2..b5c33335 100644
--- a/tests/test_experiment_rsmtool_3.py
+++ b/tests/test_experiment_rsmtool_3.py
@@ -241,7 +241,7 @@ class TestExperimentRsmtool3(unittest.TestCase):
         source = "lr-wrong-path"
         experiment_id = "lr"
         config_file = join(rsmtool_test_dir, "data", "experiments", source, f"{experiment_id}.json")
-        with self.assertRaises(FileNotFoundError):
+        with self.assertRaises(ValueError):
             do_run_experiment(source, experiment_id, config_file)
 
     def test_run_experiment_wrong_feature_file_path(self):
@@ -250,7 +250,7 @@ class TestExperimentRsmtool3(unittest.TestCase):
         source = "lr-wrong-path-features"
         experiment_id = "lr"
         config_file = join(rsmtool_test_dir, "data", "experiments", source, f"{experiment_id}.json")
-        with self.assertRaises(FileNotFoundError):
+        with self.assertRaises(ValueError):
             do_run_experiment(source, experiment_id, config_file)
 
     def test_run_experiment_lr_length_column_and_feature_list(self):
diff --git a/tests/test_preprocessor.py b/tests/test_preprocessor.py
index 53dc6f44..3c55979d 100644
--- a/tests/test_preprocessor.py
+++ b/tests/test_preprocessor.py
@@ -214,10 +214,8 @@ class TestFeaturePreprocessor(unittest.TestCase):
         (
             output_df_with_zeros,
             output_excluded_df_with_zeros,
-        ) = self.fpp.filter_on_column(bad_df, "sc1", "spkitemlab", exclude_zeros=False)
-        output_df, output_excluded_df = self.fpp.filter_on_column(
-            bad_df, "sc1", "spkitemlab", exclude_zeros=True
-        )
+        ) = self.fpp.filter_on_column(bad_df, "sc1", exclude_zeros=False)
+        output_df, output_excluded_df = self.fpp.filter_on_column(bad_df, "sc1", exclude_zeros=True)
         assert_frame_equal(output_df_with_zeros, df_filtered_with_zeros)
         assert_frame_equal(output_df, df_filtered)
 
@@ -227,9 +225,7 @@ class TestFeaturePreprocessor(unittest.TestCase):
         expected_df_excluded = bad_df.copy()
         expected_df_excluded.drop("sc1", axis=1, inplace=True)
 
-        df_filtered, df_excluded = self.fpp.filter_on_column(
-            bad_df, "sc1", "spkitemlab", exclude_zeros=True
-        )
+        df_filtered, df_excluded = self.fpp.filter_on_column(bad_df, "sc1", exclude_zeros=True)
 
         self.assertTrue(df_filtered.empty)
         self.assertTrue("sc1" not in df_filtered.columns)
@@ -246,7 +242,7 @@ class TestFeaturePreprocessor(unittest.TestCase):
         bad_df = pd.DataFrame(data=data)
 
         output_df, output_excluded_df = self.fpp.filter_on_column(
-            bad_df, "feature_zero_sd", "id", exclude_zeros=False, exclude_zero_sd=True
+            bad_df, "feature_zero_sd", exclude_zeros=False, exclude_zero_sd=True
         )
 
         good_df = bad_df[["id", "feature_ok"]].copy()
@@ -265,7 +261,7 @@ class TestFeaturePreprocessor(unittest.TestCase):
         good_df.reset_index(drop=True, inplace=True)
 
         output_df, output_excluded_df = self.fpp.filter_on_column(
-            data, "feature_with_inf", "id", exclude_zeros=False, exclude_zero_sd=True
+            data, "feature_with_inf", exclude_zeros=False, exclude_zero_sd=True
         )
 
         assert_frame_equal(output_df, good_df)
@@ -974,6 +970,16 @@ class TestFeaturePreprocessor(unittest.TestCase):
         actual = self.fpp.trim(values, 1, 8, 0.25)
         assert_array_equal(actual, expected)
 
+    def test_trim_min_max_not_specified(self):
+        values = [1.8, 10.5, 1.4]
+        with self.assertRaises(ValueError):
+            self.fpp.trim(values, None, None)
+
+    def test_trim_none_tolerance(self):
+        values = [1.8, 10.5, 1.4]
+        with self.assertRaises(ValueError):
+            self.fpp.trim(values, 1, 6, None)
+
     def test_preprocess_feature_fail(self):
         np.random.seed(10)
         values = np.random.random(size=1000)
diff --git a/tests/test_reader.py b/tests/test_reader.py
index de313f0e..df74249b 100644
--- a/tests/test_reader.py
+++ b/tests/test_reader.py
@@ -258,13 +258,7 @@ class TestDataReader(unittest.TestCase):
         config_dir = "output"
         result = DataReader.locate_files(paths, config_dir)
         assert isinstance(result, list)
-        self.assertEqual(result, [None, None])
-
-    def test_locate_files_str(self):
-        paths = "file1.csv"
-        config_dir = "output"
-        result = DataReader.locate_files(paths, config_dir)
-        self.assertEqual(result, None)
+        self.assertEqual(result, ["", ""])
 
     def test_locate_files_works(self):
         config_dir = "temp_output"
@@ -274,7 +268,7 @@ class TestDataReader(unittest.TestCase):
         full_path = os.path.abspath(os.path.join(config_dir, paths))
         open(full_path, "a").close()
 
-        result = DataReader.locate_files(paths, config_dir)
+        result = DataReader.locate_files(paths, config_dir)[0]
         rmtree(config_dir)
         self.assertEqual(result, full_path)
 
@@ -284,8 +278,8 @@ class TestDataReader(unittest.TestCase):
         with self.assertRaises(ValueError):
             DataReader.locate_files(paths, config_dir)
 
-    def test_setup_none_in_path(self):
-        paths = ["path1.csv", None, "path2.csv"]
+    def test_setup_empty_string_in_path(self):
+        paths = ["path1.csv", "", "path2.csv"]
         framenames = ["train", "test", "features"]
         with self.assertRaises(ValueError):
             DataReader(paths, framenames)