Fix multiclass / non-{0,1}-label crashes across the sklearn classification tests (ZD-704)#534
Conversation
…ry labels
WeakspotsDiagnosis scores precision/recall/F1 per feature slice using
scikit-learn's defaults (average="binary", pos_label=1). This raises when a
slice does not look like a standard {0, 1} binary problem:
- Multiclass targets -> "Target is multiclass but average='binary'".
- Binary targets encoded outside {0, 1} -> "pos_label=1 is not a valid label".
The customer-reported case (ZD-704) is a genuinely multiclass target where one
feature bin happened to contain only two classes (e.g. {0, 4}); the old
per-slice scoring treated that slice as binary and crashed with
"pos_label=1 is not a valid label. It should be one of [0, 4]".
Resolve the averaging mode once from the full label set (union of true and
predicted labels across every dataset) and apply it to every slice:
multiclass -> average="macro"; binary -> average="binary" with the largest
class value as pos_label. For the standard {0, 1} case pos_label stays 1, so
existing results are unchanged. accuracy and any custom metric without an
`average` parameter are left untouched.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
…e diagnosis metric helpers
OverfitDiagnosis and RobustnessDiagnosis have the same multiclass failures as
WeakspotsDiagnosis, plus an AUC-specific one:
- metric="f1"/"precision"/"recall" call scikit-learn with average="binary"
(pos_label=1), raising "Target is multiclass but average='binary'" on
multiclass targets and "pos_label=1 is not a valid label" on binary targets
encoded outside {0, 1};
- the default metric "auc" calls roc_auc_score, which raises "multi_class must
be in ('ovo', 'ovr')" for multiclass targets. A true multiclass ROC AUC is not
possible here because the library retains only a single probability column, so
fall back to the label-binarize convention already used by ClassifierPerformance
(multiclass_roc_auc_score).
Extract the shared logic into _diagnosis_metrics.py:
- resolve_averaging/full_labels: decide (average, pos_label) once from the full
label set (union of true and predicted labels) so every feature slice or
perturbed copy is scored consistently;
- bind_averaging/apply_averaging: bind averaging onto metrics that accept it and
pass accuracy/regression metrics through unchanged;
- multiclass_auc: robust per-subset label-binarize AUC (aligns columns on the
full label set, scores only classes actually present, returns 0.0 otherwise).
WeakspotsDiagnosis now imports the shared helpers instead of its own copies. The
standard {0, 1} binary and regression paths are unchanged; the only behavior
change is that multiclass runs where it previously crashed. Adds unit tests for
the shared helpers and end-to-end tests for OverfitDiagnosis/RobustnessDiagnosis
across binary {0,1}, binary {0,4}, multiclass (default auc and f1), and regression.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Two more sklearn tests fail on multiclass targets (found by sweeping every
classification test against a multiclass model):
- CalibrationCurve wraps sklearn's binary-only calibration_curve, which raised a
cryptic "y_true takes value in {...} and pos_label is not specified" error on
multiclass. Skip cleanly with SkipTestError instead, matching ROCCurve and
PrecisionRecallCurve.
- SHAPGlobalImportance's select_shap_values only handled the older SHAP list
layout (one 2D array per class). Newer SHAP returns a single
(samples, features, classes) array for multiclass, which fell through to the
regression branch and was passed whole to shap.summary_plot, raising
"IndexError: index N is out of bounds for axis 1". Handle the 3D array by
selecting along the trailing class axis; multiclass now works when
class_of_interest is given and raises a clear, actionable error when it is not.
Binary and regression paths are unchanged. Adds unit tests for select_shap_values
(list, 3D-array, 2D-array, and error cases) and CalibrationCurve (multiclass skip,
binary runs).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
PR SummaryThis pull request significantly enhances the model validation module by introducing a broad suite of unit tests and a new helper module for diagnosis metrics. The changes include:
These changes not only expand the test coverage to ensure robust functionality across a variety of scenarios but also improve the maintainability of the code by centralizing common diagnostic calculations and parameter bindings. Test Suggestions
|
hunner
left a comment
There was a problem hiding this comment.
Approving — this subsumes #529's follow-up work in #533 (which I'm closing in favor of this PR) and I verified the claims empirically rather than by read-through: a 15-scenario matrix (Weakspots / Overfit / Robustness × default auc / explicit f1 × labels {0,2,4} / {0,4} / {0,1}) run identically against main, #533, and this branch. main crashes on 9 of 15; this branch passes all 15, including the multiclass default-auc path that #533 left out of scope (and which would very likely have been this customer's next ticket). multiclass_auc does match ClassifierPerformance's LabelBinarizer convention as claimed, with sensible hardening on top (global-label fit for column alignment, scorable-class filtering; unseen perturbed labels binarize to all-zero rows rather than crashing). Local suite: 58 passed, 2 pre-existing skips (SHAP tests covered by the max-deps CI matrix, which is green).
Two findings worth fixing before merge, both demonstrated:
1. CalibrationCurve still crashes on binary targets encoded outside {0, 1} — the exact error this PR calls out as cryptic. The new guard only skips when len(np.unique(dataset.y)) > 2, so a two-class {0, 4} target sails past it and hits sklearn's ValueError: y_true takes value in {0, 4} and pos_label is not specified.... Reproduced on this branch with a {0, 4} logistic model. Since the PR's own convention (in resolve_averaging) is "largest label is the positive class", passing pos_label through to calibration_curve for the two-class case closes it consistently:
prob_true, prob_pred = calibration_curve(
dataset.y, dataset.y_prob(model), n_bins=n_bins,
pos_label=np.unique(dataset.y).max(),
)2. apply_averaging in WeakspotsDiagnosis rebinds user-supplied custom metrics, silently overriding their explicit choices. bind_averaging rebinds any callable whose signature exposes average — and inspect.signature of functools.partial(f1_score, average="weighted") still exposes average, so the most natural way for a user to customize averaging gets clobbered. Demonstrated on this branch:
p = functools.partial(f1_score, average="weighted")
bind_averaging(p, "macro", None)(y_true, y_pred)
# returns the macro score (0.8413), not the user's weighted score (0.8492)Since WeakspotsDiagnosis applies it to the whole prepared registry, this hits every custom metric, not just defaults. Suggest only applying averaging when the caller didn't pass metrics (i.e. rebind DEFAULT_METRICS only) — custom callables should own their kwargs.
Two non-blocking notes: the PR body's "every subset is scored consistently" slightly oversells — the strategy is resolved globally but macro without labels= still averages over each slice's present classes only (a {0,2} slice of a {0,2,4} problem: 0.80 here vs 0.53 if labels= were pinned). I actually think your behavior is the better choice for this test — a homogeneous, perfectly-predicted bin shouldn't be flagged as a weakspot — so just worth a precise sentence rather than a change. And the Shortcut checklist item: this is sc-17091.
🤖 Automated reply
Pull Request Description
What and why?
A customer (ZD-704) running a multiclass model hit
ValueErrors in the scikit-learn tests, one at a time. Rather than fix them reactively, I swept every classification sklearn test against a multiclass model and fixed all the genuine breakages.Root cause (shared): these tests score classification metrics / build plots using scikit-learn defaults that only hold for a standard
{0, 1}binary problem.Tests fixed
Per-region diagnosis tests —
WeakspotsDiagnosis,OverfitDiagnosis,RobustnessDiagnosis. They score precision/recall/F1/AUC on subsets of the data (feature bins, noise-perturbed copies):average="binary",pos_label=1→Target is multiclass but average='binary'(multiclass) andpos_label=1 is not a valid label(binary encoded outside{0,1}, e.g.{0,4}).roc_auc_score(defaultaucmetric) →multi_class must be in ('ovo', 'ovr')on multiclass. A true probability-based multiclass ROC AUC isn't possible here (the library retains only a single probability column), so multiclass AUC falls back to the label-binarize convention already used byClassifierPerformance.The customer's
WeakspotsDiagnosiscase was a genuinely multiclass target where one feature bin happened to hold only two classes (0and4) — incidental, not a deliberate encoding (customer confirmed: not binary, not intentionally0/4).CalibrationCurve— wraps sklearn's binary-onlycalibration_curve, which raised a crypticpos_label is not specifiedon multiclass. Now skips cleanly withSkipTestError, matchingROCCurve/PrecisionRecallCurve.SHAPGlobalImportance—select_shap_valuesonly handled the older SHAP list layout (one 2D array per class). Newer SHAP returns a single(samples, features, classes)array for multiclass, which fell through to the regression branch and was passed whole toshap.summary_plot→IndexError: index N is out of bounds for axis 1. Now selects along the trailing class axis; multiclass works withclass_of_interest, and raises a clear, actionable error without it.What changed
New shared module
_diagnosis_metrics.py(resolve_averaging/full_labels,bind_averaging/apply_averaging,multiclass_auc) used by all three diagnosis tests.WeakspotsDiagnosisrefactored onto it;OverfitDiagnosis/RobustnessDiagnosisrouteauc(multiclass) and label metrics through it. Averaging is resolved once from the full label set (union of true + predicted labels) so every subset is scored consistently; standard{0,1}stays onpos_label=1(unchanged). Plus the two standalone fixes above.Full multiclass sweep (21 classification tests)
ScoreProbabilityAlignmentneeds a scorecard "score" column (not a multiclass issue). Clustering/regression tests aren't applicable to a classifier.How to test
New/updated: shared-helper units (incl.
multiclass_aucandselect_shap_valuesedge cases) and end-to-end runs across binary{0,1}, binary{0,4}, multiclass (default and per-metric), and regression. Full sklearn unit dir: 66 passed.What needs special review?
aucin Overfit/Robustness is a label-binarize AUC (matchesClassifierPerformance), not a probability ROC AUC.pos_label = max(labels)for non-{0,1}binary targets.SHAPGlobalImportancenow requiresclass_of_interestfor multiclass (clear error if omitted) — consistent with the pre-existing list-branch contract, but confirm that's the desired UX vs. defaulting to a class.Dependencies, breaking changes, and deployment notes
No breaking changes;
{0,1}binary and regression results unchanged. Needs a new library release tagged to PyPI before the customer canpip installthe fix.Release notes
Fixed
ValueErrors when running scikit-learn tests on multiclass targets (or binary targets encoded outside{0, 1}):WeakspotsDiagnosis,OverfitDiagnosis, andRobustnessDiagnosisnow handle multiclass;CalibrationCurveskips cleanly for multiclass; andSHAPGlobalImportancesupports multiclass viaclass_of_interest. Previously these raised errors such asTarget is multiclass but average='binary',pos_label=1 is not a valid label,multi_class must be in ('ovo', 'ovr'), or a SHAPIndexError.Checklist
bug)🤖 Generated with Claude Code