SuperModelingFactory 0.3.10__tar.gz → 0.3.12__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Feature_Screen.py +137 -6
  2. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Weighted_Screen.py +31 -0
  3. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/credit_model.py +122 -3
  4. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/__init__.py +1 -1
  5. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/PKG-INFO +1 -1
  6. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/PKG-INFO +1 -1
  7. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/pyproject.toml +1 -1
  8. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/setup.py +1 -1
  9. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/ExcelMaster/ExcelFormatTool.py +0 -0
  10. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/ExcelMaster/ExcelMaster.py +0 -0
  11. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/ExcelMaster/Template.py +0 -0
  12. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/ExcelMaster/Utility.py +0 -0
  13. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/ExcelMaster/__init__.py +0 -0
  14. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/LICENSE +0 -0
  15. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/MANIFEST.in +0 -0
  16. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  17. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  18. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  19. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  20. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  21. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  22. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  23. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  24. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  25. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  26. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  27. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  28. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  29. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/__init__.py +0 -0
  30. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/kDataFrame.py +0 -0
  31. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  32. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  33. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Core/utils.py +0 -0
  34. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  35. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  36. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  37. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  38. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/__init__.py +0 -0
  39. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  40. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  41. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  42. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  43. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  44. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Explainability/__init__.py +0 -0
  45. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  46. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  47. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  48. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  49. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  50. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  51. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  52. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Feature/__init__.py +0 -0
  53. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  54. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  55. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  56. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  57. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  58. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  59. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  60. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Model/__init__.py +0 -0
  61. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/__init__.py +0 -0
  62. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/_common.py +0 -0
  63. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/feature_validation.py +0 -0
  64. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  65. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/reject_inference.py +0 -0
  66. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  67. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  68. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  69. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  70. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  71. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  72. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  73. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  74. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  75. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  76. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/Sample/__init__.py +0 -0
  77. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  78. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/UAT/__init__.py +0 -0
  79. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  80. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  81. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  82. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  83. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  84. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  85. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  86. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  87. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  88. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  89. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  90. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  91. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/__init__.py +0 -0
  92. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  93. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  94. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  95. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  96. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/ref_font/__init__.py +0 -0
  97. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  98. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/README.md +0 -0
  99. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Report/Report_Tool.py +0 -0
  100. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/Report/__init__.py +0 -0
  101. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  102. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  103. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  104. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/requires.txt +0 -0
  105. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  106. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/requirements.txt +0 -0
  107. {supermodelingfactory-0.3.10 → supermodelingfactory-0.3.12}/setup.cfg +0 -0
@@ -2,7 +2,6 @@
2
2
 
3
3
  from __future__ import annotations
4
4
 
5
- import warnings
6
5
  from dataclasses import dataclass, field
7
6
  from pathlib import Path
8
7
  from typing import Any, Mapping
@@ -10,11 +9,18 @@ from typing import Any, Mapping
10
9
  import numpy as np
11
10
  import pandas as pd
12
11
 
12
+ from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
13
+
13
14
  from .Weighted_Screen import (
14
15
  WeightedScreenResult,
16
+ _corr_dedup_weighted,
15
17
  _legacy_unweighted_screen,
18
+ _psi_from_distributions,
16
19
  _resolve_splits,
17
20
  _summary_row,
21
+ _weighted_bin_distribution,
22
+ _weighted_iv_from_assigned_bins,
23
+ _weighted_pearson_corr_matrix,
18
24
  _weighted_screen_impl,
19
25
  )
20
26
 
@@ -289,6 +295,128 @@ def _woe_bins_unweighted_screen(
289
295
  )
290
296
 
291
297
 
