跳转至

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)

  1. N29 flips a default: missing_policy=\"drop\"\"include\" on _calculate_single_psi and 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.0 cross_vars precedent.
  2. N38 adds a new kwarg: unseen_category_policy on apply_woe — public API surface expansion. Default is \"warn\", which is numerically identical to 0.4.2 but emits a RuntimeWarning per affected feature. Callers who treat RuntimeWarning as an error must opt in to \"silent\".
  3. N29 required expanding 6 upstream PSI API surfaces (not just flipping the internal default). In 0.4.2 missing_policy existed 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):

  1. 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.
  2. missing_policy kwarg 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\"
  1. All internal call sites forward missing_policy through the chain so a single top-level kwarg reaches _calculate_single_psi at 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 a RuntimeWarning naming 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\", PSICalculator class-level forwarding, method-level override precedence, \"warn_and_drop\" RuntimeWarning emission, and a parametrized check over all 5 module-level function names.
  • TestN38UnseenCategoryPolicy — 8 tests: default-warn warning emission and output-preservation, raise policy halt semantics, silent policy 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-policy ValueError at 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).