SMF 0.5.0 — Minor release · MEDIUM batch (N29 + N38)¶
Minor release. Two MEDIUM-severity fixes chosen for release-1 of the
0.5.x / 0.6.x rollout because they both change public API surface and
one flips a default. Together they turn two silent failure modes —
missing-rate drift on PSI and unseen categorical values on apply_woe —
into observable behaviour that a monitoring layer can act on.
Why 0.5.0 (minor), not 0.4.3 (patch)¶
- N29 flips a default:
missing_policy=\"drop\"→\"include\"on_calculate_single_psiand every upstream PSI surface. Silent PSI consumers with NaN in either dataset will see numerically different PSI values (direction: higher, because missing-rate drift now contributes). Follows the 0.3.19 → 0.4.0cross_varsprecedent. - N38 adds a new kwarg:
unseen_category_policyonapply_woe— public API surface expansion. Default is\"warn\", which is numerically identical to 0.4.2 but emits aRuntimeWarningper affected feature. Callers who treatRuntimeWarningas an error must opt in to\"silent\". - N29 required expanding 6 upstream PSI API surfaces (not just
flipping the internal default). In 0.4.2
missing_policyexisted only on the private_calculate_single_psi; none of the 6 upstream callers forwarded it. Flipping the default there was a no-op until the surfaces were expanded.
Summary¶
| Fix | Symbol | Severity | Impact |
|---|---|---|---|
| N29 | Feature.PSI_Tool.* (7 surfaces) |
MEDIUM | Default missing_policy flipped to \"include\"; kwarg exposed on PSICalculator.__init__, PSICalculator.calculate, calculate_psi, calculate_within_psi, calculate_psi_within_dataset, calculate_multivar_psi_two_sets, calculate_multigroup_psi_two_sets |
| N38 | WOE.WOE_Monotone_Binner.MonotoneWOEBinner.apply_woe |
MEDIUM | New unseen_category_policy kwarg (\"warn\" default) + _unseen_category_stats attribute for programmatic monitoring |
Fixes¶
N29 — Default missing_policy flip + full API surface expansion (Feature/PSI_Tool.py)¶
Before (0.4.2): missing_policy existed only on the private
_calculate_single_psi helper with default \"drop\". Every public
entry point (calculate_psi, calculate_within_psi,
calculate_psi_within_dataset, calculate_multivar_psi_two_sets,
calculate_multigroup_psi_two_sets, and the PSICalculator class)
routed to it at the default. There was no way for a caller to opt
in to missing-aware PSI without modifying SMF source.
After (0.5.0):
- Default flipped to
\"include\"on_calculate_single_psi. NaN rows on either side are now routed through a dedicated\"__MISSING__\"bin so missing-rate drift contributes to the PSI. This is the recommended production behaviour. missing_policykwarg added to all 6 upstream surfaces:
| Function | Position | Default |
|---|---|---|
PSICalculator.__init__ |
after precision |
\"include\" |
PSICalculator.calculate |
after return_details |
None → falls back to self.missing_policy |
calculate_psi |
after precision |
\"include\" |
calculate_within_psi |
after benchmark_display_name |
\"include\" |
calculate_psi_within_dataset |
after precision |
\"include\" |
calculate_multivar_psi_two_sets |
after precision |
\"include\" |
calculate_multigroup_psi_two_sets |
after return_details |
\"include\" |
- All internal call sites forward
missing_policythrough the chain so a single top-level kwarg reaches_calculate_single_psiat the bottom.
Three modes (unchanged from 0.4.2 semantics — only the default switches):
\"include\"(0.5.0 default) — NaN rows form a\"__MISSING__\"bin on both sides; denominators use the original (pre-dropna) row counts so fractions are directly comparable and missing-rate drift contributes.\"drop\"— 0.4.2 behaviour; silently excludes NaN rows before binning. Pass this to reproduce pre-0.5.0 numbers exactly.\"warn_and_drop\"— same numbers as\"drop\"plus aRuntimeWarningnaming the two NaN counts.
N38 — unseen_category_policy + _unseen_category_stats on apply_woe (WOE/WOE_Monotone_Binner.py)¶
Before (0.4.2): apply_woe(data, suffix=\"_woe\", inplace=False)
silently filled any transform-time categorical value not seen at fit
time with missing_woe (default 0.0, \"neutral risk\"). Concept drift
on categorical features was invisible to the caller — the pipeline saw
new categories as mid-bucket samples and model performance would
crater without warning.
After (0.5.0): New kwarg unseen_category_policy: str = \"warn\".
| Mode | Behaviour |
|---|---|
\"warn\" (default) |
Same output WOE column as 0.4.2 (unseen → missing_woe), plus a RuntimeWarning per affected feature naming (a) feature name, (b) unseen categories observed, (c) row count affected, (d) fraction of transform rows affected. Populates self._unseen_category_stats for programmatic monitoring. |
\"raise\" |
Raise ValueError on first unseen category with the same information. For strict production monitors that want the pipeline to halt. |
\"silent\" |
Old 0.4.2 behaviour — no warning, no stats. Only for callers deliberately opting out (open-world classification, or those running under -W error::RuntimeWarning who accept the drift-blindness). |
New attribute MonotoneWOEBinner._unseen_category_stats (populated on
each apply_woe call, reset at method entry):
{
feature_name: {
\"unseen_values\": {\"D\", \"E\"},
\"affected_rows\": 3,
\"affected_frac\": 0.6,
\"total_rows\": 5,
},
...
}
Numeric features (feat not in cate_feats) are untouched — the
unseen-detection block only runs inside the is_categorical branch.
Migration guide¶
If you were relying on 0.4.2 PSI defaults (and had NaN in either dataset)¶
Your PSI values will be numerically higher on 0.5.0 because missing-rate drift now contributes.
Option A — accept the new default (recommended). This is the correct behaviour for a drift monitor and matches every mainstream implementation (bank scorecard workflows, SAS Enterprise Miner, scikit-multiflow, etc.). Re-baseline any threshold you had against the new numbers.
Option B — pin the pre-0.5.0 behaviour at every call site:
# module-level functions
psi = calculate_psi(expected, actual, target_col=\"score\", missing_policy=\"drop\")
# class-level
calculator = PSICalculator(missing_policy=\"drop\")
result = calculator.calculate(expected_df, actual_df, varlist=[\"score\"])
If you use apply_woe on categorical features¶
Default \"warn\" produces the same output column as 0.4.2. Only
difference: a RuntimeWarning per feature that saw a new category.
Callers running with -W error::RuntimeWarning in strict mode will
need to either:
- Wrap the call:
with warnings.catch_warnings(): ...— recommended when you already have a monitoring path (log stats, alert on_unseen_category_stats) and simply want to prevent pytest / strict runners from tripping. - Pass
unseen_category_policy=\"silent\"— reproduces the full 0.4.2 behaviour with no warning and no stats population. Only appropriate when the drift-invisibility is a deliberate design choice (open-world classification).
For production monitors: prefer unseen_category_policy=\"warn\" (the
default) and add a post-transform check:
binner.apply_woe(df_transform)
if binner._unseen_category_stats:
for feat, stats in binner._unseen_category_stats.items():
if stats[\"affected_frac\"] > 0.05: # >5% of rows
alert_pipeline(feat, stats)
Regression coverage¶
test_pipeline_medium_0500.py in SuperModelingFactory_pytest (18 new
tests):
TestN29DefaultFlipAndApiExpansion— 10 tests: default flip verified against 0.4.2 golden numbers, backward-compat via explicit\"drop\",PSICalculatorclass-level forwarding, method-level override precedence,\"warn_and_drop\"RuntimeWarningemission, and a parametrized check over all 5 module-level function names.TestN38UnseenCategoryPolicy— 8 tests: default-warnwarning emission and output-preservation,raisepolicy halt semantics,silentpolicy no-warn / no-stats invariants, correct stats payload shape (unseen_values/affected_rows/affected_frac/total_rows), per-call stats reset, no-effect on numeric features, and invalid-policyValueErrorat method entry.
Baseline suite unchanged: 480 passed / 22 skipped on the sandbox
(deselecting ODPS-dependent tests which require internal package
access; those run green on the CI runners which have odps available
via the pytest repo's requirements).