diff --git a/model_manager/utils/repository/data_manager_repository.py b/model_manager/utils/repository/data_manager_repository.py index 03b31ec..856976f 100644 --- a/model_manager/utils/repository/data_manager_repository.py +++ b/model_manager/utils/repository/data_manager_repository.py @@ -405,6 +405,23 @@ class DataManagerRepository(SientiaMonitoring): y_true_val = y_true_val.loc[common_index] y_pred_val = y_pred_val.loc[common_index] + # NaN target, or NaN prediction for rows with NaN features, carries no error signal + # (SIENTIAPDE-2215). + valid = y_true_val.notna() & y_pred_val.notna() + if not valid.any(): + raise ValueError( + 'No validation rows with both target and prediction: all ' + f'{len(valid)} overlapping rows have NaN' + ) + if not valid.all(): + self.info( + f'Ignoring {int((~valid).sum())} validation row(s) with NaN target or prediction ' + 'in the regression metrics', + metadata, + ) + y_true_val = y_true_val[valid] + y_pred_val = y_pred_val[valid] + # Metrics helpers already round to 2 decimals. tmr.mse_val = mse(y_true_val, y_pred_val) tmr.mae_val = mae(y_true_val, y_pred_val) diff --git a/tests/utils/repository/test_data_manager_repository.py b/tests/utils/repository/test_data_manager_repository.py index 09c52b3..c198efe 100644 --- a/tests/utils/repository/test_data_manager_repository.py +++ b/tests/utils/repository/test_data_manager_repository.py @@ -309,6 +309,41 @@ def test_compute_regression_metrics_no_overlap(): repo.compute_regression_metrics(tmr, MagicMock()) +def test_compute_regression_metrics_ignores_nan_pairs(): + """NaN target or NaN prediction rows are left out of mse/mae/r2 (SIENTIAPDE-2215).""" + repo = dmr.DataManagerRepository(MagicMock()) + p = TrainModelParams.from_dict(_minimal_dict_for_prepare()) + idx = pd.Index([0, 1, 2, 3]) + tmr = TrainModelResult( + params=p, + train_data=pd.DataFrame({'t': [1.0, 2.0, 3.0, 4.0]}, index=idx), + val_data=pd.DataFrame({'t': [1.0, np.nan, 3.0, 4.0]}, index=idx), + y_pred=pd.DataFrame({'p': [1.0, 2.0, np.nan, 4.0]}, index=idx), + ) + + wrapper = MagicMock() + wrapper.model = object() # no sklearn .regr -> equation extraction is skipped + out = repo.compute_regression_metrics(tmr, wrapper) + + assert out.mse_val == 0.0 + assert out.mae_val == 0.0 + + +def test_compute_regression_metrics_all_nan_pairs_raises(): + repo = dmr.DataManagerRepository(MagicMock()) + p = TrainModelParams.from_dict(_minimal_dict_for_prepare()) + idx = pd.Index([0, 1]) + tmr = TrainModelResult( + params=p, + train_data=pd.DataFrame({'t': [1.0, 2.0]}, index=idx), + val_data=pd.DataFrame({'t': [1.0, 2.0]}, index=idx), + y_pred=pd.DataFrame({'p': [np.nan, np.nan]}, index=idx), + ) + + with pytest.raises(ValueError, match='No validation rows with both target and prediction'): + repo.compute_regression_metrics(tmr, MagicMock()) + + def test_compute_regression_metrics_linear_equation(): repo = dmr.DataManagerRepository(MagicMock()) p = TrainModelParams.from_dict(_minimal_dict_for_prepare())