298
+ def _bins_to_numpy(bins: Any) -> np.ndarray:
299
+ if isinstance(bins, pd.Series):
300
+ return bins.to_numpy(dtype=object)
301
+ return np.asarray(bins, dtype=object)
302
+
303
+
304
+ def _weighted_woe_bins_screen(
305
+ splits: dict[str, pd.DataFrame],
306
+ feature_cols: list[str],
307
+ target_col: str,
308
+ weight_col: str,
309
+ config: FeatureScreenConfig,
310
+ *,
311
+ prefit_woe_engine: Any | None = None,
312
+ ) -> FeatureScreenResult:
313
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
314
+
315
+ ins, oos, oot = splits["ins"], splits["oos"], splits["oot"]
316
+ w_ins = resolve_sample_weight(data=ins, weight_col=weight_col, expected_len=len(ins))
317
+ y_ins = ins[target_col].astype(float).to_numpy()
318
+ current = list(feature_cols)
319
+ summary_rows = [_summary_row("initial", len(feature_cols), len(current), None, weight_col)]
320
+
321
+ binner = prefit_woe_engine
322
+ if binner is None:
323
+ binner = fit_screening_woe_engine(
324
+ ins,
325
+ feature_cols,
326
+ target_col,
327
+ woe_engine=config.woe_engine,
328
+ woe_fit_query=config.woe_fit_query,
329
+ woe_params=config.woe_params,
330
+ monotone_woe_params=config.monotone_woe_params,
331
+ categorical_features=config.categorical_features,
332
+ )
333
+ adapter = as_woe_engine(binner)
334
+
335
+ psi_records: list[dict] = []
336
+ if config.psi_enabled:
337
+ for var in feature_cols:
338
+ if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
339
+ continue
340
+ bins_ins = _bins_to_numpy(adapter.assign_bins(ins, var))
341
+ exp_dist = _weighted_bin_distribution(bins_ins, w_ins, config.content)
342
+ row: dict[str, Any] = {"var": var, "psi_ins_oos": np.nan, "psi_ins_oot": np.nan}
343
+ if "oos" in config.psi_compare_splits and len(oos) > 0:
344
+ w_oos = resolve_sample_weight(data=oos, weight_col=weight_col, expected_len=len(oos))
345
+ bins_oos = _bins_to_numpy(adapter.assign_bins(oos, var))
346
+ act = _weighted_bin_distribution(bins_oos, w_oos, config.content)
347
+ row["psi_ins_oos"] = _psi_from_distributions(exp_dist, act, config.content)
348
+ if "oot" in config.psi_compare_splits and len(oot) > 0:
349
+ w_oot = resolve_sample_weight(data=oot, weight_col=weight_col, expected_len=len(oot))
350
+ bins_oot = _bins_to_numpy(adapter.assign_bins(oot, var))
351
+ act = _weighted_bin_distribution(bins_oot, w_oot, config.content)
352
+ row["psi_ins_oot"] = _psi_from_distributions(exp_dist, act, config.content)
353
+ psi_records.append(row)
354
+
355
+ psi_table = pd.DataFrame(psi_records) if psi_records else pd.DataFrame(
356
+ columns=["var", "psi_ins_oos", "psi_ins_oot"],
357
+ )
358
+ if not psi_table.empty:
359
+ compare_cols = [c for c in ("psi_ins_oos", "psi_ins_oot") if c in psi_table.columns]
360
+ psi_table["psi_max"] = psi_table[compare_cols].max(axis=1, skipna=True)
361
+ keep = psi_table.loc[psi_table["psi_max"] < config.psi_threshold, "var"].tolist()
362
+ n_before = len(current)
363
+ current = [v for v in current if v in keep] or current
364
+ summary_rows.append(_summary_row("psi", n_before, len(current), config.psi_threshold, weight_col))
365
+ else:
366
+ psi_table["psi_max"] = pd.Series(dtype=float)
367
+ else:
368
+ psi_table = pd.DataFrame(columns=["var", "psi_ins_oos", "psi_ins_oot", "psi_max"])
369
+
370
+ iv_records: list[dict] = []
371
+ if config.iv_enabled:
372
+ for var in current:
373
+ if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
374
+ continue
375
+ bins_ins = _bins_to_numpy(adapter.assign_bins(ins, var))
376
+ x_ins = ins[var].to_numpy(dtype=float)
377
+ iv_val, n_b, miss = _weighted_iv_from_assigned_bins(y_ins, w_ins, bins_ins, x_ins)
378
+ iv_records.append({
379
+ "var": var,
380
+ "iv_weighted": iv_val,
381
+ "n_bins": n_b,
382
+ "missing_rate": miss,
383
+ })
384
+ iv_table = pd.DataFrame(iv_records) if iv_records else pd.DataFrame(
385
+ columns=["var", "iv_weighted", "n_bins", "missing_rate"],
386
+ )
387
+ if not iv_table.empty:
388
+ keep = iv_table.loc[iv_table["iv_weighted"] >= config.iv_threshold, "var"].tolist()
389
+ n_before = len(current)
390
+ current = [v for v in current if v in keep] or current
391
+ summary_rows.append(_summary_row("iv", n_before, len(current), config.iv_threshold, weight_col))
392
+ else:
393
+ iv_table = pd.DataFrame(columns=["var", "iv_weighted", "n_bins", "missing_rate"])
394
+
395
+ corr_dropped = pd.DataFrame(columns=["var_a", "var_b", "corr", "iv_a", "iv_b", "kept", "dropped"])
396
+ if config.corr_enabled and len(current) > 1:
397
+ X = ins[current].astype(float).to_numpy()
398
+ corr = _weighted_pearson_corr_matrix(X, w_ins)
399
+ iv_map = dict(zip(iv_table["var"], iv_table["iv_weighted"])) if not iv_table.empty else {}
400
+ n_before = len(current)
401
+ current, corr_dropped = _corr_dedup_weighted(
402
+ current,
403
+ corr,
404
+ iv_map,
405
+ config.corr_threshold,
406
+ config.corr_max_iterations,
407
+ )
408
+ summary_rows.append(_summary_row("corr", n_before, len(current), config.corr_threshold, weight_col))
409
+
410
+ summary_rows.append(_summary_row("final", len(feature_cols), len(current), None, weight_col))
411
+ return FeatureScreenResult(
412
+ selected_features=list(current),
413
+ iv_table=iv_table,
414
+ psi_table=psi_table,
415
+ corr_dropped=corr_dropped,
416
+ summary=pd.DataFrame(summary_rows),
417
+ )
418
+
419
+
292
420
  def feature_screen(
293
421
  splits: dict[str, pd.DataFrame],
294
422
  feature_cols: list[str],
@@ -303,11 +431,14 @@ def feature_screen(
303
431
  use_woe_bins = _needs_woe_bins(cfg)
304
432
 
305
433
  if weight_col is not None:
306
- if use_woe_bins:
307
- warnings.warn(
308
- "Weighted feature screening with psi/iv/corr_use_woe_bins is not fully "
309
- "supported yet; falling back to weighted equal-frequency bins.",
310
- stacklevel=2,
434
+ if use_woe_bins or prefit_woe_engine is not None:
435
+ return _weighted_woe_bins_screen(
436
+ splits,
437
+ feature_cols,
438
+ target_col,
439
+ weight_col,
440
+ cfg,
441
+ prefit_woe_engine=prefit_woe_engine,
311
442
  )
312
443
  return _weighted_screen_impl(
313
444
  splits,
@@ -119,6 +119,37 @@ def _psi_from_distributions(expected: pd.Series, actual: pd.Series, content: flo
119
119
  return float(((a - e) * np.log(a / e)).sum())
120
120
 
121
121
 
122
+ def _weighted_iv_from_assigned_bins(
123
+ y: np.ndarray,
124
+ w: np.ndarray,
125
+ bins: np.ndarray,
126
+ x: np.ndarray,
127
+ ) -> tuple[float, int, float]:
128
+ total_bad = float(np.sum(w * y))
129
+ total_good = float(np.sum(w * (1.0 - y)))
130
+ if total_bad <= 0 or total_good <= 0:
131
+ missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
132
+ return 0.0, 0, missing_rate
133
+
134
+ rows = []
135
+ for b in pd.unique(bins):
136
+ m = bins == b
137
+ bad_w = float(np.sum(w[m] * y[m]))
138
+ good_w = float(np.sum(w[m] * (1.0 - y[m])))
139
+ rows.append({
140
+ "bad_pct": bad_w / total_bad,
141
+ "good_pct": good_w / total_good,
142
+ })
143
+ if not rows:
144
+ missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
145
+ return 0.0, 0, missing_rate
146
+
147
+ stats = pd.DataFrame(rows)
148
+ iv = float(calc_iv(stats, "bad_pct", "good_pct").sum())
149
+ missing_rate = 1.0 - float(np.sum(w[np.isfinite(x)])) / float(np.sum(w) or 1.0)
150
+ return iv, len(rows), missing_rate
151
+
152
+
122
153
  def _weighted_iv_for_var(
123
154
  x: np.ndarray,
124
155
  y: np.ndarray,
@@ -97,6 +97,11 @@ class CreditModelPipelineConfig:
97
97
  perf_pct_bins: int = 10
98
98
  perf_min_bin_prop: float = 0.03
99
99
 
100
+ screening_artifact: Any | None = None
101
+ feature_validation_result: Any | None = None
102
+ feature_selection_mode: Literal["run", "from_artifact", "skip"] = "run"
103
+ reuse_screening_woe: bool = True
104
+
100
105
 
101
106
  @dataclass
102
107
  class CreditModelPipelineResult:
@@ -190,8 +195,12 @@ class CreditModelPipeline:
190
195
  make_dirs(self._model_output_dir(), output_dir / "artifacts")
191
196
 
192
197
  splits = self._split_data(data)
193
- fs_summary, final_features = self._feature_selection(splits, feature_cols)
194
- woe_artifacts = self._fit_woe(splits, final_features)
198
+ fs_summary, final_features, screening_artifact = self._resolve_feature_selection(
199
+ splits,
200
+ feature_cols,
201
+ )
202
+ prefit_woe = screening_artifact.woe_artifacts if screening_artifact and cfg.reuse_screening_woe else None
203
+ woe_artifacts = self._fit_woe(splits, final_features, prefit_woe_artifacts=prefit_woe)
195
204
  woe_features = woe_artifacts["woe_features"]
196
205
  woe_splits = woe_artifacts["splits"]
197
206
  woe_suffix = woe_artifacts.get("woe_suffix", cfg.woe_params.get("woe_suffix", "_woe"))
@@ -424,6 +433,48 @@ class CreditModelPipeline:
424
433
  oot = oos.copy()
425
434
  return {"ins": ins.copy(), "oos": oos.copy(), "oot": oot.copy()}
426
435
 
436
+ def _resolve_screening_artifact(self) -> Any | None:
437
+ cfg = self.config
438
+ if cfg.screening_artifact is not None:
439
+ return cfg.screening_artifact
440
+ if cfg.feature_validation_result is not None:
441
+ from .screening_artifact import FeatureScreeningArtifact
442
+
443
+ return FeatureScreeningArtifact.from_fvp_result(
444
+ cfg.feature_validation_result,
445
+ target_col=cfg.target_col,
446
+ )
447
+ return None
448
+
449
+ def _resolve_feature_selection(
450
+ self,
451
+ splits: dict[str, pd.DataFrame],
452
+ feature_cols: list[str],
453
+ ) -> tuple[dict[str, Any], list[str], Any | None]:
454
+ cfg = self.config
455
+ artifact = self._resolve_screening_artifact()
456
+ mode = cfg.feature_selection_mode
457
+ if artifact is not None:
458
+ mode = "from_artifact"
459
+ if mode == "from_artifact":
460
+ if artifact is None:
461
+ raise ValueError("feature_selection_mode='from_artifact' requires screening_artifact.")
462
+ artifact.validate_for_cm(target_col=cfg.target_col, weight_col=cfg.weight_col)
463
+ summary = dict(artifact.selection_summary or {})
464
+ summary["from_artifact"] = True
465
+ summary["artifact_source"] = artifact.source
466
+ selected = list(artifact.selected_features) or list(feature_cols)
467
+ return summary, selected, artifact
468
+ if mode == "skip":
469
+ summary = {
470
+ "skipped": True,
471
+ "initial_features": list(feature_cols),
472
+ "final_features": list(feature_cols),
473
+ }
474
+ return summary, list(feature_cols), None
475
+ summary, selected = self._feature_selection(splits, feature_cols)
476
+ return summary, selected, None
477
+
427
478
  def _feature_selection(
428
479
  self,
429
480
  splits: dict[str, pd.DataFrame],
@@ -509,12 +560,80 @@ class CreditModelPipeline:
509
560
  transformed[name] = transform_fn(df)
510
561
  return transformed
511
562
 
512
- def _fit_woe(self, splits: dict[str, pd.DataFrame], feature_cols: list[str]) -> dict[str, Any]:
563
+ def _reuse_screening_woe(
564
+ self,
565
+ splits: dict[str, pd.DataFrame],
566
+ feature_cols: list[str],
567
+ prefit_woe_artifacts: dict[str, Any],
568
+ ) -> dict[str, Any] | None:
569
+ from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
570
+
571
+ cfg = self.config
572
+ by_target = prefit_woe_artifacts.get("by_target", {}) if prefit_woe_artifacts else {}
573
+ item = by_target.get(cfg.target_col)
574
+ if not item:
575
+ return None
576
+
577
+ adapter = item.get("adapter")
578
+ if adapter is None and item.get("engine") is not None:
579
+ woe_suffix = cfg.woe_params.get("woe_suffix", "_woe")
580
+ adapter = as_woe_engine(item["engine"], woe_suffix=woe_suffix)
581
+ if adapter is None:
582
+ return None
583
+
584
+ fitted = set(item.get("features") or [])
585
+ usable = [col for col in feature_cols if col in fitted]
586
+ if not usable:
587
+ return None
588
+
589
+ woe_suffix = cfg.woe_params.get("woe_suffix", "_woe")
590
+ woe_features = [f"{col}{woe_suffix}" for col in usable]
591
+ woe_splits = {
592
+ name: adapter.transform(df, varlist=usable, suffix=woe_suffix)
593
+ for name, df in splits.items()
594
+ }
595
+ extra_eval = self._transform_extra_eval_datasets(
596
+ adapter.transform,
597
+ usable,
598
+ woe_suffix=woe_suffix,
599
+ )
600
+ if cfg.warm_start_enabled and cfg.warm_start_score_col:
601
+ for name, df in woe_splits.items():
602
+ if cfg.warm_start_score_col not in df.columns and cfg.warm_start_score_col in splits[name].columns:
603
+ df[cfg.warm_start_score_col] = splits[name][cfg.warm_start_score_col].to_numpy()
604
+ for name, df in extra_eval.items():
605
+ if cfg.warm_start_score_col not in df.columns and cfg.warm_start_score_col in cfg.extra_eval_datasets[name].columns:
606
+ df[cfg.warm_start_score_col] = cfg.extra_eval_datasets[name][cfg.warm_start_score_col].to_numpy()
607
+
608
+ return {
609
+ "engine": adapter,
610
+ "engine_name": adapter.get_engine_name() if hasattr(adapter, "get_engine_name") else cfg.woe_engine,
611
+ "features": list(usable),
612
+ "woe_features": woe_features,
613
+ "woe_suffix": woe_suffix,
614
+ "splits": woe_splits,
615
+ "extra_eval": extra_eval,
616
+ "woe_table": adapter.get_woe_table(varlist=usable),
617
+ "reused_from_screening": True,
618
+ }
619
+
620
+ def _fit_woe(
621
+ self,
622
+ splits: dict[str, pd.DataFrame],
623
+ feature_cols: list[str],
624
+ *,
625
+ prefit_woe_artifacts: dict[str, Any] | None = None,
626
+ ) -> dict[str, Any]:
513
627
  from Modeling_Tool import MonotoneWOEBinner, WOE_Master
514
628
  from Modeling_Tool.WOE.WOE_Adapter import as_woe_engine
515
629
  from Modeling_Tool.WOE.WOE_Master import get_overall_woe_table
516
630
 
517
631
  cfg = self.config
632
+ if prefit_woe_artifacts and cfg.reuse_screening_woe:
633
+ reused = self._reuse_screening_woe(splits, feature_cols, prefit_woe_artifacts)
634
+ if reused is not None:
635
+ return reused
636
+
518
637
  woe_suffix = cfg.woe_params.get("woe_suffix", "_woe")
519
638
  graph_dir = str(Path(cfg.output_dir) / "figs" / "woe")
520
639
  woe_features = [f"{col}{woe_suffix}" for col in feature_cols]
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.3.10"
4
+ __version__ = "0.3.12"
5
5
 
6
6
  # Core module - base data structures
7
7
  from .Core import (
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.10
3
+ Version: 0.3.12
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.3.10
3
+ Version: 0.3.12
4
4
  Summary: Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.
5
5
  Home-page: https://github.com/Kyle-J-Sun/SuperModelingFactory
6
6
  Author: Kyle Sun
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.3.10"
15
+ version = "0.3.12"
16
16
  description = "Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting."
17
17
  readme = "README.md"
18
18
  requires-python = ">=3.10"
@@ -15,7 +15,7 @@ def _read(path: str) -> str:
15
15
 
16
16
  setup(
17
17
  name="SuperModelingFactory",
18
- version=os.environ.get("SMF_VERSION", "0.3.10"),
18
+ version=os.environ.get("SMF_VERSION", "0.3.12"),
19
19
  description="Credit risk modeling factory: WOE binning, scorecards, LightGBM, Excel reporting.",
20
20
  long_description=_read("README.md"),
21
21
  long_description_content_type="text/markdown",