SuperModelingFactory 0.3.3__tar.gz → 0.3.5__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 (104) hide show
  1. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/Evaluation_Tool.py +11 -2
  2. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +23 -5
  3. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/GBM_Tool.py +5 -1
  4. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/LRM_Tool.py +41 -6
  5. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/credit_model.py +146 -2
  6. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/feature_validation.py +65 -4
  7. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/reject_inference.py +163 -34
  8. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/sample_analysis.py +13 -1
  9. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/score_comparison.py +46 -2
  10. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Reject_Infer.py +106 -50
  11. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Reject_Infer.pyi +5 -5
  12. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Adapter.py +35 -1
  13. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +13 -4
  14. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/__init__.py +1 -1
  15. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/PKG-INFO +2 -2
  16. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/README.md +1 -1
  17. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  18. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/pyproject.toml +1 -1
  19. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/setup.py +1 -1
  20. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/ExcelMaster/ExcelFormatTool.py +0 -0
  21. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/ExcelMaster/ExcelMaster.py +0 -0
  22. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/ExcelMaster/Template.py +0 -0
  23. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/ExcelMaster/Utility.py +0 -0
  24. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/ExcelMaster/__init__.py +0 -0
  25. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/LICENSE +0 -0
  26. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/MANIFEST.in +0 -0
  27. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  28. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  29. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  30. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  31. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  32. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  33. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  34. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  35. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  36. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  37. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  38. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  39. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  40. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/__init__.py +0 -0
  41. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/kDataFrame.py +0 -0
  42. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  43. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  44. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Core/utils.py +0 -0
  45. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  46. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  47. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  48. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/__init__.py +0 -0
  49. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  50. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  51. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  52. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  53. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  54. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Explainability/__init__.py +0 -0
  55. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  56. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  57. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  58. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  59. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  60. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  61. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Feature/__init__.py +0 -0
  62. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  63. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  64. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  65. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  66. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  67. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Model/__init__.py +0 -0
  68. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/__init__.py +0 -0
  69. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/_common.py +0 -0
  70. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  71. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  72. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  73. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  74. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  75. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  76. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/Sample/__init__.py +0 -0
  77. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  78. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/UAT/__init__.py +0 -0
  79. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  80. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  81. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  82. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  83. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  84. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  85. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  86. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  87. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  88. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  89. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/__init__.py +0 -0
  90. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  91. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  92. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  93. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  94. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/ref_font/__init__.py +0 -0
  95. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  96. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Report/Report_Tool.py +0 -0
  97. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/Report/__init__.py +0 -0
  98. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  99. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  100. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  101. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/requires.txt +0 -0
  102. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  103. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/requirements.txt +0 -0
  104. {supermodelingfactory-0.3.3 → supermodelingfactory-0.3.5}/setup.cfg +0 -0
@@ -597,6 +597,7 @@ class Model_Evaluation_Tool:
597
597
  min_bin_prop: Optional[float] = None,
598
598
  include_missing: Optional[bool] = None,
599
599
  equal_freq: Optional[bool] = None,
600
+ sample_name: Optional[str] = None,
600
601
  ) -> pd.DataFrame:
