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.
- ExcelMaster/ExcelFormatTool.py +487 -0
- ExcelMaster/ExcelMaster.py +917 -0
- ExcelMaster/Template.py +525 -0
- ExcelMaster/Utility.py +233 -0
- ExcelMaster/__init__.py +3 -0
- Modeling_Tool/Core/Binning_Tool.py +1608 -0
- Modeling_Tool/Core/Binning_Tool.pyi +48 -0
- Modeling_Tool/Core/Check_DuckDB_Compatibility.py +835 -0
- Modeling_Tool/Core/Json_Data_Converter.py +621 -0
- Modeling_Tool/Core/Model_Registry_Tool.py +199 -0
- Modeling_Tool/Core/ODPS_Tool.py +284 -0
- Modeling_Tool/Core/Slope_Tool.py +356 -0
- Modeling_Tool/Core/Slope_Tool.pyi +31 -0
- Modeling_Tool/Core/XOR_Encryptor.py +207 -0
- Modeling_Tool/Core/XOR_Encryptor.pyi +26 -0
- Modeling_Tool/Core/__init__.py +99 -0
- Modeling_Tool/Core/kDataFrame.py +228 -0
- Modeling_Tool/Core/kDataFrame.pyi +43 -0
- Modeling_Tool/Core/sample_weight_utils.py +77 -0
- Modeling_Tool/Core/utils.py +2672 -0
- Modeling_Tool/Eval/Evaluation_Tool.py +1452 -0
- Modeling_Tool/Eval/Evaluation_Tool.pyi +47 -0
- Modeling_Tool/Eval/Model_Eval_Tool.py +2548 -0
- Modeling_Tool/Eval/Model_Eval_Tool.pyi +37 -0
- Modeling_Tool/Eval/__init__.py +54 -0
- Modeling_Tool/Eval/evaluate_model.py +2008 -0
- Modeling_Tool/Eval/evaluate_model.pyi +50 -0
- Modeling_Tool/Eval/weighted_eval_utils.py +326 -0
- Modeling_Tool/Explainability/Coalition_Structure.py +305 -0
- Modeling_Tool/Explainability/Model_Explainer.py +743 -0
- Modeling_Tool/Explainability/__init__.py +24 -0
- Modeling_Tool/Feature/Distribution_Tool.py +509 -0
- Modeling_Tool/Feature/Distribution_Tool.pyi +42 -0
- Modeling_Tool/Feature/Feature_Insights.py +762 -0
- Modeling_Tool/Feature/Feature_Insights.pyi +33 -0
- Modeling_Tool/Feature/PSI_Tool.py +1195 -0
- Modeling_Tool/Feature/PSI_Tool.pyi +29 -0
- Modeling_Tool/Feature/WOE_Engine_Feature_Patch.py +355 -0
- Modeling_Tool/Feature/__init__.py +40 -0
- Modeling_Tool/Model/Backward_Tool.py +778 -0
- Modeling_Tool/Model/Backward_Tool.pyi +45 -0
- Modeling_Tool/Model/GBM_Search_Tool.py +251 -0
- Modeling_Tool/Model/GBM_Tool.py +1610 -0
- Modeling_Tool/Model/GBM_Tool.pyi +90 -0
- Modeling_Tool/Model/LRM_Tool.py +1198 -0
- Modeling_Tool/Model/LRM_Tool.pyi +47 -0
- Modeling_Tool/Model/__init__.py +61 -0
- Modeling_Tool/Sample/Distribution_Adaptation.py +131 -0
- Modeling_Tool/Sample/Distribution_Adaptation.pyi +30 -0
- Modeling_Tool/Sample/Reject_Infer.py +413 -0
- Modeling_Tool/Sample/Reject_Infer.pyi +43 -0
- Modeling_Tool/Sample/Sample_Split.py +520 -0
- Modeling_Tool/Sample/Sample_Split.pyi +43 -0
- Modeling_Tool/Sample/__init__.py +31 -0
- Modeling_Tool/UAT/UAT_Consistency_Checker.py +1180 -0
- Modeling_Tool/UAT/__init__.py +19 -0
- Modeling_Tool/WOE/WOE_Adapter.py +204 -0
- Modeling_Tool/WOE/WOE_Adapter.pyi +21 -0
- Modeling_Tool/WOE/WOE_Master.py +491 -0
- Modeling_Tool/WOE/WOE_Master.pyi +40 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.py +3324 -0
- Modeling_Tool/WOE/WOE_Monotone_Binner.pyi +71 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.py +919 -0
- Modeling_Tool/WOE/WOE_Plot_Tool.pyi +46 -0
- Modeling_Tool/WOE/WOE_Report_Builder.py +214 -0
- Modeling_Tool/WOE/WOE_Report_Builder.pyi +24 -0
- Modeling_Tool/WOE/WOE_Tool.py +1094 -0
- Modeling_Tool/WOE/WOE_Tool.pyi +41 -0
- Modeling_Tool/WOE/__init__.py +86 -0
- Modeling_Tool/WOE/plot_woe_tool.py +290 -0
- Modeling_Tool/WOE/plot_woe_tool.pyi +25 -0
- Modeling_Tool/__init__.py +176 -0
- Report/Report_Tool.py +380 -0
- Report/__init__.py +3 -0
- supermodelingfactory-0.2.0.dist-info/METADATA +268 -0
- supermodelingfactory-0.2.0.dist-info/RECORD +79 -0
- supermodelingfactory-0.2.0.dist-info/WHEEL +5 -0
- supermodelingfactory-0.2.0.dist-info/licenses/LICENSE +102 -0
- 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()))
|