SuperModelingFactory 0.2.0__py3-none-any.whl

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 (79) hide show
  1. ExcelMaster/ExcelFormatTool.py +487 -0
  2. ExcelMaster/ExcelMaster.py +917 -0
  3. ExcelMaster/Template.py +525 -0
  4. ExcelMaster/Utility.py +233 -0
  5. ExcelMaster/__init__.py +3 -0
  6. Modeling_Tool/Core/Binning_Tool.py +1608 -0
  7. Modeling_Tool/Core/Binning_Tool.pyi +48 -0
  8. Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
  9. Modeling_Tool/Core/Json_Data_Converter.py +621 -0
  10. Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
  11. Modeling_Tool/Core/ODPS_Tool.py +284 -0
  12. Modeling_Tool/Core/Slope_Tool.py +356 -0
  13. Modeling_Tool/Core/Slope_Tool.pyi +31 -0
  14. Modeling_Tool/Core/XOR_Encryptor.py +207 -0
  15. Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
  16. Modeling_Tool/Core/__init__.py +99 -0
  17. Modeling_Tool/Core/kDataFrame.py +228 -0
  18. Modeling_Tool/Core/kDataFrame.pyi +43 -0
  19. Modeling_Tool/Core/sample_weight_utils.py +77 -0
  20. Modeling_Tool/Core/utils.py +2672 -0
  21. Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
  22. Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
  23. Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
  24. Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
  25. Modeling_Tool/Eval/__init__.py +54 -0
  26. Modeling_Tool/Eval/evaluate_model.py +2008 -0
  27. Modeling_Tool/Eval/evaluate_model.pyi +50 -0
  28. Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
  29. Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
  30. Modeling_Tool/Explainability/Model_Explainer.py +743 -0
  31. Modeling_Tool/Explainability/__init__.py +24 -0
  32. Modeling_Tool/Feature/Distribution_Tool.py +509 -0
  33. Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
  34. Modeling_Tool/Feature/Feature_Insights.py +762 -0
  35. Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
  36. Modeling_Tool/Feature/PSI_Tool.py +1195 -0
  37. Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
  38. Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
  39. Modeling_Tool/Feature/__init__.py +40 -0
  40. Modeling_Tool/Model/Backward_Tool.py +778 -0
  41. Modeling_Tool/Model/Backward_Tool.pyi +45 -0
  42. Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
  43. Modeling_Tool/Model/GBM_Tool.py +1610 -0
  44. Modeling_Tool/Model/GBM_Tool.pyi +90 -0
  45. Modeling_Tool/Model/LRM_Tool.py +1198 -0
  46. Modeling_Tool/Model/LRM_Tool.pyi +47 -0
  47. Modeling_Tool/Model/__init__.py +61 -0
  48. Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
  49. Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
  50. Modeling_Tool/Sample/Reject_Infer.py +413 -0
  51. Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
  52. Modeling_Tool/Sample/Sample_Split.py +520 -0
  53. Modeling_Tool/Sample/Sample_Split.pyi +43 -0
  54. Modeling_Tool/Sample/__init__.py +31 -0
  55. Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
  56. Modeling_Tool/UAT/__init__.py +19 -0
  57. Modeling_Tool/WOE/WOE_Adapter.py +204 -0
  58. Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
  59. Modeling_Tool/WOE/WOE_Master.py +491 -0
  60. Modeling_Tool/WOE/WOE_Master.pyi +40 -0
  61. Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
  62. Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
  63. Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
  64. Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
  65. Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
  66. Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
  67. Modeling_Tool/WOE/WOE_Tool.py +1094 -0
  68. Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
  69. Modeling_Tool/WOE/__init__.py +86 -0
  70. Modeling_Tool/WOE/plot_woe_tool.py +290 -0
  71. Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
  72. Modeling_Tool/__init__.py +176 -0
  73. Report/Report_Tool.py +380 -0
  74. Report/__init__.py +3 -0
  75. supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
  76. supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
  77. supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
  78. supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
  79. supermodelingfactory-0.2.0.dist-info/top_level.txt +3 -0
