跳转至

v0.6.9

Patch release from the requalification of a CDC field report ("0.6.8 CMP categorical issue"): the alleged 0.6.4→0.6.8 categorical→LR regression was disproven — the chain is behavior-identical across versions — but the investigation surfaced a family of silent categorical-transform failure modes plus one latent crash, all fixed here. Mapped WOE values are byte-identical on every input; the only behavior change is new warnings, a new stats attribute, and a config shape that previously raised TypeError now working.

G18 Categorical Transform Guards (MonotoneWOEBinner.apply_woe)

  • Field case: an upstream data-prep regression destroyed a categorical column (float32, 99.995% NaN, 2 surviving all-bad rows — the report's "N=2, WOE=5.04" phantom bin). SMF transformed it in silence: 99.99% of rows quietly took the [Missing]-bin WOE and the N38 unseen warning read "2/24716 rows (0.0%) affected" — technically true, diagnostically useless. The equivalence check the field team ran (WOE-table diff) is structurally blind to this: under artifact reuse the exported table is fit-frozen, so table-identical ≠ matrix-identical.
  • New stats: _categorical_transform_stats[feat] per apply_woe call — total_rows, missing_rows, missing_rate, fit_missing_rate (recovered from the fitted woe_table + sv_table), exact_match_rows, fallback_match_rows, unmatched_rows. Reset per call, like _unseen_category_stats. Equivalence regressions can assert on it directly.
  • Missing-rate drift guard: transform missing rate ≥ 50% while ≥ 30pp above fit time → RuntimeWarning naming both rates ("upstream column likely broken, renamed or re-typed; feature near-constant after transform"). This is the exact field shape.
  • str()-fallback tripwire: any row matched only through the str(value) fallback → RuntimeWarning. The fallback bridges fit/transform dtype drift (int codes vs zero-padded strings) but can silently mis-map when renderings collide — and int/str mixed columns mis-map with no unseen warning at all, because every rendering is "seen". Bridges get their own alarm.
  • Both guards respect unseen_category_policy="silent"; stats are always recorded. Warning class is RuntimeWarning, consistent with the N38 unseen guard in the same method.

CMP Monotone Self-Fit: init/fit Param Split

  • Defect: CreditModelPipeline._fit_woe's self-fit path passed the whole monotone_woe_params dict into MonotoneWOEBinner.__init__, so fit()-only keys — n_jobs, chi2_p, chi2_init_size — raised TypeError. (The screening-side fit_screening_woe_engine and FVP's _fit_monotone_binner already split init vs fit keys.) Field configs carrying n_jobs only survived by always taking the artifact-reuse path.
  • Fix: the self-fit path now routes chi2_binning / chi2_p / chi2_init_size / n_jobs to fit(). Configs without these keys are byte-identical; configs with them go from crashing to working (and chi2_p / chi2_init_size / n_jobs become effective on this path for the first time).

Behavior Notes

  • No numeric output changes anywhere: warn-vs-silent transform outputs are regression-asserted array-equal on healthy and broken inputs; the CMP self-fit split is regression-asserted woe_table-identical against a directly fitted reference binner.
  • Practical guidance for A/B reruns (from the field case): health-check input columns against the frozen baseline (dtype, missing rate, unique values) and diff the transform output (or read _categorical_transform_stats), not just the WOE table.

Validation

  • Full local pytest suite: 840 passed, 0 skipped (9 new regressions: healthy-path silence, stats reset, field-shape drift warning with both rates, drift suppression when fit matches, silent-policy stats retention, dtype-flip fallback warning + value identity, warn/silent byte parity, CMP self-fit fit-only keys, CMP self-fit reference parity).
  • CI green across the legacy/modern/bleeding × Python 3.11/3.12 matrix, source package checks, and version-consistency check.
  • Source package verification and wheel/sdist build passed (tasks.py verify).