You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
#64's gate: scikit-learn stays the running implementation until a comprehensive parity suite exists, passes against it, and has been seen failing on a deliberately broken numpy kernel. This story is that gate. It adds the numpy kernels as private functions next to _crps_ensemble_numpy (native_metric_calculators.py:46-77, the precedent) and the parity suite, and changes nothing that dispatches.
Measured on scikit-learn 1.7.2 (2026-09-17), the semantics to reproduce:
AP — _binary_clf_curve: order = argsort(y_score, kind="mergesort")[::-1]; y_true == 1; thresholds at np.where(np.diff(sorted_scores))[0] plus the last index (ties collapse to one operating point); tps = cumsum(float64)[idx], fps = 1 + idx - tps. precision_recall_curve: precision tps/(tps+fps) with zeros where the denominator is 0; recall tps/tps[-1], or all-ones with warnings.warn("No positive class found in y_true, recall is set to one for all thresholds.") (plain UserWarning) when tps[-1] == 0; reversed, with (1, 0) appended. AP = float(max(0.0, -np.sum(np.diff(recall) * precision[:-1]))). A literal transcription matched sklearn to 0.0 on 300 tie-heavy random cases. Accepted y_true: at most two distinct values and, if two, 1 among them ({1,2} computes with 1 as positive; {0,1,2} and continuous raise).
MTD — p<0: 2*(where(y>0,y,0)^(2-p)/((1-p)(2-p)) - y*mu^(1-p)/(1-p) + mu^(2-p)/(2-p)); p==0: (y-mu)^2; p==1: 2*(xlogy(y, y/mu) - y + mu) with xlogy = where(x==0, 0, x*log(y)) under errstate(divide="ignore", invalid="ignore"); p==2: 2*(log(mu/y) + y/mu - 1); else the generic form; plain mean; Python float. Domain: p<0 → ValueError("Mean Tweedie deviance error with power={power} can only be used on strictly positive y_pred."); 1<=p<2 → "…non-negative y and strictly positive y_pred."; p>=2 → "…strictly positive y and y_pred."; p==0 no check; 0<p<1 → ValueError. Matched to 0.0 over 11 powers × 200 cases on float64.
Maintainer decisions (2026-09-17): float64 always (sklearn computes float32 in when all inputs are float32, ~3e-7 off — documented deviation); clean ValueError for non-binary AP truth, naming the observed labels, while accepting exactly sklearn's input set.
The work
_average_precision_numpy(y_true_flat, y_score_flat) -> float and _tweedie_deviance_numpy(y_true_flat, y_pred_flat, power) -> float in native_metric_calculators.py, on the flattened form the kernels already use (np.repeat(y_true, S), y_pred.flatten()). Not called by anything yet. sklearn import untouched.
TestAPParityWithSklearn and TestMTDParityWithSklearn in tests/test_metric_calculators.py, modelled on TestCRPSParityWithProperscoring (function-local from sklearn.metrics import … oracle, pytest.approx(abs=1e-10), seeded RandomState). Content is the S5: Switch AP and MTD to the numpy kernels; scikit-learn to dev deps; import-purity guard; close C-05 (#64 gate, part 2) #64 gate list: AP — perfect, inverted, all-tied, all-positive, all-negative (value and warning message), single positive, N=1, N=2, duplicated scores with mixed labels, S in {1, 10, 100}, 200 seeded draws over N in {5, 50, 5000} × S × prevalence 1%–99%, {-1,1} and {1,2} label sets, and the raise cases ({0,1,2}, continuous) asserting ValueError on both sides. MTD — p in {-2, -1, -0.5, 0, 1, 1.0001, 1.5, 1.9999, 2, 2.0001, 2.5, 3, 5}; exact zeros in y where permitted; y == mu (deviance exactly 0); y ≪ mu and y ≫ mu; S in {1, 10, 100}; 200 seeded draws per branch; every domain rule as a raise test with the message text; float32 inputs at abs=1e-6 with the deviation named in the docstring.
Warning parity: each random case run under warnings.catch_warnings(record=True) on both paths, asserting the same warning categories — this is what catches a stray numpy RuntimeWarning from the Poisson branch.
Extreme values (1e6, 1e-6) for both.
This story does not change dispatch, pyproject.toml, docs, or the register. grep -rn sklearn views_evaluation/ still finds the import when it merges.
Acceptance criteria
Both numpy functions exist, private, uncalled by production code
Parity suite passes at abs=1e-10 (float64) on every listed case; float32 cases at abs=1e-6
Seen red, recorded in the PR: three injected defects each made the suite fail — drop [::-1] on the sort; skip the tie collapse (use every index as a threshold); flip a sign in the generic Tweedie branch. All reverted
Existing test_ap_oracle_sklearn and test_mtd_known_tweedie untouched and green
Zero behaviour change: full suite green with no test modified other than additions; ruff clean
Validation
conda run --name views_pipeline pytest tests/test_metric_calculators.py -q
conda run --name views_pipeline pytest tests/ -q
conda run --name views_pipeline ruff check views_evaluation/ tests/
git diff --stat -- views_evaluation/ # native_metric_calculators.py only, additions only
Epic: #66 · Wave 1 · Gates: S5 (#64) · Blocked by: none · Ships no behaviour change
Background
#64's gate: scikit-learn stays the running implementation until a comprehensive parity suite exists, passes against it, and has been seen failing on a deliberately broken numpy kernel. This story is that gate. It adds the numpy kernels as private functions next to
_crps_ensemble_numpy(native_metric_calculators.py:46-77, the precedent) and the parity suite, and changes nothing that dispatches.Measured on scikit-learn 1.7.2 (2026-09-17), the semantics to reproduce:
_binary_clf_curve:order = argsort(y_score, kind="mergesort")[::-1];y_true == 1; thresholds atnp.where(np.diff(sorted_scores))[0]plus the last index (ties collapse to one operating point);tps = cumsum(float64)[idx],fps = 1 + idx - tps.precision_recall_curve: precisiontps/(tps+fps)with zeros where the denominator is 0; recalltps/tps[-1], or all-ones withwarnings.warn("No positive class found in y_true, recall is set to one for all thresholds.")(plainUserWarning) whentps[-1] == 0; reversed, with(1, 0)appended. AP= float(max(0.0, -np.sum(np.diff(recall) * precision[:-1]))). A literal transcription matched sklearn to 0.0 on 300 tie-heavy random cases. Acceptedy_true: at most two distinct values and, if two,1among them ({1,2}computes with 1 as positive;{0,1,2}and continuous raise).2*(where(y>0,y,0)^(2-p)/((1-p)(2-p)) - y*mu^(1-p)/(1-p) + mu^(2-p)/(2-p)); p==0:(y-mu)^2; p==1:2*(xlogy(y, y/mu) - y + mu)withxlogy = where(x==0, 0, x*log(y))undererrstate(divide="ignore", invalid="ignore"); p==2:2*(log(mu/y) + y/mu - 1); else the generic form; plain mean; Pythonfloat. Domain: p<0 →ValueError("Mean Tweedie deviance error with power={power} can only be used on strictly positive y_pred."); 1<=p<2 →"…non-negative y and strictly positive y_pred."; p>=2 →"…strictly positive y and y_pred."; p==0 no check; 0<p<1 →ValueError. Matched to 0.0 over 11 powers × 200 cases on float64.Maintainer decisions (2026-09-17): float64 always (sklearn computes float32 in when all inputs are float32, ~3e-7 off — documented deviation); clean
ValueErrorfor non-binary AP truth, naming the observed labels, while accepting exactly sklearn's input set.The work
_average_precision_numpy(y_true_flat, y_score_flat) -> floatand_tweedie_deviance_numpy(y_true_flat, y_pred_flat, power) -> floatinnative_metric_calculators.py, on the flattened form the kernels already use (np.repeat(y_true, S),y_pred.flatten()). Not called by anything yet. sklearn import untouched.TestAPParityWithSklearnandTestMTDParityWithSklearnintests/test_metric_calculators.py, modelled onTestCRPSParityWithProperscoring(function-localfrom sklearn.metrics import …oracle,pytest.approx(abs=1e-10), seededRandomState). Content is the S5: Switch AP and MTD to the numpy kernels; scikit-learn to dev deps; import-purity guard; close C-05 (#64 gate, part 2) #64 gate list: AP — perfect, inverted, all-tied, all-positive, all-negative (value and warning message), single positive, N=1, N=2, duplicated scores with mixed labels, S in {1, 10, 100}, 200 seeded draws over N in {5, 50, 5000} × S × prevalence 1%–99%,{-1,1}and{1,2}label sets, and the raise cases ({0,1,2}, continuous) assertingValueErroron both sides. MTD — p in {-2, -1, -0.5, 0, 1, 1.0001, 1.5, 1.9999, 2, 2.0001, 2.5, 3, 5}; exact zeros in y where permitted; y == mu (deviance exactly 0); y ≪ mu and y ≫ mu; S in {1, 10, 100}; 200 seeded draws per branch; every domain rule as a raise test with the message text; float32 inputs atabs=1e-6with the deviation named in the docstring.warnings.catch_warnings(record=True)on both paths, asserting the same warning categories — this is what catches a stray numpyRuntimeWarningfrom the Poisson branch.This story does not change dispatch,
pyproject.toml, docs, or the register.grep -rn sklearn views_evaluation/still finds the import when it merges.Acceptance criteria
abs=1e-10(float64) on every listed case; float32 cases atabs=1e-6[::-1]on the sort; skip the tie collapse (use every index as a threshold); flip a sign in the generic Tweedie branch. All revertedtest_ap_oracle_sklearnandtest_mtd_known_tweedieuntouched and greenruffcleanValidation
conda run --name views_pipeline pytest tests/test_metric_calculators.py -q conda run --name views_pipeline pytest tests/ -q conda run --name views_pipeline ruff check views_evaluation/ tests/ git diff --stat -- views_evaluation/ # native_metric_calculators.py only, additions only