v0.9.1¶
The first published release of the 0.9 line. Version 0.9.0 (commit e1ef627) was prepared but never tagged or uploaded to
PyPI, where the release before 0.9.1 is 0.8.2. Upgrading from 0.8.2 therefore brings in everything in the
v0.9.0 notes, including its three changed defaults (repeated in section 8), as well as the changes below. The
upgrade guide lists every setting that reproduces earlier numbers.
Behavior-changing fixes (exception to RELEASING.md)
A patch release normally changes no numbers on healthy inputs. 0.9.1 ships the following changes anyway, because each one corrects results that were wrong or misleading; this is a documented exception to the release policy. For each change the table gives the settings that reproduce the 0.9.0 (and 0.8.2) numbers, or says plainly that it is a bug fix without a legacy switch.
| Change | What changes | Reproduce 0.9.0 / 0.8.2 with |
|---|---|---|
Class-pure WOE bins are merged by default: MonotoneWOEBinner defaults to min_bad_count=1, min_good_count=1, small_bin_policy="merge" (section 1) |
Bins, WOE, IV, selections and models of every feature with a bin that holds no bad or no good, or less than min_bin_size (3%) of the rows, fitted with MonotoneWOEBinner, the pipelines' monotone woe_engine or feature_screen; with iv_upper_threshold set, the leakage gate of the weighted screening on WOE bins, now tested on equal-frequency bins |
small_bin_policy=None (in monotone_woe_params for the pipelines). The leakage gate has no switch: it now uses the bins of the unweighted screening on WOE bins |
sv_small_policy="neutral" or "merge_missing" (opt-in) also acts on special-value bins with fewer bads than min_bad_count or fewer goods than min_good_count (class-pure SV bins with the defaults) (section 1) |
The WOE and IV of class-pure SV bins, only for configurations that set one of these two policies; the default "keep" only warns |
min_bad_count=None, min_good_count=None (this also switches off the merging of class-pure ordinary bins) |
Equal-frequency binning gives missing values their own bin, and the default fill value -1.797e308 no longer overflows (sections 4 and 5) |
WOE fits with include_missing=True (WOE_Master, get_woe_table, WOETransformer, the non-monotone woe_engine), unweighted Gains and cross-risk tables with include_missing=True, of features or scores with missing values |
Bug fix, no legacy switch: the old bins put the missing rows into the lowest bin together with real values (include_missing=False leaves them out instead, which is not the old result) |
RejectInferencePipeline: rows without a label were trained as goods; ri_score_direction="high_good" with a pre-score trained by the pipeline inverted the reject labels; the random OOT rows trained the pre-score, so their labels leaked into the RI models through the inferred reject labels; an external reference kept its own score scale (sections 2 and 3) |
ri_summary, ri_model_perf, best_method, the inferred labels and the pre-score |
Bug fixes, no legacy switch. "high_good" with a trained pre-score now raises ValueError |
ScoreComparisonPipeline and model_perf_compare compare every score on the same rows (section 4) |
global_perf and group_perf whenever the scores do not cover the same rows |
perf_common_rows=False (sync_data_size=False in model_perf_compare) evaluates each score on its own rows: with one comparison score this gives the 0.9.0 numbers; with several, the base score gets its 0.9.0 numbers back, while the 0.9.0 numbers of the comparison scores (measured on the rows where every comparison score was valid) have no switch |
ScoreComparisonPipeline: groups selected by value, weighted Gains labels and missing scores, min_data_size as a minimum (sections 4 and 6) |
Group tables of numeric group columns (empty before), weighted Gains tables with missing targets or scores, single-column groups with exactly min_data_size rows |
Bug fixes, no legacy switch |
Further changes alter what you get but not existing numbers: a UserWarning (and an sv_class_pure flag) for
class-pure special-value bins under the default sv_small_policy="keep" (section 1), the weighted Gains table now adds
the add_func columns and the Grand Summary row it used to drop (section 6), ODPSRunner needs ODPS_PROJECT and
ODPS_ENDPOINT (section 7), and the type stubs show the current signatures and defaults (section 9).
1. Class-Pure WOE Bins Are Merged by Default¶
A WOE bin that holds only goods (or only bads) has a WOE that comes from eps alone,
ln((bad share + 1e-6) / (good share + 1e-6)): about -9.9 for a bin with 2% of the goods and -12.4 for one with 20%.
Such a bin inflates the IV (in the regression test, a feature whose lowest fifth holds no bad has an IV of 3.09; with the
pure bin merged it is 0.57) and can dominate a logistic regression. The formula is the same in 0.8.2; the parameters that
prevent it existed but were off by default.
Parameter of MonotoneWOEBinner |
0.9.1 default | Up to 0.9.0 |
|---|---|---|
min_bad_count |
1 |
None |
min_good_count |
1 |
None |
small_bin_policy |
"merge" |
None (no check) |
- A bin without bads or without goods is merged into its WOE-closest neighbor at the end of
fit(after the optional chi-square step), ofrefine_dtree, and ofrefine_chi2withn_jobs=1(scorecard practice: every bin needs both goods and bads). The parallelrefine_chi2skips the check; chi-square merging only joins adjacent bins, so it cannot create a pure or a smaller bin. Categorical features merge with the bad-rate clustering ofrefine_cateinfit. - The policy also enforces
min_bin_size(default 3% of the rows). In the default flow this effectively only adds the merging of class-pure bins: the greedy fit starts from equal-frequency bins (about 5% each withn_init_bins=20) and only merges them,refine_dtreeusesmin_samples_leaf=0.05, chi-square merging only makes bins larger, and merging adjacent bins keeps a monotone feature monotone (the merged bad rate lies between the two neighbors). The one exception: tied values at the equal-frequency edges can leave a bin under 3% (for example a value held by 2% of the rows between two percentiles), and that bin is merged too; the greedy fit does not enforcemin_bin_sizeitself. Passmin_bin_size=0to restrict the merging to class-pure bins. - When
min_n_binsstops the merging while a bin still has no bad or no good, the bin is kept and aUserWarningnames it (also whenfitruns withn_jobs > 1). small_bin_policy="warn"keeps the bins with a warning;small_bin_policy=Nonegives the 0.9.0 bins. The pipelines (CreditModelPipeline,FeatureValidationPipeline,feature_screen) build the binner frommonotone_woe_params, so they follow the new default unless you pass the key there.- A binner pickled with 0.8.2 or 0.9.0 stores
small_bin_policy=None,min_bad_count=Noneandmin_good_count=Noneand keeps them when it is loaded or refitted (verified with binners pickled by both versions: the refit gives the same tables as the old code), and bins loaded withload_woe_binskeep their WOE. Special-value bins are not merged; see below.
Merging can also fold a near-perfect separator into an ordinary bin: on the hard-leak fixture of the tests, the class-pure
bins of the good side merge into the bad side and the WOE-bin IV falls from 11.5 to 0.6. The leakage gate
(iv_upper_threshold) of the weighted screening on WOE bins used the IV of the WOE engine's bins and would no longer have
caught such a leak. It now tests numeric features on weighted equal-frequency bins, as the unweighted screening on WOE
bins already did, so the gate does not depend on the merging (with weights of 1.0 both paths give the same floored IV;
with small_bin_policy=None the gate value of this path changes from the floored IV of the WOE bins to that of the
equal-frequency bins).
Regression tests: test_woe_pure_bin_defaults.py and test_feature_screen_gates.py. Tests that pin the old pure-bin
numbers pass small_bin_policy=None explicitly.
Class-pure special-value bins are reported, not merged¶
The merge covers ordinary bins only. A special-value (SV) bin keeps its empirical WOE (sv_min_bin_size=0.0,
sv_small_policy="keep", sv_woe_smoothing="none", sv_smoothing_alpha=0.0 by default, also in the pipelines'
monotone_woe_params), so an SV bin with only bads or only goods still gets a WOE from eps (about +/-9.9 or more).
Smoothing is not the fix: sv_woe_smoothing="laplace" shrinks the SV bin's bad rate toward the global bad rate with
sv_smoothing_alpha as a pseudo-count on the whole bin, so its effect fades as the bin grows (with 10,000 bads and 90,000
goods, an all-bad SV bin has a WOE of 7.6, 9.9 and 12.2 for 20, 200 and 2,000 rows without smoothing, and 5.8, 8.1 and
10.4 with alpha=0.5), and a large pure SV bin is often a genuine signal.
Since 0.9.1 sv_small_policy also treats an SV bin with fewer bads than min_bad_count or fewer goods than
min_good_count (a class-pure bin with the defaults of 1) as a violation:
- With the default
"keep", the bin keeps its WOE.fitemits aUserWarningthat names the feature, the special value,n,bad,goodand the WOE; the SV table flags the bin in a newsv_class_purecolumn andbinner._sv_pure_stats[feature]lists it. The warning changes no number: bins, WOE, IV and scores are the same as in 0.9.0. The warning reaches the caller throughFeatureValidationPipelineandfeature_screen(monotone engine by default) and throughCreditModelPipelinewithwoe_engine="monotone", also whenfitruns withn_jobs > 1. "neutral"and"merge_missing"stay opt-in. Configurations that already use them now also neutralize or merge class-pure SV bins, not only those undersv_min_bin_size: their numbers change for features with such bins.- Missing values form an SV bin only when NaN is listed in
special_values, and then the same rule applies. With the defaultmissing_bin_strategy=Noneand NaN not listed, missing values scoremissing_woe(0) and have no bin. min_bad_count=None, min_good_count=Noneswitches the check off (binners pickled with 0.9.0 or earlier have it off and keep their SV tables unchanged, without the new column).
CreditModelPipeline uses woe_engine="equal_freq" (WOE_Master) by default, which has no min_bad_count /
min_good_count and no such check. Regression tests: test_sv_pure_bin_warning.py.
2. RejectInferencePipeline: Rows Without a Label Are No Longer Trained as Goods¶
An end-to-end audit of RejectInferencePipeline ran it on synthetic approved and rejected applications whose true label is known for every row. Two defects made the models learn labels that do not exist. Each has a regression test in test_reject_inference_audit.py that fails without the fix.
| Defect | Fix |
|---|---|
Approved rows whose target is missing (not yet performed) were skipped by the pre-score but kept by the RI models and the no_ri_benchmark model, and the fit turned the missing target into 0: every such row was trained as a good. The random OOT and the validation sample could draw them too, and ri_summary['prescore_AUC'] came out NaN without a warning. In the audit, 20% of the approved rows were unlabelled with a true bad rate of 10.5%, and 664 of them went into every model as goods |
Model training, the validation sample and the random OOT use only rows with an observed target. The rows stay in ri_datasets; ri_summary counts them in N_approved_unlabelled, ri_model_perf counts the rows left out of each model in train_unlabelled_n, and a UserWarning names them. prescore_AUC is computed on the approved rows with an observed target |
hard_cutoff gives a rejected row with a missing score a missing label on purpose (since 0.4.2, so that it is not invented as a good), and the same conversion in the fit turned it back into a good. In the audit, 300 such rejects with a true bad rate of 42% were trained as goods |
These rejects stay in ri_datasets['hard_cutoff'] with a missing label and are left out of the model; ri_summary['N_rejected_unlabelled'] and ri_model_perf['train_unlabelled_n'] count them |
With train_ri_models=True, a run in which no approved row has an observed target (possible with an external ri_approved_data) now raises ValueError instead of training every model on approved rows labelled good.
Results that change
Only runs with missing targets change. When every approved row has a target and every reject gets a label, ri_summary, ri_model_perf, the OOT and the validation sample are identical to 0.9.0 (checked with the default random OOT, with split_col and with an external oot_data); the only difference is the new columns N_approved_unlabelled, N_rejected_unlabelled and train_unlabelled_n, which are 0.
3. RejectInferencePipeline: One Score Scale, and an OOT the Pre-score Never Saw¶
The same audit found three ways in which the pre-score disagreed with the rest of the pipeline. Each has regression tests in test_reject_inference_audit.py that fail without the fix.
| Defect | Fix |
|---|---|
The pre-score the Pipeline trains is the probability of bad, but ri_score_direction="high_good" was accepted with it and read it as a good score: hard_cutoff and fuzzy_augment labelled the riskiest rejects good (hard-cutoff reject bad rate 0.23 instead of 0.99). The only hint was a prescore_AUC of 0.22 |
"high_good" with a pre-score trained by the Pipeline (train_prescore=True, or score_col absent) raises ValueError, and validate_pipeline_config reports it. Your own high-good score with train_prescore=False works as before |
An external ri_approved_data that carried its own score_col kept it when the Pipeline trained a new pre-score, so the rules were fitted on one score scale and applied to the rejects' other one (reject bad rate 0.09 for hard_cutoff and 0.21 for parceling, for a true 0.41) |
When the Pipeline trains the pre-score, the reference is scored with it; an existing score_col is replaced with a UserWarning. With train_prescore=False the reference keeps its own scores |
With neither oot_data nor OOT rows from split_col, the random OOT was drawn from approved rows the pre-score had been trained on. Their labels reached the RI models through the inferred reject labels, which inflated the RI models' OOT AUC against no_ri_benchmark and skewed best_method: on pure-noise features the hard_cutoff model scored an OOT AUC of 0.54 (0.51 with the OOT held out) |
The random OOT is drawn before the pre-score is trained and is left out of its training data. It keeps the same rows as before. With ri_approved_scope="output_subset" it is drawn from all labelled approved rows and only the drawn rows inside the subset are used (ValueError if none falls inside) |
Results that change
- A configuration with
ri_score_direction="high_good"and a pre-score trained by the Pipeline now raises. Its results were inverted; use"high_bad". - A run with an external
ri_approved_datathat carries its ownscore_col, while the Pipeline trains the pre-score, now infers the reject labels from the new pre-score. - With the default random OOT, the pre-score is trained on about
1 - oot_fracof the labelled approved rows instead of all of them, soprescore_model, the pre-score column, the inferred labels,ri_model_perfandbest_methodchange. Runs withoot_dataor with OOT rows fromsplit_col, and runs withtrain_prescore=Falseand your own scores, are not affected by this change (checked: identical results).
4. ScoreComparisonPipeline: Scores Compared on the Same Rows, Groups by Value, Weighted Gains Fixed¶
An end-to-end audit of ScoreComparisonPipeline ran it on synthetic scores whose true performance is known. Seven defects distorted the comparison or lost data without a word. Each has a regression test in test_score_comparison_audit.py that fails without the fix. Items 1, 2, 5, 6 and 7 are fixed in the shared evaluation code, so Model_Evaluation_Tool.model_perf_compare and the Gains tables behave the same way outside the pipeline.
| Defect | Fix |
|---|---|
model_perf_compare evaluated each comparison score on the rows where every comparison score was above 0: one comparison score that was always 0 removed every other comparison score from global_perf and group_perf, and a score covering 10% of the rows cut the others to 800 of 8,000 rows |
A score without any valid row is left out of the comparison with a UserWarning instead of emptying the others |
The base score kept all its rows while the comparison scores used the shared ones, so global_perf compared AUCs measured on different populations. With a comparison score covering 60% of the rows both AUCs read about 0.71, while on the same rows the base score has 0.637 and the new score 0.713 |
Every score, the base score included, is evaluated on the rows where all the scores are valid (sync_data_size=True, the default). The new column N_OWN gives each score's own number of valid rows. The pipeline's new perf_common_rows=False evaluates each score on its own rows instead |
multi_group_wrapper selected each group with the query col == 'value', so a numeric group column (the default apply_month holding 202401) gave an empty group_perf table |
Groups are selected by value; missing values form no group |
A group value containing a quote (kid's app) stopped the run with SyntaxError |
Fixed by the same change |
The weighted Gains table counted rows with a missing target as goods: the top bin's bad rate read 0.374 for a true 0.417, and the AUC came out NaN |
Rows with a missing target count in N only; PERF_CNT, N_BAD, N_GOOD, AVG_BAD, LIFT and AUC use the labelled rows, as in the unweighted table |
The weighted Gains table ranked rows with a missing score into the last bins whatever include_missing said (the top bin held only missing scores) |
Missing scores never enter the bins: they are left out, or reported in a Missing row with include_missing=True |
With include_missing=True and equal-frequency bins, the unweighted Gains table (get_gains_table, GainsTableCalculator, the pipeline's gains) and cross_risk filled missing scores with fillna and counted them in the quantiles: in the test the 800 missing rows shared the lowest bin with 800 real scores and moved every edge, unless they made up more than one bin's share. Equal-width bins already gave them a bin of their own |
Missing scores (and scores already holding fillna) form their own bin (-inf, fillna]; the other bins are the equal-frequency bins of the real scores, the same as with include_missing=False. Grouped tables (grp_name) decide this once over all groups, so every group keeps the same bin numbers and labels. Complete scores keep their bins |
The group tables collect the notices about scores without valid rows into one UserWarning that names the affected group dimensions.
Results that change
global_perfandgroup_perf(andmodel_perf_comparewith the defaultsync_data_size=True) evaluate the base score on the common rows: whenever a comparison score is missing, zero or negative where the base score is valid, the base score'sN,AUC,KSand lift change. With full coverage nothing changes except the newN_OWNcolumn.sync_data_size=Falsenow means "each score on its own valid rows" for every score (comparison scores no longer need a valid base score).- Group tables of numeric group columns are filled, and their rows come in order of appearance instead of an arbitrary order.
- Unweighted Gains and cross-risk tables built with
include_missing=True(the default ofget_gains_table,GainsTableCalculatorandModel_Evaluation_Tool) change when a score has missing values: the missing rows get bin 0 and the equal-frequency edges of the real scores move. WOE binning follows the same rule since the change in section 5. - The weighted Gains table (pipeline
gainswithweight_col,GainsTableCalculatorandget_gains_tablewithweight_col, and the weightedPerformanceEvaluatorsummary'sLIFTandIV) changes only when the data has a missing target or a missing score.
5. Missing Values Get Their Own Bin on Every Binning Path¶
The equal-frequency branch of quick_binning, the binning behind WOE_Master, get_woe_table, WOETransformer, the Binning class and the evaluation tables, filled missing values with the fill value and counted them in its quantiles, so with include_missing=True the missing rows shared the lowest bin with real values. The other branches already gave them a bin of their own, but only for a fill value such as -999999: WOE_Master's default missing_ref_value is -1.797e308, which Python's round and pandas' interval labels overflow to -inf, so the missing bin merged with the lowest bin on every path, and chi-square binning raised ValueError: Bin edges must be unique. Regression tests: test_woe_missing_bin.py.
| Defect | Fix |
|---|---|
| Equal-frequency bins counted the filled missing values in the quantiles: in the test the 800 missing rows shared the lowest bin with 800 real values, and every edge moved | The quantiles use the real values only and the fill value becomes a bin edge, as in the equal-width branch, when some rows are missing (or hold the fill value). Complete data keeps its bins |
The default missing_ref_value of WOE_Master (-1.797e308) was rounded to -inf, so the missing bin vanished on the equal-width, tree and chi-square paths too, and chi-square binning raised |
An edge that the rounding or pandas' interval labels would overflow into the same infinity as another edge is kept as it is (bins are numbered instead of labelled by pandas in that case); chi-square binning uses the exact cut points when pandas' labels merge two edges, and its special-value step uses the next representable value near the float limits. Edges at the float limits that merge with nothing are labelled ±inf as before |
transform scores the missing rows with the WOE of their bin on every path; previously they took the WOE of the lowest bin.
Results that change
- A WOE fit (
WOE_Master, also the pipelines' equal-frequencywoe_enginewhen it is not"monotone",get_woe_table,WOETransformer) withinclude_missing=Trueon a feature with missing values gets a missing bin, so its bins, WOE values, IV and the transformed feature change. Engines fitted earlier keep scoring with their stored bins until they are refit.MonotoneWOEBinner, the default engine of the pipelines, is not affected. - With the default
missing_ref_value, equal-width and tree WOE tables of complete data now number their bins from 1 and show-1.7976931348623157e+308as the lower end of the first range, as the tables built withmissing_ref_value=-999999always did; the WOE values and the scores do not change. fit(chi2_config=...)with the defaultmissing_ref_valueno longer raises.
6. ScoreComparisonPipeline: Weighted Gains Keep Their Columns, min_data_size Is a Minimum, Safe File Names¶
The remaining four defects of the audit in section 4. Each has a regression test in test_score_comparison_audit.py that fails without the fix.
| Defect | Fix |
|---|---|
With weight_col, the gains table silently lost the gains_add_func / custom_metric_cols columns and each score's Grand Summary row: the weighted Gains table ignored add_func and withSummary (also in get_gains_table, GainsTableCalculator.calculate and Model_Evaluation_Tool.get_gains_summary) |
The weighted table applies add_func to the rows of every row of the table (bins, special:<value> rows and the Missing row) and adds the Grand Summary row with withSummary=True; its counts and rates cover every row of the table. The pipeline's default <col>_mean columns are weighted means when weight_col is set. The other columns are unchanged |
Single-column groups were kept only with more than min_data_size (or a spec's min_size) rows, crossed groups with at least that many: a group value with exactly 50 rows was missing from group_perf['channel'] but present in the crossed table |
A group value with exactly min_data_size rows is evaluated in both. Model_Evaluation_Tool.multi_group_wrapper itself keeps its documented "more than min_subset_size" rule |
cross_binning_numeric=True or False, allowed by the annotation and shown in the docs, stopped the run with TypeError: 'bool' object is not subscriptable |
cross_risk takes a single bool for both columns (and None for [True, True]); a list, tuple or array of another length than two raises ValueError instead of IndexError |
A group_specs name containing / wrote report/step2_by_chan/month.csv into a subfolder, and a cross metric name with / did the same for step4_ files |
Characters that a file name cannot hold (/, \, :, *, ?, quotes, angle brackets, the vertical bar and control characters) become _ in the CSV file names, and a name that would repeat another one (ignoring case) gets a _2 suffix. The keys of group_perf and cross_results are unchanged |
Results that change
- Weighted
gains(pipeline withweight_col, andget_gains_table,GainsTableCalculatorandget_gains_summarywith weights andadd_funcorwithSummary=True) gain the custom columns and theGrand Summaryrow; the columns that were there do not change. group_perftables of single columns gain the group values that have exactlymin_data_sizerows (or the spec'smin_size).- CSV files of names with unsafe characters, or of names that differ only in case, are written under new names.
7. ODPSRunner Has No Built-in Project or Endpoint¶
Up to 0.9.0, ODPSRunner() fell back to a MaxCompute project and a VPC endpoint of the author's own environment when
ODPS_PROJECT or ODPS_ENDPOINT was not set, and the documentation repeated them. Both defaults are removed from the
package and the documentation: ODPS_PROJECT and ODPS_ENDPOINT are now required like the AccessKey variables, and a
missing or empty one raises KeyError that names it. The examples use placeholders (<your-project>,
https://service.<region>.maxcompute.aliyun.com/api). Regression tests: test_odps_upload_semantics.py.
8. Defaults Changed in 0.9.0, First Published Here¶
0.9.0 was never published, so 0.9.1 is the first release on PyPI with its three changed defaults. They stay as they are. Saved scorecards, binners and models keep the numbers they were built with until they are refitted; to refit with the pre-0.9 behavior, pass the old value:
| Setting | Default since 0.9.0 | Reproduce 0.8.2 with | What a refit with the new default changes |
|---|---|---|---|
MonotoneWOEBinner(sv_total_basis=...), also in monotone_woe_params |
"all" |
"ordinary" |
The WOE of the ordinary bins (one constant per feature) and the IV of every feature with special or missing values |
CreditModelPipelineConfig(warm_start_score_scope=...) |
"full" |
"train" |
Early stopping, the Optuna table and the Owen explanation of warm-started LightGBM and XGBoost models use the combined score |
MonotoneWOEBinner(unseen_special_policy=...), also in monotone_woe_params |
"neutral" |
"normal_bin" |
A declared special value without fit rows scores missing_woe instead of the WOE of the bin its number falls in |
A pickled MonotoneWOEBinner keeps the settings it was created with: a binner from 0.8.2 or earlier keeps
unseen_special_policy="normal_bin", sv_total_basis="ordinary" and small_bin_policy=None when it is loaded, applied or
refitted. See the v0.9.0 notes for the details.
9. Type Stubs Match the Source¶
The .pyi stubs shipped for Modeling_Tool/{Core,Eval,Feature,Model,Sample,WOE} had not been regenerated since
September: the stub of MonotoneWOEBinner showed min_bad_count=None, small_bin_policy=None and
unseen_special_policy='normal_bin' and had no sv_total_basis, none of the 0.9.0 defaults were reflected, and
Backward_Tool.pyi described an API that no longer exists. All 24 stubs are regenerated from the source by
scripts/generate_stubs.py in the main repository (the licence header of each stub is kept; @staticmethod,
@classmethod and @property are now included). test_stub_signatures.py compares every public function, class and
method of each stub with its source (parameter names, order, kinds and default values) and fails on any difference.
warm_start_score_scope lives in Pipeline/credit_model.py, which ships no stub.
Known Issue: Screening IV on WOE Bins Counts Class-Pure Special-Value Bins¶
Fixed in 0.9.2
Since 0.9.2 the screening IV leaves class-pure bins out of the sum by default
(screen_iv_convention="guarded"); screen_iv_convention="raw" keeps the behavior described below. The screening IV
of the test frame is 4.0247 (4.02 at two decimals; it was misprinted as 4.03 below), 0.4437 under the new default.
The by-group IV of MonotoneWOEBinner leaves out unsmoothed bins that hold one class (the iv_guard convention), but
the screening IV on WOE bins counts every bin with ln((bad share + 1e-6) / (good share + 1e-6)): the unweighted path in
WOE_Engine_Feature_Patch and the weighted path in feature_screen (since 0.9.0, so that both agree). A class-pure SV
bin therefore inflates the screening IV and the IV thresholds of both pipelines: in the test frame, a feature whose
sentinel (10% of the rows) holds only bads has a screening IV of 4.03, of which 3.58 comes from that bin, against 0.44
under the iv_guard convention. The 0.9.1 warning names such bins; a change of the screening IV is left for a later
release (done in 0.9.2).