11 Commits

Author SHA1 Message Date
github-actions[bot]
ca3f37da78 chore: bump version to 2.0.5
Some checks failed
Quality gate / quality-gate (push) Has been cancelled
2026-10-26 14:11:44 +00:00
PedroHMCosme
f054bc9517 Merge pull request #35 from Aignosi/fix/SIENTIAPDE-2215
fix(metrics): ignore NaN target/prediction pairs in validation metrics (SIENTIAPDE-2215)
2026-10-26 11:11:30 -03:00
PedroHMCosme
52f9f2724b Merge remote-tracking branch 'origin/main' into fix/SIENTIAPDE-2215 2026-10-23 11:37:59 -03:00
PedroHMCosme
9fa1430d75 fix(metrics): name the wrapper mock correctly and document NaN r2 with one valid pair (SIENTIAPDE-2215) 2026-10-22 10:57:53 -03:00
PedroHMCosme
fcb9bb263b test(metrics): build the metadata mock outside pytest.raises (SIENTIAPDE-2215) 2026-10-22 10:34:00 -03:00
PedroHMCosme
2678c5d638 Merge remote-tracking branch 'origin/main' into fix/SIENTIAPDE-2215 2026-10-21 21:48:06 -03:00
PedroHMCosme
959a430314 test(metrics): compare metric floats with pytest.approx (SIENTIAPDE-2215) 2026-10-21 09:43:49 -03:00
PedroHMCosme
5043137f97 refactor(metrics): build the NaN-pairs warning before the call (SIENTIAPDE-2215) 2026-10-21 09:32:26 -03:00
PedroHMCosme
0920d8138e Merge branch 'main' into fix/SIENTIAPDE-2215 2026-10-21 09:14:20 -03:00
PedroHMCosme
8c81aa68bb refactor(metrics): warn with the remaining count and document NaN pairs (SIENTIAPDE-2215) 2026-10-21 09:11:40 -03:00
PedroHMCosme
9c05871fcf fix(metrics): ignore NaN target/prediction pairs in validation metrics (SIENTIAPDE-2215) 2026-10-20 22:17:57 -03:00
3 changed files with 65 additions and 2 deletions

View File

@@ -357,7 +357,8 @@ class DataManagerRepository(SientiaMonitoring):
This helper mirrors the previous TrainingRepository.after_train_calculation
behavior, assuming that predictions (y_pred/y_train_pred) are already on the
correct scale for metric calculation (any scaling is handled inside the
model wrapper).
model wrapper). Pairs with a NaN target or a NaN prediction are left out of
mse/mae/r2. With a single valid pair left, r2_val is NaN (R² needs 2 samples).
Args:
tmr: Training result containing:
@@ -368,6 +369,10 @@ class DataManagerRepository(SientiaMonitoring):
Return:
TrainModelResult: Same object with mse_val, mae_val and r2_val set.
Raises:
ValueError: If y_pred is not set, val_data and y_pred share no indices,
or no pair has both a valid target and a valid prediction.
"""
if tmr.y_pred is None:
raise ValueError('y_pred must be set before computing regression metrics')
@@ -405,6 +410,24 @@ 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():
n_valid = int(valid.sum())
message = (
f'Ignoring {len(valid) - n_valid} of {len(valid)} validation row(s) with NaN '
f'target or prediction; regression metrics use the remaining {n_valid}'
)
self.warning(message, 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)

View File

@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
[project]
name = "model-manager"
version = "2.0.4"
version = "2.0.5"
description = "Sientia DataOps Model Manager - ML Model Orchestration System"
readme = "README.md"
requires-python = ">=3.11"

View File

@@ -309,6 +309,46 @@ 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
with patch.object(repo, 'warning') as warn:
out = repo.compute_regression_metrics(tmr, wrapper)
assert out.mse_val == pytest.approx(0.0)
assert out.mae_val == pytest.approx(0.0)
assert out.r2_val == pytest.approx(1.0)
warn.assert_called_once()
assert 'Ignoring 2 of 4' in warn.call_args.args[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),
)
wrapper = MagicMock()
with pytest.raises(ValueError, match='No validation rows with both target and prediction'):
repo.compute_regression_metrics(tmr, wrapper)
def test_compute_regression_metrics_linear_equation():
repo = dmr.DataManagerRepository(MagicMock())
p = TrainModelParams.from_dict(_minimal_dict_for_prepare())