SuperModelingFactory 0.5.6__tar.gz → 0.5.8__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 (113) hide show
  1. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +23 -2
  2. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Feature_Screen.py +11 -1
  3. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Weighted_Screen.py +22 -0
  4. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/feature_validation.py +4 -0
  5. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/field_meta.py +2 -0
  6. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/reject_inference.py +120 -0
  7. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/__init__.py +1 -1
  8. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/PKG-INFO +2 -2
  9. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/README.md +1 -1
  10. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  11. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/pyproject.toml +1 -1
  12. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/setup.py +1 -1
  13. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/ExcelMaster/ExcelFormatTool.py +0 -0
  14. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/ExcelMaster/ExcelMaster.py +0 -0
  15. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/ExcelMaster/Template.py +0 -0
  16. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/ExcelMaster/Utility.py +0 -0
  17. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/ExcelMaster/__init__.py +0 -0
  18. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/LICENSE +0 -0
  19. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/MANIFEST.in +0 -0
  20. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  21. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  22. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  23. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  24. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  25. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  26. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  27. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  28. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  29. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  30. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  31. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  32. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/__init__.py +0 -0
  33. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/kDataFrame.py +0 -0
  34. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  35. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  36. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Core/utils.py +0 -0
  37. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/Evaluation_Tool.py +0 -0
  38. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  39. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  40. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  41. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/__init__.py +0 -0
  42. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  43. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  44. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  45. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  46. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  47. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Explainability/__init__.py +0 -0
  48. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  49. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  50. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  51. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  52. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  53. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  54. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +0 -0
  55. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Feature/__init__.py +0 -0
  56. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  57. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  58. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  59. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/GBM_Tool.py +0 -0
  60. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  61. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/LRM_Tool.py +0 -0
  62. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  63. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Model/__init__.py +0 -0
  64. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/__init__.py +0 -0
  65. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/_common.py +0 -0
  66. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/credit_model.py +0 -0
  67. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  68. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/orchestrator.py +0 -0
  69. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/sample_analysis.py +0 -0
  70. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/score_comparison.py +0 -0
  71. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  72. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Pipeline/screening_artifact.py +0 -0
  73. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  74. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  75. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Reject_Infer.py +0 -0
  76. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Reject_Infer.pyi +0 -0
  77. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  78. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  79. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/Sample/__init__.py +0 -0
  80. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  81. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/UAT/__init__.py +0 -0
  82. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Adapter.py +0 -0
  83. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  84. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  85. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  86. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +0 -0
  87. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  88. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  89. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  90. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  91. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  92. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  93. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  94. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/__init__.py +0 -0
  95. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  96. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  97. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/_utils/__init__.py +0 -0
  98. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/_utils/nan_guard.py +0 -0
  99. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/_utils/robust.py +0 -0
  100. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/_utils/sentinels.py +0 -0
  101. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  102. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  103. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/ref_font/__init__.py +0 -0
  104. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  105. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Report/Report_Tool.py +0 -0
  106. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/Report/__init__.py +0 -0
  107. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  108. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  109. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  110. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/requires.txt +0 -0
  111. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  112. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/requirements.txt +0 -0
  113. {supermodelingfactory-0.5.6 → supermodelingfactory-0.5.8}/setup.cfg +0 -0
@@ -64,6 +64,26 @@ def _strip_trailing_semicolon(sql: str) -> str:
64
64
  return sql.strip().rstrip(";").strip()
65
65
 
66
66
 
