SuperModelingFactory 0.3.2__tar.gz → 0.3.4__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.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/Evaluation_Tool.py +11 -2
  2. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +23 -5
  3. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/GBM_Tool.py +5 -1
  4. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/LRM_Tool.py +41 -6
  5. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/credit_model.py +113 -1
  6. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/feature_validation.py +65 -4
  7. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/reject_inference.py +163 -34
  8. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/sample_analysis.py +13 -1
  9. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/score_comparison.py +46 -2
  10. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Reject_Infer.py +106 -50
  11. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Reject_Infer.pyi +5 -5
  12. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Adapter.py +35 -1
  13. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Monotone_Binner.py +82 -33
  14. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/__init__.py +1 -1
  15. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/PKG-INFO +2 -2
  16. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/README.md +1 -1
  17. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/PKG-INFO +2 -2
  18. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/pyproject.toml +1 -1
  19. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/setup.py +1 -1
  20. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/ExcelMaster/ExcelFormatTool.py +0 -0
  21. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/ExcelMaster/ExcelMaster.py +0 -0
  22. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/ExcelMaster/Template.py +0 -0
  23. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/ExcelMaster/Utility.py +0 -0
  24. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/ExcelMaster/__init__.py +0 -0
  25. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/LICENSE +0 -0
  26. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/MANIFEST.in +0 -0
  27. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Binning_Tool.py +0 -0
  28. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Binning_Tool.pyi +0 -0
  29. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Check_DuckDB_Compatibility.py +0 -0
  30. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Json_Data_Converter.py +0 -0
  31. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Model_Registry_Tool.py +0 -0
  32. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/ODPS_Tool.py +0 -0
  33. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Parallel_Engine.py +0 -0
  34. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Parallel_ODPS_Manager.py +0 -0
  35. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Proc_Compare.py +0 -0
  36. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Slope_Tool.py +0 -0
  37. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/Slope_Tool.pyi +0 -0
  38. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/XOR_Encryptor.py +0 -0
  39. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/XOR_Encryptor.pyi +0 -0
  40. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/__init__.py +0 -0
  41. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/kDataFrame.py +0 -0
  42. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/kDataFrame.pyi +0 -0
  43. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/sample_weight_utils.py +0 -0
  44. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Core/utils.py +0 -0
  45. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/Evaluation_Tool.pyi +0 -0
  46. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/Model_Eval_Tool.py +0 -0
  47. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/Model_Eval_Tool.pyi +0 -0
  48. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/__init__.py +0 -0
  49. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/evaluate_model.py +0 -0
  50. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/evaluate_model.pyi +0 -0
  51. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Eval/weighted_eval_utils.py +0 -0
  52. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Explainability/Coalition_Structure.py +0 -0
  53. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Explainability/Model_Explainer.py +0 -0
  54. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Explainability/__init__.py +0 -0
  55. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/Distribution_Tool.py +0 -0
  56. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/Distribution_Tool.pyi +0 -0
  57. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/Feature_Insights.py +0 -0
  58. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/Feature_Insights.pyi +0 -0
  59. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/PSI_Tool.py +0 -0
  60. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/PSI_Tool.pyi +0 -0
  61. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Feature/__init__.py +0 -0
  62. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/Backward_Tool.py +0 -0
  63. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/Backward_Tool.pyi +0 -0
  64. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/GBM_Search_Tool.py +0 -0
  65. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/GBM_Tool.pyi +0 -0
  66. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/LRM_Tool.pyi +0 -0
  67. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Model/__init__.py +0 -0
  68. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/__init__.py +0 -0
  69. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/_common.py +0 -0
  70. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/mock_sample.py +0 -0
  71. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Pipeline/score_consistency_uat.py +0 -0
  72. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Distribution_Adaptation.py +0 -0
  73. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Distribution_Adaptation.pyi +0 -0
  74. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Sample_Split.py +0 -0
  75. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/Sample_Split.pyi +0 -0
  76. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/Sample/__init__.py +0 -0
  77. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/UAT/UAT_Consistency_Checker.py +0 -0
  78. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/UAT/__init__.py +0 -0
  79. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Adapter.pyi +0 -0
  80. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Master.py +0 -0
  81. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Master.pyi +0 -0
  82. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +0 -0
  83. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Plot_Tool.py +0 -0
  84. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Plot_Tool.pyi +0 -0
  85. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Report_Builder.py +0 -0
  86. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Report_Builder.pyi +0 -0
  87. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Tool.py +0 -0
  88. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/WOE_Tool.pyi +0 -0
  89. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/__init__.py +0 -0
  90. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/plot_woe_tool.py +0 -0
  91. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/WOE/plot_woe_tool.pyi +0 -0
  92. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/ref_font/KaiTi.ttf +0 -0
  93. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/ref_font/WeiRuanYaHei.ttf +0 -0
  94. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/ref_font/__init__.py +0 -0
  95. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Modeling_Tool/ref_font/simsun.ttc +0 -0
  96. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Report/Report_Tool.py +0 -0
  97. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/Report/__init__.py +0 -0
  98. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/SOURCES.txt +0 -0
  99. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/dependency_links.txt +0 -0
  100. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/not-zip-safe +0 -0
  101. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/requires.txt +0 -0
  102. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/SuperModelingFactory.egg-info/top_level.txt +0 -0
  103. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/requirements.txt +0 -0
  104. {supermodelingfactory-0.3.2 → supermodelingfactory-0.3.4}/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:
