跳转至

v0.6.3

Patch release closing out defect #3 from the 2026-07-14 review — the equal_freq WOE-plot silent failure that 0.6.2 only half-fixed.

Why 0.6.2 wasn't the fix

0.6.2 addressed defect #3 by:

  1. making WOE_Master.plot_bivar_graph(..., group=None, ...) optional,
  2. explicitly passing group=None at the pipeline's ungrouped call site, and
  3. replacing _plot_woe's bare except Exception: return with a logger.warning(...).

That fix rested on an incorrect assumption: "the underlying WOE_Plot_Tool.get_bivar_graph primitive has always accepted group=None." The signature default was there, but the function body's if group: guard sat after the crash point:

# 0.6.2 and earlier — Modeling_Tool/WOE/WOE_Plot_Tool.py :: get_bivar_graph
# Reference WOE Table Plot — this loop runs FIRST, writes PNGs successfully
for var in varlist:
    plot_woe(..., fig_name=f"{var}.png")           # ①

# Group WOE Table Plot — runs UNCONDITIONALLY, even when group is None
grp_woe_res = get_mapped_woe_summary(..., grp_name=[group])   # ②  <- crash here
grp_woe_res = align_bin_num(..., grp_name=[group])            # ③
if group:                                                     # ④  <- guard too late
    for var in varlist:
        plot_woe_group(..., fig_name=f"{var}_{group}.png")

When group=None, ② invokes data.groupby([None]) internally, raising TypeError: 'NoneType' object is not callable. The if group: guard at ④ never gets a chance to skip that crash.

Net effect on 0.6.2 in the field

engine base PNGs by_<group>/*.png plot exception
monotone 6 3 none
equal_freq 3 (from ① before crash) 0 TypeError every run

Base PNGs still appeared for equal_freq because loop ① completed before loop ② crashed. The 0.6.2 logger.warning then caught the TypeError, and the pipeline's subsequent for group in cfg.woe_plot_groups: loop that would emit by_<group>/*.png never ran — so grouped plots were still missing.

The fix

Hoist if group: in WOE_Plot_Tool.get_bivar_graph so the guard wraps the whole summary + align + plot triplet — the summary/align never run for the ungrouped case, and there is no groupby([None]) to crash on:

# 0.6.3
if group:
    grp_woe_res = get_mapped_woe_summary(..., grp_name=[group])
    grp_woe_res = align_bin_num(..., grp_name=[group])
    for var in varlist:
        plot_woe_group(..., fig_name=f"{var}_{group}.png")

Also fixed the WOE_Master.plot_bivar_graph docstring that claimed get_bivar_graph "has always accepted group=None" — the source of the 0.6.2 wrong-assumption chain.

Verification

Coordinated regression coverage in SuperModelingFactory_pytest:

  • TestN46b — pipeline base plots emit no WOE plot generation failed warning (previous D5 only counted PNGs, which is why the 0.6.2 half-fix slipped through — see "Durable lesson" below).
  • TestN48equal_freq engine + woe_plot_groups=['chan'] produces figs/woe/<target>/by_chan/*.png > 0.
  • TestN49 — direct call get_bivar_graph(..., group=None) does not raise (white-box guard against reintroducing an unconditional groupby([None])).

All three fail on 0.6.2, pass on 0.6.3. Full suite on Py3.14 editable: 665 passed / 0 skipped / 0 failed.

Durable lesson

Two mistakes, one shared root cause: checking a signature without reading the function body.

  1. In 0.6.1 the diagnosis was "the underlying primitive already supports group=None" — based only on the default in the signature. It didn't.
  2. In 0.6.2 the fix acted on that same wrong assumption and stopped at the caller's ungrouped call.

Neither round would have shipped if we'd read get_bivar_graph's body to where the guard actually lived.

Second lesson, on test design: asserting "output exists" is not the same as asserting "no error occurred." Loop ① wrote 3 PNGs, so a "produced ≥ 1 figure" assertion passed even while every run also raised. Any test path where the failure would be caught by a surrounding try/except must assert both the presence of the desired artifact and the absence of warnings/exceptions from the operation.

The 0.6.2 logger.warning — the visibility half of the 0.6.2 fix — is what finally made the residual crash observable on the first 0.6.3 pipeline run. That was worth shipping on its own.