67
+ def _first_error_line(exc: BaseException, limit: int = 200) -> str:
68
+ """Return ``ExcType: first non-empty message line`` for embedding in a
69
+ re-raised error message, truncated to ``limit`` chars.
70
+
71
+ ODPS errors carry the actionable ``ODPS-XXXXXXX: ...`` description on
72
+ their first line followed by long instance/traceback detail; only the
73
+ first line is worth surfacing inline.
74
+ """
75
+ for line in str(exc).splitlines():
76
+ line = line.strip()
77
+ if line:
78
+ break
79
+ else:
80
+ line = ""
81
+ text = f"{type(exc).__name__}: {line}" if line else type(exc).__name__
82
+ if len(text) > limit:
83
+ return text[:limit] + "..."
84
+ return text
85
+
86
+
67
87
  def _pull_one_chunk(
68
88
  chunk_spec: dict[str, Any],
69
89
  runner: ODPSRunner | None,
@@ -218,8 +238,9 @@ class ParallelODPSManager:
218
238
  self.odps_runner.run_sql(probe_sql, to_df=True, n_process=1)
219
239
  except Exception as exc:
220
240
  raise ValueError(
221
- f"unique_key validation failed for pull SQL. "
222
- f"Check that unique_key={self.config.unique_key!r} is visible in the SQL scope."
241
+ f"unique_key validation failed for pull SQL: {_first_error_line(exc)}. "
242
+ f"Common cause: unique_key={self.config.unique_key!r} not visible in "
243
+ f"the SQL scope; see the chained exception for the full ODPS error."
223
244
  ) from exc
224
245
 
225
246
  def _build_hash_pull_chunks(
@@ -413,7 +413,17 @@ def _weighted_woe_bins_screen(
413
413
  if var not in ins.columns or ins[var].nunique(dropna=False) <= 1:
414
414
  continue
415
415
  bins_ins = _bins_to_numpy(adapter.assign_bins(ins, var))
416
- x_ins = ins[var].to_numpy(dtype=float)
416
+ x_series = ins[var]
417
+ if pd.api.types.is_numeric_dtype(x_series):
418
+ x_ins = x_series.to_numpy(dtype=float)
419
+ else:
420
+ # Categorical/object feature: IV itself is computed from the
421
+ # already-assigned bins; ``x`` only feeds np.isfinite() inside
422
+ # _weighted_iv_from_assigned_bins to derive the weighted
423
+ # missing rate. A hard float cast crashes on string levels
424
+ # (e.g. '4.高中'), so encode observed -> 1.0 / missing -> NaN,
425
+ # matching the notna semantics of _missing_rate_for_series.
426
+ x_ins = np.where(x_series.notna().to_numpy(), 1.0, np.nan)
417
427
  iv_val, n_b, miss = _weighted_iv_from_assigned_bins(y_ins, w_ins, bins_ins, x_ins)
418
428
  iv_records.append({
419
429
  "var": var,
@@ -383,6 +383,28 @@ def _weighted_corr_for_screen(
383
383
  if cols:
384
384
  X = woe_ins[cols].to_numpy(dtype=float)
385
385
  return _weighted_pearson_corr_matrix(X, w_ins, nan_policy="pairwise")
386
+ non_numeric = [v for v in current if not pd.api.types.is_numeric_dtype(ins[v])]
387
+ if non_numeric:
388
+ # Raw-value Pearson correlation is undefined for categorical/object
389
+ # features. Exclude them from the correlation computation but keep
390
+ # them in the returned matrix as NaN rows/columns: NaN never exceeds
391
+ # the dedup threshold, so categorical features survive the corr stage
392
+ # untouched instead of crashing the float cast.
393
+ warnings.warn(
394
+ f"raw-value correlation skips {len(non_numeric)} non-numeric "
395
+ f"feature(s) {non_numeric[:5]}; they are kept through the corr "
396
+ f"stage. Set corr_use_woe_bins=True to correlate categorical "
397
+ f"features via their WOE encoding.",
398
+ UserWarning,
399
+ stacklevel=2,
400
+ )
401
+ corr_full = np.full((len(current), len(current)), np.nan)
402
+ numeric_idx = [i for i, v in enumerate(current) if v not in set(non_numeric)]
403
+ if numeric_idx:
404
+ X_num = ins[[current[i] for i in numeric_idx]].astype(float).to_numpy()
405
+ corr_num = _weighted_pearson_corr_matrix(X_num, w_ins, nan_policy=corr_nan_policy)
406
+ corr_full[np.ix_(numeric_idx, numeric_idx)] = corr_num
407
+ return corr_full
386
408
  X = ins[current].astype(float).to_numpy()
387
409
  return _weighted_pearson_corr_matrix(X, w_ins, nan_policy=corr_nan_policy)
388
410
 
@@ -1231,6 +1231,10 @@ class FeatureValidationPipeline:
1231
1231
  "missing_rate_ref",
1232
1232
  cfg.woe_params.get("missing_ref_value", -999999),
1233
1233
  ),
1234
+ # Without this, a selection run that has to fit its own screening
1235
+ # binner (no prefit WOE engine) would bin declared categorical
1236
+ # features as numeric and crash on string levels.
1237
+ "categorical_features": params.get("categorical_features", cfg.categorical_features),
1234
1238
  }
1235
1239
  return screen_config_from_mapping(
1236
1240
  mapping,
@@ -198,6 +198,7 @@ _FIELD_LABELS = {
198
198
  "train_ri_models": "训练 RI 后模型",
199
199
  "ri_model_type": "RI 后模型类型",
200
200
  "ri_model_params": "RI 后模型参数",
201
+ "lr_nan_handling": "lr 模型缺失值处理",
201
202
  "include_no_ri_benchmark": "加入无 RI benchmark",
202
203
  "ri_validation_frac": "RI 验证集比例",
203
204
  "write_ri_datasets": "写出 RI 增强样本",
@@ -352,6 +353,7 @@ _FIELD_OPTIONS = {
352
353
  "feature_selection_mode": ["run", "from_artifact", "skip"],
353
354
  "prescore_model_type": ["lgb", "xgb", "cat", "lr"],
354
355
  "ri_model_type": ["lgb", "xgb", "cat", "lr"],
356
+ "lr_nan_handling": ["fillna_median", "fillna_mean", "fillna_0", "raise"],
355
357
  "ri_methods": ["simple_augment", "hard_cutoff", "fuzzy_augment", "parceling"],
356
358
  "ri_score_direction": ["high_bad", "high_good"],
357
359
  "ri_approved_scope": ["reference_only", "output_subset"],
@@ -20,6 +20,48 @@ from ._common import (
20
20
  )
21
21
 
22
22
 
23
+ class _LRNanFillWrapper:
24
+ """Wrap a fitted LRMaster with train-derived NaN/Inf fill values.
25
+
26
+ sklearn's LogisticRegression rejects non-finite inputs while the RI
27
+ pipeline feeds models raw feature frames, so the exact fill values
28
+ learned from the training frame must be re-applied at every predict
29
+ call — otherwise training and scoring would silently run on different
30
+ imputations. Module-level class so saved models stay picklable.
31
+ """
32
+
33
+ def __init__(self, model: Any, fill_values: dict[str, float], nan_handling: str):
34
+ self._inner = model
35
+ self.fill_values_ = dict(fill_values)
36
+ self.nan_handling = str(nan_handling)
37
+
38
+ def _fill(self, data: pd.DataFrame) -> pd.DataFrame:
39
+ fill_cols = [c for c in self.fill_values_ if c in data.columns]
40
+ if not fill_cols:
41
+ return data
42
+ filled = data.copy()
43
+ block = filled[fill_cols].replace([np.inf, -np.inf], np.nan)
44
+ filled[fill_cols] = block.fillna(self.fill_values_)
45
+ return filled
46
+
47
+ def predict_proba(self, data: pd.DataFrame, varlist: list[str] | None = None, **kwargs: Any) -> Any:
48
+ return self._inner.predict_proba(self._fill(data), varlist=varlist, **kwargs)
49
+
50
+ def predict(self, data: pd.DataFrame, varlist: list[str] | None = None, **kwargs: Any) -> Any:
51
+ return self._inner.predict(self._fill(data), varlist=varlist, **kwargs)
52
+
53
+ def __getattr__(self, name: str) -> Any:
54
+ # Never delegate dunders: leaking the inner model's __getstate__ /
55
+ # __setstate__ into pickle would serialize the wrapper as a bare
56
+ # LRMaster and drop the fill values on reload.
57
+ if name.startswith("__") and name.endswith("__"):
58
+ raise AttributeError(name)
59
+ inner = self.__dict__.get("_inner")
60
+ if inner is None:
61
+ raise AttributeError(name)
62
+ return getattr(inner, name)
63
+
64
+
23
65
  @dataclass
24
66
  class RejectInferencePipelineConfig:
25
67
  output_dir: str = "output/reject_inference"
@@ -46,6 +88,12 @@ class RejectInferencePipelineConfig:
46
88
  train_ri_models: bool = True
47
89
  ri_model_type: str = "lgb"
48
90
  ri_model_params: dict[str, Any] = field(default_factory=dict)
91
+ # NaN/Inf handling for lr models (both prescore_model_type="lr" and
92
+ # ri_model_type="lr"). GBM backends accept NaN natively; sklearn
93
+ # LogisticRegression does not, so lr features are imputed with
94
+ # train-derived fill values that are re-applied at scoring time.
95
+ # "raise" forbids imputation and fails fast on non-finite features.
96
+ lr_nan_handling: Literal["fillna_median", "fillna_mean", "fillna_0", "raise"] = "fillna_median"
49
97
  include_no_ri_benchmark: bool = True
50
98
  ri_validation_frac: float = 0.2
51
99
  save_models: bool = False
@@ -239,6 +287,9 @@ class RejectInferencePipeline:
239
287
  if model_type == "lr":
240
288
  lr_params = dict(params)
241
289
  standardize = bool(lr_params.pop("standardize", False))
290
+ train_fit, val_fit, fill_values = self._prepare_lr_frames(
291
+ train_fit, val_fit, feature_cols,
292
+ )
242
293
  model = LRMaster(params=lr_params or None, standardize=standardize)
243
294
  model.fit(
244
295
  data=train_fit,
@@ -249,6 +300,8 @@ class RejectInferencePipeline:
249
300
  val_tgt_name=cfg.target_col,
250
301
  weight_col=weight_col,
251
302
  )
303
+ if fill_values is not None:
304
+ return _LRNanFillWrapper(model, fill_values, cfg.lr_nan_handling)
252
305
  return model
253
306
 
254
307
  model = GradientBoostingModel(model_type, params)
@@ -264,6 +317,69 @@ class RejectInferencePipeline:
264
317
  )
265
318
  return model
266
319
 
320
+ def _prepare_lr_frames(
321
+ self,
322
+ train_fit: pd.DataFrame,
323
+ val_fit: pd.DataFrame,
324
+ feature_cols: list[str],
325
+ ) -> tuple[pd.DataFrame, pd.DataFrame, dict[str, float] | None]:
326
+ """Apply cfg.lr_nan_handling to the lr feature frames.
327
+
328
+ GBM backends consume NaN natively; sklearn LogisticRegression raises
329
+ ``Input X contains NaN`` on raw pipeline features. Fill values are
330
+ computed on the training frame only and returned so the fitted model
331
+ re-applies the identical imputation at scoring time via
332
+ _LRNanFillWrapper. Callers pass throwaway copies, so frames are
333
+ modified in place.
334
+ """
335
+ cfg = self.config
336
+ allowed = {"fillna_median", "fillna_mean", "fillna_0", "raise"}
337
+ mode = str(cfg.lr_nan_handling)
338
+ if mode not in allowed:
339
+ raise ValueError(
340
+ f"lr_nan_handling must be one of {sorted(allowed)}; got {mode!r}"
341
+ )
342
+ train_block = train_fit[feature_cols].replace([np.inf, -np.inf], np.nan)
343
+ val_block = val_fit[feature_cols].replace([np.inf, -np.inf], np.nan)
344
+ n_bad_train = int(train_block.isna().sum().sum())
345
+ n_bad_val = int(val_block.isna().sum().sum())
346
+ if mode == "raise":
347
+ if n_bad_train or n_bad_val:
348
+ bad_cols = sorted(
349
+ set(train_block.columns[train_block.isna().any()])
350
+ | set(val_block.columns[val_block.isna().any()])
351
+ )
352
+ raise ValueError(
353
+ f"lr model features contain {n_bad_train + n_bad_val} "
354
+ f"non-finite value(s) across {len(bad_cols)} feature(s) "
355
+ f"(e.g. {bad_cols[:5]}) and lr_nan_handling='raise'. "
356
+ f"Clean the features first, or set lr_nan_handling to "
357
+ f"'fillna_median'/'fillna_mean'/'fillna_0'."
358
+ )
359
+ return train_fit, val_fit, None
360
+ if mode == "fillna_0":
361
+ fill_series = pd.Series(0.0, index=feature_cols)
362
+ elif mode == "fillna_mean":
363
+ fill_series = train_block.mean(numeric_only=True).reindex(feature_cols).fillna(0.0)
364
+ else:
365
+ fill_series = train_block.median(numeric_only=True).reindex(feature_cols).fillna(0.0)
366
+ fill_values = {str(col): float(fill_series[col]) for col in feature_cols}
367
+ if n_bad_train or n_bad_val:
368
+ n_cols = int((train_block.isna().any() | val_block.isna().any()).sum())
369
+ warnings.warn(
370
+ f"lr model: {n_bad_train + n_bad_val} non-finite feature "
371
+ f"value(s) across {n_cols}/{len(feature_cols)} feature(s) "
372
+ f"filled with train-derived {mode} values (sklearn LR cannot "
373
+ f"handle NaN). Fill values are stored on the model and "
374
+ f"re-applied at scoring time; set lr_nan_handling='raise' to "
375
+ f"forbid imputation.",
376
+ UserWarning,
377
+ stacklevel=2,
378
+ )
379
+ train_fit[feature_cols] = train_block.fillna(fill_values)
380
+ val_fit[feature_cols] = val_block.fillna(fill_values)
381
+ return train_fit, val_fit, fill_values
382
+
267
383
  def run(self, data: pd.DataFrame) -> RejectInferencePipelineResult:
268
384
  cfg = self.config
269
385
  feature_cols = self._resolve_feature_cols(data)
@@ -1074,6 +1190,10 @@ class RejectInferencePipeline:
1074
1190
  "model_type": model_type,
1075
1191
  "random_state": cfg.random_state,
1076
1192
  }
1193
+ fill_values = getattr(model, "fill_values_", None)
1194
+ if fill_values is not None:
1195
+ metadata["lr_nan_handling"] = getattr(model, "nan_handling", None)
1196
+ metadata["lr_fill_values"] = dict(fill_values)
1077
1197
  save_model(
1078
1198
  model,
1079
1199
  path,
@@ -1,7 +1,7 @@
1
1
  # encoding: utf-8
2
2
 
3
3
  __author__ = "Jingkai Sun"
4
- __version__ = "0.5.6"
4
+ __version__ = "0.5.8"
5
5
 
6
6
  from ._utils import SMF_MISSING_BIN
7
7
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.6
3
+ Version: 0.5.8
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
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.6
268
+ - **Version**: 0.5.8
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -208,7 +208,7 @@ em.close_workbook()
208
208
 
209
209
  ## 版本
210
210
 
211
- - **Version**: 0.5.6
211
+ - **Version**: 0.5.8
212
212
  - **Author**: Jingkai Sun
213
213
 
214
214
  ## 许可证
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: SuperModelingFactory
3
- Version: 0.5.6
3
+ Version: 0.5.8
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
@@ -265,7 +265,7 @@ em.close_workbook()
265
265
 
266
266
  ## 版本
267
267
 
268
- - **Version**: 0.5.6
268
+ - **Version**: 0.5.8
269
269
  - **Author**: Jingkai Sun
270
270
 
271
271
  ## 许可证
@@ -12,7 +12,7 @@ build-backend = "setuptools.build_meta"
12
12
 
13
13
  [project]
14
14
  name = "SuperModelingFactory"
15
- version = "0.5.6"
15
+ version = "0.5.8"
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.5.6"),
18
+ version=os.environ.get("SMF_VERSION", "0.5.8"),
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",