@@ -30,6 +30,10 @@ class CreditModelPipelineConfig:
30
30
  write_outputs: bool = True
31
31
  write_excel: bool = True
32
32
  plot_outputs: bool = True
33
+ save_models: bool = False
34
+ model_output_dir: str | None = None
35
+ model_include_metadata: bool = True
36
+ save_woe_artifacts: bool = True
33
37
 
34
38
  split_config: dict[str, Any] = field(default_factory=lambda: {"test_size": 0.3, "stratify": True})
35
39
  feature_selection: dict[str, Any] = field(
@@ -103,6 +107,8 @@ class CreditModelPipelineResult:
103
107
  model_feature_sets: dict[str, list[str]] = field(default_factory=dict)
104
108
  selected_raw_features: list[str] = field(default_factory=list)
105
109
  selected_woe_features: list[str] = field(default_factory=list)
110
+ model_paths: dict[str, str] = field(default_factory=dict)
111
+ artifact_paths: dict[str, str] = field(default_factory=dict)
106
112
 
107
113
 
108
114
  class CreditModelPipeline:
@@ -166,9 +172,10 @@ class CreditModelPipeline:
166
172
  output_dir / "figs" / "woe",
167
173
  output_dir / "figs" / "mono_woe",
168
174
  output_dir / "figs" / "perf",
169
- output_dir / "models",
170
175
  output_dir / "explain",
171
176
  )
177
+ if cfg.save_models:
178
+ make_dirs(self._model_output_dir(), output_dir / "artifacts")
172
179
 
173
180
  splits = self._split_data(data)
174
181
  fs_summary, final_features = self._feature_selection(splits, feature_cols)
@@ -202,6 +209,19 @@ class CreditModelPipeline:
202
209
  optuna_results = self._run_optuna(model_inputs)
203
210
  perf_results = self._evaluate_models(model_inputs, models)
204
211
  explain_outputs = self._run_explainability(model_inputs, models)
212
+ model_paths: dict[str, str] = {}
213
+ artifact_paths: dict[str, str] = {}
214
+ model_paths_frame = None
215
+ if cfg.save_models:
216
+ model_paths, artifact_paths = self._save_models_and_artifacts(
217
+ models=models,
218
+ woe_artifacts=woe_artifacts,
219
+ perf_results=perf_results,
220
+ model_feature_sources=model_feature_sources,
221
+ model_feature_sets=model_feature_sets,
222
+ )
223
+ model_paths_frame = self._paths_to_frame(model_paths, artifact_paths)
224
+ safe_to_csv(model_paths_frame, output_dir / "model_paths.csv", index=False)
205
225
 
206
226
  if cfg.write_outputs:
207
227
  self._write_outputs(
@@ -214,6 +234,7 @@ class CreditModelPipeline:
214
234
  lr_search_results,
215
235
  warm_start_summary,
216
236
  model_feature_source_summary,
237
+ model_paths_frame,
217
238
  )
218
239
 
219
240
  report_path = None
@@ -226,6 +247,7 @@ class CreditModelPipeline:
226
247
  "LR_Param_Search": lr_search_results,
227
248
  "Warm_Start": warm_start_summary,
228
249
  "Model_Feature_Source": model_feature_source_summary,
250
+ "Model_Paths": model_paths_frame,
229
251
  }
230
252
  for name, perf in perf_results.items():
231
253
  sheets[f"Perf_{name.upper()}"] = perf
@@ -248,6 +270,8 @@ class CreditModelPipeline:
248
270
  model_feature_sets=model_feature_sets,
249
271
  selected_raw_features=list(selected_raw_features),
250
272
  selected_woe_features=list(selected_woe_features),
273
+ model_paths=model_paths,
274
+ artifact_paths=artifact_paths,
251
275
  )
252
276
 
253
277
  def _resolve_feature_cols(self, data: pd.DataFrame) -> list[str]:
@@ -956,6 +980,92 @@ class CreditModelPipeline:
956
980
  },
957
981
  }
958
982
 