@@ -0,0 +1,47 @@
1
+ # =============================================================================
2
+ # Modeling_Tool.Model.LRM_Tool
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2026 Kyle Sun <github.com/Kyle-J-Sun>. All rights reserved.
5
+ # SuperModelingFactory — Licensed under the Business Source License 1.1.
6
+ #
7
+ # This stub describes the public API of a closed-source module compiled to a
8
+ # native extension (.so / .pyd). The original source is not distributed.
9
+ # Production / commercial use requires a separate commercial license.
10
+ #
11
+ # FINGERPRINT: SMF-LRMTOOL-3703d3ce
12
+ # (Unique trace marker. Do not remove or alter — used for plagiarism
13
+ # detection across the public internet.)
14
+ # =============================================================================
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ from sklearn.linear_model import LogisticRegression
19
+ import logging
20
+
21
+ def lr_model(mdlx, mdly, valx, valy, params_dict): ...
22
+ def lr_varimp(model): ...
23
+ def get_lr_statsmodel_summary(model, x, y, feature_names = None): ...
24
+ def compute_aic(model, x, y): ...
25
+ def compute_bic(model, x, y): ...
26
+
27
+ class FeatureSelectionAnalyzer:
28
+ def __init__(self, significance_level = 0.05): ...
29
+ def chi2_selection(self, data, feature_cols, target_col): ...
30
+ def compute_vif(self, data): ...
31
+ def correlation_filter(self, data, threshold = 0.8): ...
32
+
33
+ class LRMaster:
34
+ def __init__(self, params = None, model = None, varlist = None, tgt_name = None, standardize = False, scaler = None): ...
35
+ def set_data(self, data): ...
36
+ def fit(self, data, varlist, tgt_name, val_data = None, val_varlist = None, val_tgt_name = None): ...
37
+ def calibrate_model(self, model = None, train_df = None, method = 'sigmoid', cv = 5): ...
38
+ def eval_calibrated_outcome(self, evalset, plot = False): ...
39
+ def predict(self, data, varlist = None, calibrated_model = False): ...
40
+ def predict_proba(self, data, varlist = None, calibrated_model = False): ...
41
+ def get_variable_importance(self): ...
42
+ def get_statsmodel_summary(self, data = None, varlist = None, tgt_name = None): ...
43
+ def get_aic(self, data = None, varlist = None, tgt_name = None): ...
44
+ def get_bic(self, data = None, varlist = None, tgt_name = None): ...
45
+ def stepwise_selection(self, data, varlist, tgt_name, criterion = 'aic', direction = 'both', max_iter = 100, verbose = True): ...
46
+ def grid_search_params(self, data, varlist, tgt_name, eval_sets, param_grid, objective = 'oot_gap_penalized', primary_set = None, gap_ref_sets = None, metric = 'auc', refit = True, verbose = True): ...
47
+ def clone(self): ...
@@ -0,0 +1,61 @@
1
+ """
2
+ Modeling module for credit modeling toolkit.
3
+ """
4
+
5
+ from .LRM_Tool import (
6
+ lr_model,
7
+ lr_varimp,
8
+ get_lr_statsmodel_summary,
9
+ compute_aic,
10
+ compute_bic,
11
+ LRMaster,
12
+ FeatureSelectionAnalyzer,
13
+ )
14
+
15
+ from .GBM_Tool import (
16
+ set_num_leaves,
17
+ lgb_model,
18
+ lgb_varimp,
19
+ lgbm_quick_train,
20
+ xgb_model,
21
+ xgbm_quick_train,
22
+ xgb_varimp,
23
+ catboost_model,
24
+ catboost_varimp,
25
+ catboost_quick_train,
26
+ LightGBMModel,
27
+ XGBoostModel,
28
+ CatBoostModel,
29
+ GradientBoostingModel as _GradientBoostingModelBase,
30
+ )
31
+
32
+ GradientBoostingModel = _GradientBoostingModelBase
33
+ try:
34
+ from .GBM_Search_Tool import _gbm_param_search, attach_gbm_param_search
35
+ attach_gbm_param_search(_GradientBoostingModelBase)
36
+ if not hasattr(_GradientBoostingModelBase, "param_search"):
37
+ class GradientBoostingModel(_GradientBoostingModelBase):
38
+ param_search = _gbm_param_search
39
+ best_params_ = None
40
+ search_results_ = None
41
+ except Exception:
42
+ GradientBoostingModel = _GradientBoostingModelBase
43
+
44
+ from .Backward_Tool import (
45
+ backward_lgbm,
46
+ backward_xgbm,
47
+ BackwardVariableEliminator,
48
+ BackwardEliminationAnalyzer,
49
+ )
50
+
51
+ __all__ = [
52
+ 'lr_model', 'lr_varimp', 'get_lr_statsmodel_summary',
53
+ 'compute_aic', 'compute_bic',
54
+ 'LRMaster', 'FeatureSelectionAnalyzer',
55
+ 'set_num_leaves', 'lgb_model', 'lgb_varimp', 'lgbm_quick_train',
56
+ 'xgb_model', 'xgbm_quick_train', 'xgb_varimp',
57
+ 'catboost_model', 'catboost_varimp', 'catboost_quick_train',
58
+ 'LightGBMModel', 'XGBoostModel', 'CatBoostModel', 'GradientBoostingModel',
59
+ 'backward_lgbm', 'backward_xgbm',
60
+ 'BackwardVariableEliminator', 'BackwardEliminationAnalyzer',
61
+ ]
@@ -0,0 +1,131 @@
1
+ import numpy as np
2
+ import pandas as pd
3
+ from sklearn.model_selection import train_test_split
4
+ from sklearn.ensemble import HistGradientBoostingClassifier
5
+ import matplotlib.pyplot as plt
6
+ import seaborn as sns
7
+ from sklearn.calibration import CalibratedClassifierCV
8
+
9
+ class DistributionAdaptation:
10
+ def __init__(self, method='density_ratio'):
11
+ """
12
+ 初始化分布适配器
13
+
14
+ Parameters:
15
+ -----------
16
+ method: str
17
+ 'density_ratio': 密度比估计
18
+ 'kl_divergence': KL散度加权
19
+ 'covariate_shift': 协变量偏移修正
20
+ """
21
+ self.method = method
22
+ self.sample_weights = None
23
+ self.feature_importances = None
24
+
25
+ def estimate_density_ratio(self, X_train, X_oot):
26
+ """
27
+ 使用密度比估计方法计算样本权重
28
+ KLIEP/KMM等方法的简化实现
29
+ """
30
+ from sklearn.neighbors import KernelDensity
31
+
32
+ # 使用KDE估计密度
33
+ kde_train = KernelDensity(kernel='gaussian', bandwidth=0.5)
34
+ kde_oot = KernelDensity(kernel='gaussian', bandwidth=0.5)
35
+
36
+ kde_train.fit(X_train)
37
+ kde_oot.fit(X_oot)
38
+
39
+ # 计算密度比: p_oot(x) / p_train(x)
40
+ log_density_train = kde_train.score_samples(X_train)
41
+ log_density_oot = kde_oot.score_samples(X_train)
42
+
43
+ # 防止除零和数值不稳定
44
+ density_ratio = np.exp(log_density_oot - log_density_train)
45
+
46
+ # 截断处理异常值
47
+ density_ratio = np.clip(density_ratio, 0.1, 10)
48
+
49
+ # 归一化
50
+ density_ratio = density_ratio / density_ratio.mean()
51
+
52
+ return density_ratio
53
+
54
+ def covariate_shift_weighting(self, X_train, X_oot):
55
+ """
56
+ 使用领域分类器估计样本重要性权重
57
+ """
58
+ from sklearn.linear_model import LogisticRegression
59
+
60
+ # 创建领域标签: 训练集为0, OOT为1
61
+ n_train = len(X_train)
62
+ n_oot = len(X_oot)
63
+
64
+ X_combined = np.vstack([X_train, X_oot])
65
+ y_domain = np.hstack([np.zeros(n_train), np.ones(n_oot)])
66
+
67
+ # 训练领域分类器
68
+ domain_classifier = LogisticRegression(
69
+ C=1.0, max_iter=1000, random_state=42
70
+ )
71
+ domain_classifier.fit(X_combined, y_domain)
72
+
73
+ # 预测训练样本来自OOT的概率
74
+ p_oot = domain_classifier.predict_proba(X_train)[:, 1]
75
+
76
+ # 计算权重: p(oot|x) / p(train|x)
77
+ # 防止数值不稳定
78
+ epsilon = 1e-10
79
+ weights = p_oot / (1 - p_oot + epsilon)
80
+
81
+ # 使用beta分布平滑权重
82
+ weights = np.clip(weights, 0.1, 10)
83
+ weights = weights / weights.mean()
84
+
85
+ return weights
86
+
87
+ def fit(self, X_train, X_oot, y_train=None):
88
+ """
89
+ 计算适应OOT分布的样本权重
90
+ """
91
+ if self.method == 'density_ratio':
92
+ self.sample_weights = self.estimate_density_ratio(X_train, X_oot)
93
+ elif self.method == 'covariate_shift':
94
+ self.sample_weights = self.covariate_shift_weighting(X_train, X_oot)
95
+ else:
96
+ # 默认使用混合方法
97
+ w1 = self.estimate_density_ratio(X_train, X_oot)
98
+ w2 = self.covariate_shift_weighting(X_train, X_oot)
99
+ self.sample_weights = (w1 + w2) / 2
100
+
101
+ return self
102
+
103
+ def get_weights(self):
104
+ """返回样本权重"""
105
+ return self.sample_weights
106
+
107
+ def visualize_distribution_comparison(self, X_train, X_oot, features=None, n_features=5):
108
+ """
109
+ 可视化训练集和OOT集的分布差异
110
+ """
111
+ if features is None:
112
+ # 选择方差最大的特征
113
+ variances = np.var(X_train, axis=0)
114
+ features = np.argsort(variances)[-n_features:]
115
+
116
+ fig, axes = plt.subplots(1, n_features, figsize=(5*n_features, 4))
117
+
118
+ for idx, feature_idx in enumerate(features[:n_features]):
119
+ ax = axes[idx] if n_features > 1 else axes
120
+
121
+ # 绘制分布
122
+ sns.kdeplot(X_train[:, feature_idx], ax=ax, label='Train', fill=True, alpha=0.5)
123
+ sns.kdeplot(X_oot[:, feature_idx], ax=ax, label='OOT', fill=True, alpha=0.5)
124
+
125
+ ax.set_title(f'Feature {feature_idx}')
126
+ ax.legend()
127
+ ax.set_xlabel('Value')
128
+ ax.set_ylabel('Density')
129
+
130
+ plt.tight_layout()
131
+ plt.show()
@@ -0,0 +1,30 @@
1
+ # =============================================================================
2
+ # Modeling_Tool.Sample.Distribution_Adaptation
3
+ # -----------------------------------------------------------------------------
4
+ # Copyright (c) 2026 Kyle Sun <github.com/Kyle-J-Sun>. All rights reserved.
5
+ # SuperModelingFactory — Licensed under the Business Source License 1.1.
6
+ #
7
+ # This stub describes the public API of a closed-source module compiled to a
8
+ # native extension (.so / .pyd). The original source is not distributed.
9
+ # Production / commercial use requires a separate commercial license.
10
+ #
11
+ # FINGERPRINT: SMF-DISTRIBUTION-2dd20c74
12
+ # (Unique trace marker. Do not remove or alter — used for plagiarism
13
+ # detection across the public internet.)
14
+ # =============================================================================
15
+
16
+ import numpy as np
17
+ import pandas as pd
18
+ from sklearn.model_selection import train_test_split
19
+ from sklearn.ensemble import HistGradientBoostingClassifier
20
+ import matplotlib.pyplot as plt
21
+ import seaborn as sns
22
+ from sklearn.calibration import CalibratedClassifierCV
23
+
24
+ class DistributionAdaptation:
25
+ def __init__(self, method = 'density_ratio'): ...
26
+ def estimate_density_ratio(self, X_train, X_oot): ...
27
+ def covariate_shift_weighting(self, X_train, X_oot): ...
28
+ def fit(self, X_train, X_oot, y_train = None): ...
29
+ def get_weights(self): ...
30
+ def visualize_distribution_comparison(self, X_train, X_oot, features = None, n_features = 5): ...
@@ -0,0 +1,413 @@
1
+ """
2
+ Reject inference classes for credit modeling.
3
+
4
+ This module provides classes for applying reject inference techniques
5
+ to handle the selection bias in credit modeling when using
6
+ approved loan data only.
7
+
8
+ Classes
9
+ -------
10
+ RejectInferrer : Base class for reject inference.
11
+ RejectInferenceFactory : Factory for creating reject inference methods.
12
+ ParcelingInferrer : Parceling method for reject inference.
13
+ FuzzyAugmentInferrer : Fuzzy augmentation method.
14
+ HardCutoffInferrer : Hard cutoff method.
15
+ SimpleAugmentInferrer : Simple augmentation method.
16
+
17
+ Examples
18
+ --------
19
+ >>> from Modeling_Tool_refactored.sample import RejectInferrer
20
+ >>> inferrer = RejectInferenceFactory.create('parceling')
21
+ >>> df_inferred = inferrer.infer(df_approved, df_rejected, 'score')
22
+ """
23
+
24
+ import pandas as pd
25
+ import numpy as np
26
+ from typing import Union, Optional, List, Dict, Any, Tuple
27
+ from abc import ABC, abstractmethod
28
+
29
+
30
+ class RejectInferrer(ABC):
31
+ """
32
+ Abstract base class for reject inference methods.
33
+
34
+ Reject inference is used to address selection bias when building
35
+ credit models on approved loans only.
36
+
37
+ Parameters
38
+ ----------
39
+ target_col : str, default 'target'
40
+ Name of the target column.
41
+ score_col : str, default 'score'
42
+ Name of the score/probability column.
43
+
44
+ Methods
45
+ -------
46
+ infer(df_approved, df_rejected, score_col)
47
+ Apply reject inference.
48
+ """
49
+
50
+ def __init__(self, target_col: str = 'target',
51
+ score_col: str = 'score'):
52
+ """
53
+ Initialize RejectInferrer.
54
+
55
+ Parameters
56
+ ----------
57
+ target_col : str, default 'target'
58
+ Target column name.
59
+ score_col : str, default 'score'
60
+ Score column name.
61
+ """
62
+ self.target_col = target_col
63
+ self.score_col = score_col
64
+
65
+ @abstractmethod
66
+ def infer(self, df_approved: pd.DataFrame,
67
+ df_rejected: pd.DataFrame,
68
+ score_col: Optional[str] = None) -> pd.DataFrame:
69
+ """
70
+ Apply reject inference.
71
+
72
+ Parameters
73
+ ----------
74
+ df_approved : pandas.DataFrame
75
+ DataFrame with approved applications (has target).
76
+ df_rejected : pandas.DataFrame
77
+ DataFrame with rejected applications (no target).
78
+ score_col : str, optional
79
+ Score column name.
80
+
81
+ Returns
82
+ -------
83
+ pandas.DataFrame
84
+ Combined DataFrame with inferred targets for rejected applications.
85
+ """
86
+ pass
87
+
88
+
89
+ class SimpleAugmentInferrer(RejectInferrer):
90
+ """
91
+ Simple augmentation reject inference method.
92
+
93
+ Assigns the average bad rate from approved applications
94
+ to all rejected applications.
95
+
96
+ Parameters
97
+ ----------
98
+ bad_rate : float, optional
99
+ Override bad rate to use.
100
+
101
+ Examples
102
+ --------
103
+ >>> inferrer = SimpleAugmentInferrer()
104
+ >>> df_combined = inferrer.infer(df_approved, df_rejected)
105
+ """
106
+
107
+ def __init__(self, target_col: str = 'target',
108
+ score_col: str = 'score',
109
+ bad_rate: Optional[float] = None):
110
+ """
111
+ Initialize SimpleAugmentInferrer.
112
+ """
113
+ super().__init__(target_col, score_col)
114
+ self.bad_rate = bad_rate
115
+
116
+ def infer(self, df_approved: pd.DataFrame,
117
+ df_rejected: pd.DataFrame,
118
+ score_col: Optional[str] = None) -> pd.DataFrame:
119
+ """
120
+ Apply simple augmentation.
121
+
122
+ Parameters
123
+ ----------
124
+ df_approved : pandas.DataFrame
125
+ Approved applications.
126
+ df_rejected : pandas.DataFrame
127
+ Rejected applications.
128
+ score_col : str, optional
129
+ Score column.
130
+
131
+ Returns
132
+ -------
133
+ pandas.DataFrame
134
+ Combined data with inferred targets.
135
+ """
136
+ score_col = score_col or self.score_col
137
+
138
+ if self.bad_rate is None:
139
+ bad_rate = df_approved[self.target_col].mean()
140
+ else:
141
+ bad_rate = self.bad_rate
142
+
143
+ np.random.seed(42)
144
+ inferred_target = np.random.binomial(1, bad_rate, len(df_rejected))
145
+
146
+ df_rejected_copy = df_rejected.copy()
147
+ df_rejected_copy[self.target_col] = inferred_target
148
+
149
+ return pd.concat([df_approved, df_rejected_copy], ignore_index=True)
150
+
151
+
152
+ class HardCutoffInferrer(RejectInferrer):
153
+ """
154
+ Hard cutoff reject inference method.
155
+
156
+ Assigns all rejected applications below a score threshold
157
+ as bad (target=1), and all above as good (target=0).
158
+
159
+ Parameters
160
+ ----------
161
+ cutoff : float, default 0.5
162
+ Score cutoff threshold.
163
+
164
+ Examples
165
+ --------
166
+ >>> inferrer = HardCutoffInferrer(cutoff=0.3)
167
+ >>> df_combined = inferrer.infer(df_approved, df_rejected, 'probability')
168
+ """
169
+
170
+ def __init__(self, target_col: str = 'target',
171
+ score_col: str = 'score',
172
+ cutoff: float = 0.5):
173
+ """
174
+ Initialize HardCutoffInferrer.
175
+ """
176
+ super().__init__(target_col, score_col)
177
+ self.cutoff = cutoff
178
+
179
+ def infer(self, df_approved: pd.DataFrame,
180
+ df_rejected: pd.DataFrame,
181
+ score_col: Optional[str] = None) -> pd.DataFrame:
182
+ """
183
+ Apply hard cutoff inference.
184
+
185
+ Parameters
186
+ ----------
187
+ df_approved : pandas.DataFrame
188
+ Approved applications.
189
+ df_rejected : pandas.DataFrame
190
+ Rejected applications.
191
+ score_col : str, optional
192
+ Score column.
193
+
194
+ Returns
195
+ -------
196
+ pandas.DataFrame
197
+ Combined data with inferred targets.
198
+ """
199
+ score_col = score_col or self.score_col
200
+
201
+ df_rejected_copy = df_rejected.copy()
202
+ df_rejected_copy[self.target_col] = (df_rejected_copy[score_col] < self.cutoff).astype(int)
203
+
204
+ return pd.concat([df_approved, df_rejected_copy], ignore_index=True)
205
+
206
+
207
+ class FuzzyAugmentInferrer(RejectInferrer):
208
+ """
209
+ Fuzzy augmentation reject inference method.
210
+
211
+ Weights approved applications based on their predicted probability
212
+ and creates pseudo-target values for rejected applications.
213
+
214
+ Parameters
215
+ ----------
216
+ weight_factor : float, default 1.0
217
+ Factor to adjust weights.
218
+
219
+ Examples
220
+ --------
221
+ >>> inferrer = FuzzyAugmentInferrer(weight_factor=0.9)
222
+ >>> df_combined = inferrer.infer(df_approved, df_rejected, 'probability')
223
+ """
224
+
225
+ def __init__(self, target_col: str = 'target',
226
+ score_col: str = 'score',
227
+ weight_factor: float = 1.0):
228
+ """
229
+ Initialize FuzzyAugmentInferrer.
230
+ """
231
+ super().__init__(target_col, score_col)
232
+ self.weight_factor = weight_factor
233
+
234
+ def infer(self, df_approved: pd.DataFrame,
235
+ df_rejected: pd.DataFrame,
236
+ score_col: Optional[str] = None) -> pd.DataFrame:
237
+ """
238
+ Apply fuzzy augmentation.
239
+
240
+ Parameters
241
+ ----------
242
+ df_approved : pandas.DataFrame
243
+ Approved applications.
244
+ df_rejected : pandas.DataFrame
245
+ Rejected applications.
246
+ score_col : str, optional
247
+ Score column.
248
+
249
+ Returns
250
+ -------
251
+ pandas.DataFrame
252
+ Combined data with inferred targets.
253
+ """
254
+ score_col = score_col or self.score_col
255
+
256
+ df_approved_copy = df_approved.copy()
257
+ df_rejected_copy = df_rejected.copy()
258
+
259
+ proba = df_approved_copy[score_col]
260
+ df_approved_copy['_weight'] = (
261
+ proba * df_approved_copy[self.target_col] +
262
+ (1 - proba) * (1 - df_approved_copy[self.target_col])
263
+ ) * self.weight_factor
264
+
265
+ df_rejected_copy[self.target_col] = (1 - df_rejected_copy[score_col])
266
+ df_rejected_copy['_weight'] = 1.0
267
+
268
+ return pd.concat([df_approved_copy, df_rejected_copy], ignore_index=True)
269
+
270
+
271
+ class ParcelingInferrer(RejectInferrer):
272
+ """
273
+ Parceling reject inference method.
274
+
275
+ Splits rejected applications into parcels based on score bands
276
+ and assigns average bad rate from approved applications in
277
+ each parcel.
278
+
279
+ Parameters
280
+ ----------
281
+ n_parcels : int, default 10
282
+ Number of score parcels.
283
+
284
+ Examples
285
+ --------
286
+ >>> inferrer = ParcelingInferrer(n_parcels=5)
287
+ >>> df_combined = inferrer.infer(df_approved, df_rejected, 'score')
288
+ """
289
+
290
+ def __init__(self, target_col: str = 'target',
291
+ score_col: str = 'score',
292
+ n_parcels: int = 10):
293
+ """
294
+ Initialize ParcelingInferrer.
295
+ """
296
+ super().__init__(target_col, score_col)
297
+ self.n_parcels = n_parcels
298
+ self.parcel_rates_ = None
299
+
300
+ def infer(self, df_approved: pd.DataFrame,
301
+ df_rejected: pd.DataFrame,
302
+ score_col: Optional[str] = None) -> pd.DataFrame:
303
+ """
304
+ Apply parceling inference.
305
+
306
+ Parameters
307
+ ----------
308
+ df_approved : pandas.DataFrame
309
+ Approved applications.
310
+ df_rejected : pandas.DataFrame
311
+ Rejected applications.
312
+ score_col : str, optional
313
+ Score column.
314
+
315
+ Returns
316
+ -------
317
+ pandas.DataFrame
318
+ Combined data with inferred targets.
319
+ """
320
+ score_col = score_col or self.score_col
321
+
322
+ df_approved_copy = df_approved.copy()
323
+ df_rejected_copy = df_rejected.copy()
324
+
325
+ df_approved_copy['_parcel'] = pd.qcut(
326
+ df_approved_copy[score_col],
327
+ q=self.n_parcels,
328
+ labels=False,
329
+ duplicates='drop'
330
+ )
331
+
332
+ parcel_rates = df_approved_copy.groupby('_parcel')[self.target_col].mean()
333
+ self.parcel_rates_ = parcel_rates
334
+
335
+ df_rejected_copy['_parcel'] = pd.cut(
336
+ df_rejected_copy[score_col],
337
+ bins=self.n_parcels,
338
+ labels=False,
339
+ include_lowest=True
340
+ )
341
+
342
+ df_rejected_copy[self.target_col] = df_rejected_copy['_parcel'].map(parcel_rates)
343
+ df_rejected_copy[self.target_col] = df_rejected_copy[self.target_col].fillna(
344
+ df_approved_copy[self.target_col].mean()
345
+ )
346
+
347
+ df_approved_copy = df_approved_copy.drop('_parcel', axis=1)
348
+ df_rejected_copy = df_rejected_copy.drop('_parcel', axis=1)
349
+
350
+ return pd.concat([df_approved_copy, df_rejected_copy], ignore_index=True)
351
+
352
+
353
+ class RejectInferenceFactory:
354
+ """
355
+ Factory class for creating reject inference methods.
356
+
357
+ Examples
358
+ --------
359
+ >>> inferrer = RejectInferenceFactory.create('parceling', n_parcels=5)
360
+ >>> inferrer = RejectInferenceFactory.create('fuzzy', weight_factor=0.9)
361
+ """
362
+
363
+ _methods = {
364
+ 'simple': SimpleAugmentInferrer,
365
+ 'augment': SimpleAugmentInferrer,
366
+ 'hard': HardCutoffInferrer,
367
+ 'hardcutoff': HardCutoffInferrer,
368
+ 'fuzzy': FuzzyAugmentInferrer,
369
+ 'parceling': ParcelingInferrer,
370
+ 'parcel': ParcelingInferrer
371
+ }
372
+
373
+ @classmethod
374
+ def create(cls, method: str = 'parceling', **kwargs) -> RejectInferrer:
375
+ """
376
+ Create a reject inference method.
377
+
378
+ Parameters
379
+ ----------
380
+ method : str, default 'parceling'
381
+ Method name.
382
+ **kwargs
383
+ Additional parameters for the method.
384
+
385
+ Returns
386
+ -------
387
+ RejectInferrer
388
+ Instantiated reject inferrer.
389
+
390
+ Raises
391
+ ------
392
+ ValueError
393
+ If method name is not recognized.
394
+ """
395
+ method_lower = method.lower()
396
+ if method_lower not in cls._methods:
397
+ raise ValueError(
398
+ f"Unknown method '{method}'. "
399
+ f"Available: {list(set(cls._methods.keys()))}"
400
+ )
401
+ return cls._methods[method_lower](**kwargs)
402
+
403
+ @classmethod
404
+ def available_methods(cls) -> List[str]:
405
+ """
406
+ Get list of available methods.
407
+
408
+ Returns
409
+ -------
410
+ list of str
411
+ Available method names.
412
+ """
413
+ return list(set(cls._methods.keys()))