Skip to content

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), of refine_dtree, and of refine_chi2 with n_jobs=1 (scorecard practice: every bin needs both goods and bads). The parallel refine_chi2 skips 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 of refine_cate in fit.
  • 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 with n_init_bins=20) and only merges them, refine_dtree uses min_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 enforce min_bin_size itself. Pass min_bin_size=0 to restrict the merging to class-pure bins.
  • When min_n_bins stops the merging while a bin still has no bad or no good, the bin is kept and a UserWarning names it (also when fit runs with n_jobs > 1).
  • small_bin_policy="warn" keeps the bins with a warning; small_bin_policy=None gives the 0.9.0 bins. The pipelines (CreditModelPipeline, FeatureValidationPipeline, feature_screen) build the binner from monotone_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=None and min_good_count=None and 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 with load_woe_bins keep 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. fit emits a UserWarning that names the feature, the special value, n, bad, good and the WOE; the SV table flags the bin in a new sv_class_pure column and binner._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 through FeatureValidationPipeline and feature_screen (monotone engine by default) and through CreditModelPipeline with woe_engine="monotone", also when fit runs with n_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 under sv_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 default missing_bin_strategy=None and NaN not listed, missing values score missing_woe (0) and have no bin.
  • min_bad_count=None, min_good_count=None switches 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_data that carries its own score_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_frac of the labelled approved rows instead of all of them, so prescore_model, the pre-score column, the inferred labels, ri_model_perf and best_method change. Runs with oot_data or with OOT rows from split_col, and runs with train_prescore=False and 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_perf and group_perf (and model_perf_compare with the default sync_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's N, AUC, KS and lift change. With full coverage nothing changes except the new N_OWN column. sync_data_size=False now 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 of get_gains_table, GainsTableCalculator and Model_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 gains with weight_col, GainsTableCalculator and get_gains_table with weight_col, and the weighted PerformanceEvaluator summary's LIFT and IV) 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-frequency woe_engine when it is not "monotone", get_woe_table, WOETransformer) with include_missing=True on 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+308 as the lower end of the first range, as the tables built with missing_ref_value=-999999 always did; the WOE values and the scores do not change.
  • fit(chi2_config=...) with the default missing_ref_value no 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 with weight_col, and get_gains_table, GainsTableCalculator and get_gains_summary with weights and add_func or withSummary=True) gain the custom columns and the Grand Summary row; the columns that were there do not change.
  • group_perf tables of single columns gain the group values that have exactly min_data_size rows (or the spec's min_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).