983
+ def _model_output_dir(self) -> Path:
984
+ cfg = self.config
985
+ return Path(cfg.model_output_dir) if cfg.model_output_dir else Path(cfg.output_dir) / "models"
986
+
987
+ def _save_models_and_artifacts(
988
+ self,
989
+ models: dict[str, tuple[Any, Any, list[str]]],
990
+ woe_artifacts: dict[str, Any],
991
+ perf_results: dict[str, pd.DataFrame],
992
+ model_feature_sources: dict[str, str],
993
+ model_feature_sets: dict[str, list[str]],
994
+ ) -> tuple[dict[str, str], dict[str, str]]:
995
+ from Modeling_Tool import save_model
996
+
997
+ cfg = self.config
998
+ model_dir = self._model_output_dir()
999
+ artifact_dir = Path(cfg.output_dir) / "artifacts"
1000
+ make_dirs(model_dir)
1001
+ model_paths: dict[str, str] = {}
1002
+ artifact_paths: dict[str, str] = {}
1003
+
1004
+ woe_table_path = None
1005
+ if cfg.save_woe_artifacts:
1006
+ make_dirs(artifact_dir)
1007
+ woe_table = woe_artifacts.get("woe_table")
1008
+ if isinstance(woe_table, pd.DataFrame):
1009
+ woe_table_path = artifact_dir / "woe_table.csv"
1010
+ safe_to_csv(woe_table, woe_table_path, index=False)
1011
+ artifact_paths["woe_table"] = str(woe_table_path)
1012
+ engine = woe_artifacts.get("engine")
1013
+ if engine is not None:
1014
+ engine_path = artifact_dir / "woe_engine.pkl"
1015
+ save_model(
1016
+ engine,
1017
+ engine_path,
1018
+ metadata={
1019
+ "pipeline": "CreditModelPipeline",
1020
+ "artifact_role": "woe_engine",
1021
+ "target_col": cfg.target_col,
1022
+ "raw_features": list(woe_artifacts.get("features") or []),
1023
+ "woe_features": list(woe_artifacts.get("woe_features") or []),
1024
+ "woe_engine": woe_artifacts.get("engine_name"),
1025
+ "random_state": cfg.random_state,
1026
+ },
1027
+ feature_cols=woe_artifacts.get("features"),
1028
+ include_metadata=cfg.model_include_metadata,
1029
+ )
1030
+ artifact_paths["woe_engine"] = str(engine_path)
1031
+
1032
+ warm_start_requested = {str(x).lower() for x in as_list(cfg.warm_start_models)}
1033
+ for name, (wrapper, _, feature_cols) in models.items():
1034
+ path = model_dir / f"model_{name}.pkl"
1035
+ metadata = {
1036
+ "pipeline": "CreditModelPipeline",
1037
+ "model_name": name,
1038
+ "target_col": cfg.target_col,
1039
+ "feature_cols": list(feature_cols),
1040
+ "feature_source": model_feature_sources.get(name),
1041
+ "model_feature_set": model_feature_sets.get(name, list(feature_cols)),
1042
+ "model_params": dict(cfg.model_params.get(name, {})),
1043
+ "warm_start_enabled": bool(cfg.warm_start_enabled and name in warm_start_requested),
1044
+ "warm_start_score_col": cfg.warm_start_score_col,
1045
+ "random_state": cfg.random_state,
1046
+ }
1047
+ metrics = None
1048
+ if isinstance(perf_results.get(name), pd.DataFrame):
1049
+ metrics = {"perf_results": perf_results[name].to_dict("records")}
1050
+ save_model(
1051
+ wrapper,
1052
+ path,
1053
+ metadata=metadata,
1054
+ feature_cols=feature_cols,
1055
+ woe_mapping_path=str(woe_table_path) if woe_table_path else None,
1056
+ metrics=metrics,
1057
+ model_name=name,
1058
+ include_metadata=cfg.model_include_metadata,
1059
+ )
1060
+ model_paths[name] = str(path)
1061
+ return model_paths, artifact_paths
1062
+
1063
+ @staticmethod
1064
+ def _paths_to_frame(model_paths: dict[str, str], artifact_paths: dict[str, str]) -> pd.DataFrame:
1065
+ rows = [{"name": name, "path": path, "kind": "model"} for name, path in model_paths.items()]
1066
+ rows.extend({"name": name, "path": path, "kind": "artifact"} for name, path in artifact_paths.items())
1067
+ return pd.DataFrame(rows)
1068
+
959
1069
  def _write_outputs(
960
1070
  self,
961
1071
  output_dir: Path,
@@ -967,6 +1077,7 @@ class CreditModelPipeline:
967
1077
  lr_search_results: pd.DataFrame | None,
968
1078
  warm_start_summary: pd.DataFrame | None,
969
1079
  model_feature_source_summary: pd.DataFrame | None,
1080
+ model_paths_frame: pd.DataFrame | None,
970
1081
  ) -> None:
971
1082
  if isinstance(fs_summary.get("psi"), pd.DataFrame):
972
1083
  safe_to_csv(fs_summary["psi"], output_dir / "psi_result.csv", index=False)
@@ -977,6 +1088,7 @@ class CreditModelPipeline:
977
1088
  safe_to_csv(lr_search_results, output_dir / "lr_param_search.csv", index=False)
978
1089
  safe_to_csv(warm_start_summary, output_dir / "warm_start_summary.csv", index=False)
979
1090
  safe_to_csv(model_feature_source_summary, output_dir / "model_feature_sources.csv", index=False)
1091
+ safe_to_csv(model_paths_frame, output_dir / "model_paths.csv", index=False)
980
1092
  for name, df in optuna_results.items():
981
1093
  safe_to_csv(df, output_dir / f"{name}_optuna_search.csv", index=False)
982
1094
  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,