601
602
  """
602
603
  Compare model performance across different scores.
@@ -626,7 +627,12 @@ class Model_Evaluation_Tool:
626
627
  Defaults to ``False`` for performance comparison.
627
628
  equal_freq : bool, optional
628
629
  Use equal-frequency binning. Defaults to the instance setting.
629
-
630
+ sample_name : str, optional
631
+ Label applied to the evaluated dataset and emitted in the result's
632
+ ``index`` column. Defaults to ``'all'`` (the whole ``data`` treated as
633
+ a single sample). Callers evaluating a specific slice can pass e.g.
634
+ ``'global'`` or a split name.
635
+
630
636
  Returns
631
637
  -------
632
638
  pandas.DataFrame
@@ -636,7 +642,10 @@ class Model_Evaluation_Tool:
636
642
  score_list = self.comp_scrlist
637
643
  dep = self.dep
638
644
  base_score = self.base_score
639
- sample_name = 'oot'
645
+ # Label for the single (unsplit) dataset being evaluated; surfaces as the
646
+ # 'index' column of the result. Defaults to a neutral 'all' rather than the
647
+ # historical (and misleading) 'oot', which implied an out-of-time sample.
648
+ sample_name = sample_name if sample_name is not None else 'all'
640
649
 
641
650
  if base_score is None:
642
651
  return pd.DataFrame()
@@ -155,17 +155,35 @@ class PSICalculator:
155
155
 
156
156
  detail = {}
157
157
  rows = []
158
- groups = [(None, current_data)] if group_by is None else list(current_data.groupby(group_by))
158
+ resolved_group = group_name if group_name is not None else group_by
159
+ if resolved_group is None:
160
+ groups = [(None, current_data, {})]
161
+ else:
162
+ group_cols = [resolved_group] if isinstance(resolved_group, str) else list(resolved_group)
163
+ grouped_data = current_data.copy()
164
+ for col in group_cols:
165
+ if grouped_data[col].isna().sum() > 0:
166
+ grouped_data[col] = grouped_data[col].fillna("__NULL__")
167
+ group_key = group_cols[0] if len(group_cols) == 1 else group_cols
168
+ groups = []
169
+ for grp_value, grp_df in grouped_data.groupby(group_key, dropna=False):
170
+ if len(group_cols) == 1:
171
+ group_info = {group_cols[0]: grp_value}
172
+ else:
173
+ value_tuple = grp_value if isinstance(grp_value, tuple) else (grp_value,)
174
+ group_info = dict(zip(group_cols, value_tuple))
175
+ groups.append((grp_value, grp_df, group_info))
176
+
159
177
  for var in varlist:
160
178
  expected_bins = adapter.assign_bins(expected_df, var)
161
- for grp_value, grp_df in groups:
179
+ for grp_value, grp_df, group_info in groups:
162
180
  current_bins = adapter.assign_bins(grp_df, var)
163
181
  row = {"var": var, "psi": round(_psi_from_bins(expected_bins, current_bins, self.content), self.precision)}
164
- if group_by is not None:
165
- row[group_name or group_by] = grp_value
182
+ row.update(group_info)
166
183
  rows.append(row)
167
184
  if return_details:
168
- detail[var if grp_value is None else (var, grp_value)] = {
185
+ detail_key = var if resolved_group is None else (var, grp_value)
186
+ detail[detail_key] = {
169
187
  "expected_bins": expected_bins.value_counts(normalize=True, dropna=False),
170
188
  "current_bins": current_bins.value_counts(normalize=True, dropna=False),
171
189
  }
@@ -153,7 +153,9 @@ def _normalize_catboost_params(params_dict):
153
153
  ``eval_metric`` defaults to ``'AUC'`` when neither ``eval_metric`` nor
154
154
  ``metric`` is supplied. Common lower-case aliases (e.g. ``'auc'``,
155
155
  ``'logloss'``) are normalized to the case-sensitive names CatBoost expects;
156
- any other string is left untouched.
156
+ any other string is left untouched. CatBoost writes ``catboost_info`` under
157
+ the current working directory by default, so SMF disables file output unless
158
+ the caller explicitly passes ``allow_writing_files`` or ``train_dir``.
157
159
 
158
160
  Returns
159
161
  -------
@@ -174,6 +176,8 @@ def _normalize_catboost_params(params_dict):
174
176
 
175
177
  params.setdefault('loss_function', 'Logloss')
176
178
  params.setdefault('verbose', False)
179
+ if 'allow_writing_files' not in params and 'train_dir' not in params:
180
+ params['allow_writing_files'] = False
177
181
 
178
182
  # eval_metric must be passed to the CatBoostClassifier constructor, not fit().
179
183
  if eval_metric is None:
@@ -523,6 +523,44 @@ class LRMaster:
523
523
  self._data = data
524
524
  return self
525
525
 
526
+ def __getstate__(self):
527
+ """
528
+ Exclude the reference training frame from pickling.
529
+
530
+ ``self._data`` holds the full training set (row-level customer records)
531
+ only as a convenience fallback for
532
+ ``get_statsmodel_summary``/``get_aic``/``get_bic``. Serializing it bloats
533
+ saved models (tens to hundreds of MB) and leaks raw data alongside the
534
+ model artifact. Prediction never depends on it, so it is dropped on
535
+ pickle; pass ``data=`` explicitly to the diagnostic methods on a loaded
536
+ model.
537
+ """
538
+ state = self.__dict__.copy()
539
+ state["_data"] = None
540
+ return state
541
+
542
+ def __setstate__(self, state):
543
+ self.__dict__.update(state)
544
+ self.__dict__.setdefault("_data", None)
545
+
546
+ def _require_data(self, data):
547
+ """
548
+ Resolve evaluation data, falling back to the stored training frame.
549
+
550
+ Raises a clear error when neither an explicit ``data`` argument nor a
551
+ stored ``self._data`` is available -- e.g. on a model loaded from disk,
552
+ where ``_data`` is intentionally dropped during pickling.
553
+ """
554
+ if data is None:
555
+ data = self._data
556
+ if data is None:
557
+ raise ValueError(
558
+ "No data available: this model was loaded without its training "
559
+ "frame (_data is dropped on save). Pass data=... to "
560
+ "get_statsmodel_summary/get_aic/get_bic."
561
+ )
562
+ return data
563
+
526
564
  def fit(self, data, varlist, tgt_name, val_data=None, val_varlist=None, val_tgt_name=None, weight_col=None):
527
565
  """
528
566
  Train the logistic regression model.
@@ -796,8 +834,7 @@ class LRMaster:
796
834
  When standardization is enabled the summary is computed on the
797
835
  standardized feature space, consistent with how the model was trained.
798
836
  """
799
- if data is None:
800
- data = self._data
837
+ data = self._require_data(data)
801
838
  if varlist is None:
802
839
  varlist = self.varlist
803
840
  if tgt_name is None:
@@ -824,8 +861,7 @@ class LRMaster:
824
861
  -------
825
862
  float
826
863
  """
827
- if data is None:
828
- data = self._data
864
+ data = self._require_data(data)
829
865
  if varlist is None:
830
866
  varlist = self.varlist
831
867
  if tgt_name is None:
@@ -851,8 +887,7 @@ class LRMaster:
851
887
  -------
852
888
  float
853
889
  """
854
- if data is None:
855
- data = self._data
890
+ data = self._require_data(data)
856
891
  if varlist is None:
857
892
  varlist = self.varlist
858
893
  if tgt_name is None:
@@ -26,10 +26,15 @@ class CreditModelPipelineConfig:
26
26
  split_col: str | None = None
27
27
  sample_col: str = "sample_ind"
28
28
  oot_col: str | None = "oot_flag"
29
+ weight_col: str | None = None
29
30
  random_state: int = 42
30
31
  write_outputs: bool = True
31
32
  write_excel: bool = True
32
33
  plot_outputs: bool = True
34
+ save_models: bool = False
35
+ model_output_dir: str | None = None
36
+ model_include_metadata: bool = True
37
+ save_woe_artifacts: bool = True
33
38
 
34
39
  split_config: dict[str, Any] = field(default_factory=lambda: {"test_size": 0.3, "stratify": True})
35
40
  feature_selection: dict[str, Any] = field(
@@ -103,6 +108,8 @@ class CreditModelPipelineResult:
103
108
  model_feature_sets: dict[str, list[str]] = field(default_factory=dict)
104
109
  selected_raw_features: list[str] = field(default_factory=list)
105
110
  selected_woe_features: list[str] = field(default_factory=list)
111
+ model_paths: dict[str, str] = field(default_factory=dict)
112
+ artifact_paths: dict[str, str] = field(default_factory=dict)
106
113
 
107
114
 
108
115
  class CreditModelPipeline:
@@ -166,9 +173,10 @@ class CreditModelPipeline:
166
173
  output_dir / "figs" / "woe",
167
174
  output_dir / "figs" / "mono_woe",
168
175
  output_dir / "figs" / "perf",
169
- output_dir / "models",
170
176
  output_dir / "explain",
171
177
  )
178
+ if cfg.save_models:
179
+ make_dirs(self._model_output_dir(), output_dir / "artifacts")
172
180
 
173
181
  splits = self._split_data(data)
174
182
  fs_summary, final_features = self._feature_selection(splits, feature_cols)
@@ -202,6 +210,19 @@ class CreditModelPipeline:
202
210
  optuna_results = self._run_optuna(model_inputs)
203
211
  perf_results = self._evaluate_models(model_inputs, models)
204
212
  explain_outputs = self._run_explainability(model_inputs, models)
213
+ model_paths: dict[str, str] = {}
214
+ artifact_paths: dict[str, str] = {}
215
+ model_paths_frame = None
216
+ if cfg.save_models:
217
+ model_paths, artifact_paths = self._save_models_and_artifacts(
218
+ models=models,
219
+ woe_artifacts=woe_artifacts,
220
+ perf_results=perf_results,
221
+ model_feature_sources=model_feature_sources,
222
+ model_feature_sets=model_feature_sets,
223
+ )
224
+ model_paths_frame = self._paths_to_frame(model_paths, artifact_paths)
225
+ safe_to_csv(model_paths_frame, output_dir / "model_paths.csv", index=False)
205
226
 
206
227
  if cfg.write_outputs:
207
228
  self._write_outputs(
@@ -214,6 +235,7 @@ class CreditModelPipeline:
214
235
  lr_search_results,
215
236
  warm_start_summary,
216
237
  model_feature_source_summary,
238
+ model_paths_frame,
217
239
  )
218
240
 
219
241
  report_path = None
@@ -226,6 +248,7 @@ class CreditModelPipeline:
226
248
  "LR_Param_Search": lr_search_results,
227
249
  "Warm_Start": warm_start_summary,
228
250
  "Model_Feature_Source": model_feature_source_summary,
251
+ "Model_Paths": model_paths_frame,
229
252
  }
230
253
  for name, perf in perf_results.items():
231
254
  sheets[f"Perf_{name.upper()}"] = perf
@@ -248,6 +271,8 @@ class CreditModelPipeline:
248
271
  model_feature_sets=model_feature_sets,
249
272
  selected_raw_features=list(selected_raw_features),
250
273
  selected_woe_features=list(selected_woe_features),
274
+ model_paths=model_paths,
275
+ artifact_paths=artifact_paths,
251
276
  )
252
277
 
253
278
  def _resolve_feature_cols(self, data: pd.DataFrame) -> list[str]:
@@ -259,6 +284,8 @@ class CreditModelPipeline:
259
284
  excluded.add(cfg.split_col)
260
285
  if cfg.oot_col:
261
286
  excluded.add(cfg.oot_col)
287
+ if cfg.weight_col:
288
+ excluded.add(cfg.weight_col)
262
289
  numeric_cols = data.select_dtypes(include=[np.number]).columns
263
290
  return [col for col in numeric_cols if col not in excluded]
264
291
 
@@ -268,6 +295,12 @@ class CreditModelPipeline:
268
295
  missing = [col for col in missing if col not in data.columns]
269
296
  if missing:
270
297
  raise KeyError(f"Missing required columns: {missing}")
298
+ if cfg.weight_col:
299
+ if cfg.weight_col not in data.columns:
300
+ raise KeyError(f"Missing weight_col {cfg.weight_col!r}")
301
+ from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
302
+
303
+ resolve_sample_weight(data=data, weight_col=cfg.weight_col, expected_len=len(data))
271
304
  if cfg.warm_start_enabled:
272
305
  if not cfg.warm_start_score_col:
273
306
  raise ValueError("warm_start_score_col is required when warm_start_enabled=True")
@@ -278,6 +311,21 @@ class CreditModelPipeline:
278
311
  if cfg.warm_start_on_unsupported not in {"skip", "raise"}:
279
312
  raise ValueError("warm_start_on_unsupported must be 'skip' or 'raise'")
280
313
 
314
+ def _gbm_weight_kwargs(self, train: pd.DataFrame, val: pd.DataFrame) -> dict[str, Any]:
315
+ cfg = self.config
316
+ if not cfg.weight_col:
317
+ return {}
318
+ from Modeling_Tool.Core.sample_weight_utils import resolve_sample_weight
319
+
320
+ kwargs: dict[str, Any] = {}
321
+ train_sw = resolve_sample_weight(data=train, weight_col=cfg.weight_col, expected_len=len(train))
322
+ if train_sw is not None:
323
+ kwargs["sample_weight"] = train_sw
324
+ eval_sw = resolve_sample_weight(data=val, weight_col=cfg.weight_col, expected_len=len(val))
325
+ if eval_sw is not None:
326
+ kwargs["eval_sample_weight"] = eval_sw
327
+ return kwargs
328
+
281
329
  def _validate_gbm_feature_source_config(self) -> None:
282
330
  source_cfg = self.config.gbm_feature_source
283
331
  if isinstance(source_cfg, dict):
@@ -590,6 +638,7 @@ class CreditModelPipeline:
590
638
  val_data=val,
591
639
  val_varlist=feature_cols,
592
640
  val_tgt_name=cfg.target_col,
641
+ weight_col=cfg.weight_col,
593
642
  )
594
643
  models[name] = (lr, getattr(lr, "model", lr), list(feature_cols))
595
644
  elif name in {"lgb", "xgb", "cat"}:
@@ -605,6 +654,7 @@ class CreditModelPipeline:
605
654
  valx=val[feature_cols],
606
655
  valy=val[cfg.target_col].astype(int),
607
656
  init_score=init_score,
657
+ **self._gbm_weight_kwargs(train, val),
608
658
  )
609
659
  raw = gbm._model.model if hasattr(gbm, "_model") else gbm
610
660
  models[name] = (gbm, raw, list(feature_cols))
@@ -627,6 +677,8 @@ class CreditModelPipeline:
627
677
  "model_type": f"{cfg.backward_model}m" if cfg.backward_model == "lgb" else cfg.backward_model,
628
678
  "validation_data": splits["oos"],
629
679
  "test_data_dict": {"oot": splits["oot"]},
680
+ "weight_col": cfg.weight_col,
681
+ "validation_weight_col": cfg.weight_col,
630
682
  },
631
683
  cfg.backward_params.get("init", {}),
632
684
  )
@@ -687,6 +739,8 @@ class CreditModelPipeline:
687
739
  tgt_name=cfg.target_col,
688
740
  eval_sets={"oos": splits["oos"], "oot": splits["oot"]},
689
741
  param_grid=cfg.lr_search_param_grid,
742
+ weight_col=cfg.weight_col,
743
+ eval_weight_col=cfg.weight_col,
690
744
  **params,
691
745
  )
692
746
  self._lr_best_params = dict(getattr(lr, "best_params_", {}) or {})
@@ -807,6 +861,8 @@ class CreditModelPipeline:
807
861
  data=splits["ins"],
808
862
  search_space=search_spaces[name],
809
863
  fit_kwargs=fit_kwargs or None,
864
+ weight_col=cfg.weight_col,
865
+ eval_weight_col=cfg.weight_col,
810
866
  **common,
811
867
  )
812
868
  except Exception as exc:
@@ -834,7 +890,7 @@ class CreditModelPipeline:
834
890
  for ds_name, df in splits.items():
835
891
  scored = df.copy()
836
892
  scored[f"pred_{name}"] = self._predict_model_positive(name, wrapper, scored, feature_cols)
837
- add_dataset_with_optional_weight(evaluator, ds_name, scored)
893
+ add_dataset_with_optional_weight(evaluator, ds_name, scored, weight_col=cfg.weight_col)
838
894
  fig_save_path = None
839
895
  if cfg.write_outputs and cfg.plot_outputs:
840
896
  fig_save_path = str(Path(cfg.output_dir) / "figs" / "perf" / f"perf_{name}.png")
@@ -956,6 +1012,92 @@ class CreditModelPipeline:
956
1012
  },
957
1013
  }
958
1014
 
1015
+ def _model_output_dir(self) -> Path:
1016
+ cfg = self.config
1017
+ return Path(cfg.model_output_dir) if cfg.model_output_dir else Path(cfg.output_dir) / "models"
1018
+
1019
+ def _save_models_and_artifacts(
1020
+ self,
1021
+ models: dict[str, tuple[Any, Any, list[str]]],
1022
+ woe_artifacts: dict[str, Any],
1023
+ perf_results: dict[str, pd.DataFrame],
1024
+ model_feature_sources: dict[str, str],
1025
+ model_feature_sets: dict[str, list[str]],
1026
+ ) -> tuple[dict[str, str], dict[str, str]]:
1027
+ from Modeling_Tool import save_model
1028
+
1029
+ cfg = self.config
1030
+ model_dir = self._model_output_dir()
1031
+ artifact_dir = Path(cfg.output_dir) / "artifacts"
1032
+ make_dirs(model_dir)
1033
+ model_paths: dict[str, str] = {}
1034
+ artifact_paths: dict[str, str] = {}
1035
+
1036
+ woe_table_path = None
1037
+ if cfg.save_woe_artifacts:
1038
+ make_dirs(artifact_dir)
1039
+ woe_table = woe_artifacts.get("woe_table")
1040
+ if isinstance(woe_table, pd.DataFrame):
1041
+ woe_table_path = artifact_dir / "woe_table.csv"
1042
+ safe_to_csv(woe_table, woe_table_path, index=False)
1043
+ artifact_paths["woe_table"] = str(woe_table_path)
1044
+ engine = woe_artifacts.get("engine")
1045
+ if engine is not None:
1046
+ engine_path = artifact_dir / "woe_engine.pkl"
1047
+ save_model(
1048
+ engine,
1049
+ engine_path,
1050
+ metadata={
1051
+ "pipeline": "CreditModelPipeline",
1052
+ "artifact_role": "woe_engine",
1053
+ "target_col": cfg.target_col,
1054
+ "raw_features": list(woe_artifacts.get("features") or []),
1055
+ "woe_features": list(woe_artifacts.get("woe_features") or []),
1056
+ "woe_engine": woe_artifacts.get("engine_name"),
1057
+ "random_state": cfg.random_state,
1058
+ },
1059
+ feature_cols=woe_artifacts.get("features"),
1060
+ include_metadata=cfg.model_include_metadata,
1061
+ )
1062
+ artifact_paths["woe_engine"] = str(engine_path)
1063
+
1064
+ warm_start_requested = {str(x).lower() for x in as_list(cfg.warm_start_models)}
1065
+ for name, (wrapper, _, feature_cols) in models.items():
1066
+ path = model_dir / f"model_{name}.pkl"
1067
+ metadata = {
1068
+ "pipeline": "CreditModelPipeline",
1069
+ "model_name": name,
1070
+ "target_col": cfg.target_col,
1071
+ "feature_cols": list(feature_cols),
1072
+ "feature_source": model_feature_sources.get(name),
1073
+ "model_feature_set": model_feature_sets.get(name, list(feature_cols)),
1074
+ "model_params": dict(cfg.model_params.get(name, {})),
1075
+ "warm_start_enabled": bool(cfg.warm_start_enabled and name in warm_start_requested),
1076
+ "warm_start_score_col": cfg.warm_start_score_col,
1077
+ "random_state": cfg.random_state,
1078
+ }
1079
+ metrics = None
1080
+ if isinstance(perf_results.get(name), pd.DataFrame):
1081
+ metrics = {"perf_results": perf_results[name].to_dict("records")}
1082
+ save_model(
1083
+ wrapper,
1084
+ path,
1085
+ metadata=metadata,
1086
+ feature_cols=feature_cols,
1087
+ woe_mapping_path=str(woe_table_path) if woe_table_path else None,
1088
+ metrics=metrics,
1089
+ model_name=name,
1090
+ include_metadata=cfg.model_include_metadata,
1091
+ )
1092
+ model_paths[name] = str(path)
1093
+ return model_paths, artifact_paths
1094
+
1095
+ @staticmethod
1096
+ def _paths_to_frame(model_paths: dict[str, str], artifact_paths: dict[str, str]) -> pd.DataFrame:
1097
+ rows = [{"name": name, "path": path, "kind": "model"} for name, path in model_paths.items()]
1098
+ rows.extend({"name": name, "path": path, "kind": "artifact"} for name, path in artifact_paths.items())
1099
+ return pd.DataFrame(rows)
1100
+
959
1101
  def _write_outputs(
960
1102
  self,
961
1103
  output_dir: Path,
@@ -967,6 +1109,7 @@ class CreditModelPipeline:
967
1109
  lr_search_results: pd.DataFrame | None,
968
1110
  warm_start_summary: pd.DataFrame | None,
969
1111
  model_feature_source_summary: pd.DataFrame | None,
1112
+ model_paths_frame: pd.DataFrame | None,
970
1113
  ) -> None:
971
1114
  if isinstance(fs_summary.get("psi"), pd.DataFrame):
972
1115
  safe_to_csv(fs_summary["psi"], output_dir / "psi_result.csv", index=False)
@@ -977,6 +1120,7 @@ class CreditModelPipeline:
977
1120
  safe_to_csv(lr_search_results, output_dir / "lr_param_search.csv", index=False)
978
1121
  safe_to_csv(warm_start_summary, output_dir / "warm_start_summary.csv", index=False)
979
1122
  safe_to_csv(model_feature_source_summary, output_dir / "model_feature_sources.csv", index=False)
1123
+ safe_to_csv(model_paths_frame, output_dir / "model_paths.csv", index=False)
980
1124
  for name, df in optuna_results.items():
981
1125
  safe_to_csv(df, output_dir / f"{name}_optuna_search.csv", index=False)
982
1126
  for name, df in perf_results.items():
@@ -648,6 +648,7 @@ class FeatureValidationPipeline:
648
648
  continue
649
649
  params = dict(cfg.corr_params or {})
650
650
  max_iterations = int(params.pop("max_iterations", 10))
651
+ corr_method = params.get("method", "pearson")
651
652
  filt = CorrelationFilter(data=observed, dep=target, **params)
652
653
  keep = filt.remove_highly_correlated(features, max_iterations=max_iterations)
653
654
  removed = [feature for feature in features if feature not in keep]
@@ -659,6 +660,7 @@ class FeatureValidationPipeline:
659
660
  for _, corr_row in corr.iterrows():
660
661
  key = tuple(sorted([str(corr_row.get("VAR1")), str(corr_row.get("VAR2"))]))
661
662
  meta = pair_meta.get(key, {})
663
+ row_corr_method = meta.get("corr_method", corr_method)
662
664
  for var in [corr_row.get("VAR1"), corr_row.get("VAR2")]:
663
665
  metrics = metric_map.get(var, {})
664
666
  rows.append(
@@ -673,9 +675,14 @@ class FeatureValidationPipeline:
673
675
  "iv": metrics.get("iv"),
674
676
  "ks_in_gains": metrics.get("ks_in_gains"),
675
677
  "lift_in_gains": metrics.get("lift_in_gains"),
678
+ **self._corr_detail_fields(
679
+ corr_row.get("VAR1"),
680
+ corr_row.get("VAR2"),
681
+ var,
682
+ row_corr_method,
683
+ "cross_batch",
684
+ ),
676
685
  "pair_type": "new_new_cross_batch",
677
- "detail_scope": "cross_batch",
678
- "corr_method": meta.get("corr_method", params.get("method", "pearson")),
679
686
  "batch_left": meta.get("batch_left"),
680
687
  "batch_right": meta.get("batch_right"),
681
688
  }
@@ -688,8 +695,14 @@ class FeatureValidationPipeline:
688
695
  "corr_var1": features[0],
689
696
  "corr_var2": features[1],
690
697
  "recommended_action": "remove",
698
+ **self._corr_detail_fields(
699
+ features[0],
700
+ features[1],
701
+ ",".join(removed),
702
+ pair.get("corr_method", params.get("method", "pearson")),
703
+ "cross_batch",
704
+ ),
691
705
  "pair_type": "new_new_cross_batch",
692
- "detail_scope": "cross_batch",
693
706
  "batch_left": pair.get("batch_left"),
694
707
  "batch_right": pair.get("batch_right"),
695
708
  }
@@ -1121,6 +1134,12 @@ class FeatureValidationPipeline:
1121
1134
  result = pd.DataFrame(result)
1122
1135
  if group_col is None and "psi" not in result.columns:
1123
1136
  result = pd.DataFrame({"psi": result.iloc[:, 0]})
1137
+ if group_col is None:
1138
+ result["group_value"] = "global"
1139
+ elif group_col in result.columns:
1140
+ result["group_value"] = result[group_col]
1141
+ else:
1142
+ result["group_value"] = np.nan
1124
1143
  result["target"] = target_key
1125
1144
  result["group_col"] = group_col or "global"
1126
1145
  rows.append(result)
@@ -1350,6 +1369,7 @@ class FeatureValidationPipeline:
1350
1369
  continue
1351
1370
  params = dict(cfg.corr_params or {})
1352
1371
  max_iterations = int(params.pop("max_iterations", 10))
1372
+ corr_method = params.get("method", "pearson")
1353
1373
  binner = woe_artifacts.get("by_target", {}).get(target, {}).get("engine") if cfg.corr_use_woe_bins else None
1354
1374
  filt = CorrelationFilter(data=data, dep=target, woe_binner=binner, **params)
1355
1375
  keep = filt.remove_highly_correlated(features, max_iterations=max_iterations)
@@ -1374,12 +1394,53 @@ class FeatureValidationPipeline:
1374
1394
  "iv": metrics.get("iv"),
1375
1395
  "ks_in_gains": metrics.get("ks_in_gains"),
1376
1396
  "lift_in_gains": metrics.get("lift_in_gains"),
1397
+ **self._corr_detail_fields(
1398
+ row.get("VAR1"),
1399
+ row.get("VAR2"),
1400
+ var,
1401
+ corr_method,
1402
+ "within_batch",
1403
+ ),
1377
1404
  }
1378
1405
  )
1379
1406
  if not rows and removed:
1380
- rows.append({"target": target, "recommended_action": "remove", "var": ",".join(removed)})
1407
+ rows.append(
1408
+ {
1409
+ "target": target,
1410
+ "recommended_action": "remove",
1411
+ "var": ",".join(removed),
1412
+ "metric_var": ",".join(removed),
1413
+ "corr_method": corr_method,
1414
+ "detail_scope": "within_batch",
1415
+ }
1416
+ )
1381
1417
  return pd.DataFrame(rows)
1382
1418
 
1419
+ @staticmethod
1420
+ def _corr_detail_fields(
1421
+ corr_var1: Any,
1422
+ corr_var2: Any,
1423
+ metric_var: Any,
1424
+ corr_method: str,
1425
+ detail_scope: str,
1426
+ ) -> dict[str, Any]:
1427
+ corr_var1_str = str(corr_var1) if pd.notna(corr_var1) else ""
1428
+ corr_var2_str = str(corr_var2) if pd.notna(corr_var2) else ""
1429
+ metric_var_str = str(metric_var) if pd.notna(metric_var) else ""
1430
+ if metric_var_str == corr_var1_str:
1431
+ metric_var_position = "corr_var1"
1432
+ elif metric_var_str == corr_var2_str:
1433
+ metric_var_position = "corr_var2"
1434
+ else:
1435
+ metric_var_position = np.nan
1436
+ return {
1437
+ "corr_pair_id": f"{corr_var1_str}||{corr_var2_str}" if corr_var1_str or corr_var2_str else np.nan,
1438
+ "metric_var": metric_var,
1439
+ "metric_var_position": metric_var_position,
1440
+ "corr_method": corr_method,
1441
+ "detail_scope": detail_scope,
1442
+ }
1443
+
1383
1444
  def _build_validation_summary(
1384
1445
  self,
1385
1446
  data: pd.DataFrame,