Python基于toad实现生成评分卡 完整的示例代码和数据集
import pandas as pd
from sklearn.metrics import roc_auc_score, roc_curve, auc
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
import numpy as np
import math
import xgboost as xgb
import toad
# 加载数据
data_all = pd.read_csv("scorecard.txt")
# 指定不参与训练列名
ex_lis = ['uid', 'samp_type', 'bad_ind']
# 参与训练列名
ft_lis = list(data_all.columns)
for i in ex_lis:
ft_lis.remove(i)
# 开发样本、验证样本与时间外样本
dev = data_all[(data_all['samp_type'] == 'dev')]
val = data_all[(data_all['samp_type'] == 'val')]
off = data_all[(data_all['samp_type'] == 'off')]
toad.detector.detect(data_all)
dev_slct1, drop_lst = toad.selection.select(dev, dev['bad_ind'],
empty=0.7, iv=0.03,
corr=0.7,
return_drop=True,
exclude=ex_lis)
print("keep:", dev_slct1.shape[1],
"drop empty:", len(drop_lst['empty']),
"drop iv:", len(drop_lst['iv']),
"drop corr:", len(drop_lst['corr']))
# 得到切分节点
combiner = toad.transform.Combiner()
combiner.fit(dev_slct1, dev_slct1['bad_ind'], method='chi',
min_samples=0.05, exclude=ex_lis)
# 导出箱的节点
bins = combiner.export()
print(bins)
# 根据节点实施分箱
dev_slct2 = combiner.transform(dev_slct1)
val2 = combiner.transform(val[dev_slct1.columns])
off2 = combiner.transform(off[dev_slct1.columns])
# 分箱后通过画图观察
from toad.plot import bin_plot, badrate_plot
bin_plot(dev_slct2, x='act_info', target='bad_ind')
bin_plot(val2, x='act_info', target='bad_ind')
bin_plot(off2, x='act_info', target='bad_ind')
print(bins['act_info'])
adj_bin = {'act_info': [0.16666666666666666, 0.35897435897435903, ]}
combiner.set_rules(adj_bin)
dev_slct3 = combiner.transform(dev_slct1)
val3 = combiner.transform(val[dev_slct1.columns])
off3 = combiner.transform(off[dev_slct1.columns])
# 画出Bivar图
bin_plot(dev_slct3, x='act_info', target='bad_ind')
bin_plot(val3, x='act_info', target='bad_ind')
bin_plot(off3, x='act_info', target='bad_ind')
data = pd.concat([dev_slct3, val3, off3], join='inner')
badrate_plot(data, x='samp_type', target='bad_ind', by='act_info')
t = toad.transform.WOETransformer()
dev_slct3_woe = t.fit_transform(dev_slct3, dev_slct3['bad_ind'], exclude=ex_lis)
val_woe = t.transform(val3[dev_slct3.columns])
off_woe = t.transform(off3[dev_slct3.columns])
data = pd.concat([dev_slct3_woe, val_woe, off_woe])
A = toad.metrics.PSI(dev_slct3_woe, val_woe)
psi_df = toad.metrics.PSI(dev_slct3_woe, val_woe).sort_values()
psi_df = psi_df.reset_index()
psi_df = psi_df.rename(columns={'index': 'feature', 0: 'psi'})
psi_013 = list(psi_df[psi_df.psi < 0.13].feature)
for i in ex_lis:
if i in psi_013:
pass
else:
psi_013.append(i)
data = data[psi_013]
dev_woe_psi = dev_slct3_woe[psi_013]
val_woe_psi = val_woe[psi_013]
off_woe_psi = off_woe[psi_013]
print(data.shape)
dev_woe_psi2, drop_lst = toad.selection.select(dev_woe_psi,
dev_woe_psi['bad_ind'],
empty=0.6,
iv=0.001,
corr=0.5,
return_drop=True,
exclude=ex_lis)
print("keep:", dev_woe_psi2.shape[1],
"drop empty:", len(drop_lst['empty']),
"drop iv:", len(drop_lst['iv']),
"drop corr:", len(drop_lst['corr']))
dev_woe_psi_stp = toad.selection.stepwise(dev_woe_psi2,
dev_woe_psi2['bad_ind'],
exclude=ex_lis,
direction='both',
criterion='aic',
estimator='ols',
intercept=False)
val_woe_psi_stp = val_woe_psi[dev_woe_psi_stp.columns]
off_woe_psi_stp = off_woe_psi[dev_woe_psi_stp.columns]
data = pd.concat([dev_woe_psi_stp, val_woe_psi_stp, off_woe_psi_stp])
print(data.shape)
def lr_model(x, y, valx, valy, offx, offy, C):
model = LogisticRegression(C=C, class_weight='balanced')
model.fit(x, y)
y_pred = model.predict_proba(x)[:, 1]
fpr_dev, tpr_dev, _ = roc_curve(y, y_pred)
train_ks = abs(fpr_dev - tpr_dev).max()
print('train_ks : ', train_ks)
y_pred = model.predict_proba(valx)[:, 1]
fpr_val, tpr_val, _ = roc_curve(valy, y_pred)
val_ks = abs(fpr_val - tpr_val).max()
print('val_ks : ', val_ks)
y_pred = model.predict_proba(offx)[:, 1]
fpr_off, tpr_off, _ = roc_curve(offy, y_pred)
off_ks = abs(fpr_off - tpr_off).max()
print('off_ks : ', off_ks)
from matplotlib import pyplot as plt
plt.plot(fpr_dev, tpr_dev, label='dev')
plt.plot(fpr_val, tpr_val, label='val')
plt.plot(fpr_off, tpr_off, label='off')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc='best')
plt.show()
def xgb_model(x, y, valx, valy, offx, offy):
model = xgb.XGBClassifier(learning_rate=0.05,
n_estimators=400,
max_depth=2,
class_weight='balanced',
min_child_weight=1,
subsample=1,
nthread=-1,
scale_pos_weight=1,
random_state=1,
n_jobs=-1,
reg_lambda=300)
model.fit(x, y)
y_pred = model.predict_proba(x)[:, 1]
fpr_dev, tpr_dev, _ = roc_curve(y, y_pred)
train_ks = abs(fpr_dev - tpr_dev).max()
print('train_ks : ', train_ks)
y_pred = model.predict_proba(valx)[:, 1]
fpr_val, tpr_val, _ = roc_curve(valy, y_pred)
val_ks = abs(fpr_val - tpr_val).max()
print('val_ks : ', val_ks)
y_pred = model.predict_proba(offx)[:, 1]
fpr_off, tpr_off, _ = roc_curve(offy, y_pred)
off_ks = abs(fpr_off - tpr_off).max()
print('off_ks : ', off_ks)
from matplotlib import pyplot as plt
plt.plot(fpr_dev, tpr_dev, label='dev')
plt.plot(fpr_val, tpr_val, label='val')
plt.plot(fpr_off, tpr_off, label='off')
plt.plot([0, 1], [0, 1], 'k--')
plt.xlabel('False positive rate')
plt.ylabel('True positive rate')
plt.title('ROC Curve')
plt.legend(loc='best')
plt.show()
def bi_train(data, dep='bad_ind', exclude=None):
from sklearn.preprocessing import StandardScaler
std_scaler = StandardScaler()
# 变量名
lis = list(data.columns)
for i in exclude:
lis.remove(i)
data[lis] = std_scaler.fit_transform(data[lis])
devv = data[(data['samp_type'] == 'dev')]
vall = data[(data['samp_type'] == 'val')]
offf = data[(data['samp_type'] == 'off')]
x, y = devv[lis], devv[dep]
valx, valy = vall[lis], vall[dep]
offx, offy = offf[lis], offf[dep]
# 逻辑回归正向
print("逻辑回归正向:")
lr_model(x, y, valx, valy, offx, offy, 0.1)
# 逻辑回归反向
print("逻辑回归反向:")
lr_model(offx, offy, valx, valy, x, y, 0.1)
# XGBoost正向
print("XGBoost正向:")
xgb_model(x, y, valx, valy, offx, offy)
# XGBoost反向
print("XGBoost反向:")
xgb_model(offx, offy, valx, valy, x, y)
bi_train(data, dep='bad_ind', exclude=ex_lis)
dep = 'bad_ind'
lis = list(data.columns)
for i in ex_lis:
lis.remove(i)
devv = data[data['samp_type'] == 'dev']
vall = data[data['samp_type'] == 'val']
offf = data[data['samp_type'] == 'off']
x, y = devv[lis], devv[dep]
valx, valy = vall[lis], vall[dep]
offx, offy = offf[lis], offf[dep]
lr = LogisticRegression()
lr.fit(x, y)
from toad.metrics import KS, F1, AUC
prob_dev = lr.predict_proba(x)[:, 1]
print('训练集')
print('F1:', F1(prob_dev, y))
print('KS:', KS(prob_dev, y))
print('AUC:', AUC(prob_dev, y))
prob_val = lr.predict_proba(valx)[:, 1]
print('跨时间')
print('F1:', F1(prob_val, valy))
print('KS:', KS(prob_val, valy))
print('AUC:', AUC(prob_val, valy))
prob_off = lr.predict_proba(offx)[:, 1]
print('跨时间')
print('F1:', F1(prob_off, offy))
print('KS:', KS(prob_off, offy))
print('AUC:', AUC(prob_off, offy))
print('模型PSI:', toad.metrics.PSI(prob_dev, prob_off))
print('特征PSI:', '\n', toad.metrics.PSI(x, offx).sort_values())
toad.metrics.KS_bucket(prob_off, offy,
bucket=10,
method='quantile')
from toad.scorecard import ScoreCard
card = ScoreCard(combiner=combiner,
transer=t, C=0.1,
class_weight='balanced',
base_score=600,
base_odds=35,
pdo=60,
rate=2)
card.fit(x, y)
final_card = card.export(to_frame=True)
print(final_card)
下面让我们通过这段完整的示例代码,详细解读其流程与要点,构建并评估一个基于 toad 库的信用评分卡(ScoreCard)。示例中演示了特征选择、分箱、WOE 转换、模型评估以及评分卡生成等常见步骤。
1. 代码整体结构与功能
-
数据加载与准备
- 从
scorecard.txt
文件中读取数据到data_all
。 - 指定部分列(如
uid
,samp_type
,bad_ind
)不参与训练,并将剩余列作为特征列ft_lis
。 - 根据
samp_type
划分开发样本(dev)、验证样本(val)和时间外样本(off)。 -
缺陷检测与特征初步筛选
toad.detector.detect(data_all)
检测数据中的异常值、缺失值等情况。toad.selection.select()
进行空值比例、IV 值、相关性等规则的特征筛选。-
分箱
- 初始化
Combiner
,使用卡方(chi
)方法进行分箱。可指定min_samples=0.05
,意味着每个分箱至少包含 5% 的数据。 combiner.export()
可导出分箱节点信息;然后通过combiner.transform()
将分箱规则应用于 dev、val、off 三个样本。- 使用
bin_plot
、badrate_plot
等函数可视化分箱结果、坏账率分布。 -
分箱规则手动调整
- 将导出的分箱节点进行手动合并或切割(存入
adj_bin
),再用combiner.set_rules()
更新规则,以符合业务或统计需求。 - 再次可视化分箱结果,确认效果。
-
WOE 转换
- 使用
WOETransformer
拟合并转换分箱后的数据,将特征映射为相应的 WOE 值,便于后续模型训练和可解释性分析。 -
PSI 检测与再次特征筛选
PSI
(Population Stability Index)衡量不同样本(如 dev 与 val)之间的特征分布变化。- 剔除 PSI 过高(如大于 0.13)的特征,保留 PSI 变化较小的特征。
- 再次用
toad.selection.select()
进行 IV、相关性等筛选。 - 使用
stepwise
(逐步回归)对特征进一步精简。 -
模型训练与评估
- 定义
lr_model
、xgb_model
等方法分别测试逻辑回归和 XGBoost 并绘制 ROC 曲线,观察在开发集(dev)、验证集(val)及时间外(off)上的表现。 - 通过
bi_train
做一次特征标准化(仅演示)并查看正向、反向训练的效果。 - 最后训练逻辑回归模型并输出 F1、KS、AUC 等指标,并计算特征 PSI、模型 PSI。
-
评分卡生成
- 使用
ScoreCard
类,结合combiner
与WOETransformer
,并传入一些核心参数(pdo
,base_odds
,base_score
, etc.)构建信用评分卡。 card.export(to_frame=True)
可导出最终评分卡信息,查看每个特征的分数映射。
2. 关键流程与要点
2.1 样本划分
dev = data_all[data_all['samp_type'] == 'dev']
val = data_all[data_all['samp_type'] == 'val']
off = data_all[data_all['samp_type'] == 'off']
2.2 特征初筛(空值、IV、相关性)
dev_slct1, drop_lst = toad.selection.select(
dev, dev['bad_ind'],
empty=0.7, iv=0.03, corr=0.7,
return_drop=True, exclude=ex_lis
)
uid
、samp_type
、bad_ind
。2.3 分箱(Combiner)
combiner = toad.transform.Combiner()
combiner.fit(dev_slct1, dev_slct1['bad_ind'], method='chi',
min_samples=0.05, exclude=ex_lis)
...
dev_slct2 = combiner.transform(dev_slct1)
val2 = combiner.transform(val[dev_slct1.columns])
off2 = combiner.transform(off[dev_slct1.columns])
[0,0,1,2, ...]
)。2.4 分箱可视化(bin_plot / badrate_plot)
bin_plot(dev_slct2, x='act_info', target='bad_ind')
...
badrate_plot(data, x='samp_type', target='bad_ind', by='act_info')
bin_plot
:可视化单个特征的每个分箱占比及坏账率。badrate_plot
:可视化坏账率在不同分箱、不同样本类型下的对比。2.5 调整分箱
adj_bin = {'act_info': [0.16666666666666666, 0.35897435897435903, ]}
combiner.set_rules(adj_bin)
...
dev_slct3 = combiner.transform(dev_slct1)
val3 = combiner.transform(val[dev_slct1.columns])
off3 = combiner.transform(off[dev_slct1.columns])
act_info
,以满足业务或统计需求。transform
并可视化确认效果。2.6 WOE 转换
t = toad.transform.WOETransformer()
dev_slct3_woe = t.fit_transform(dev_slct3, dev_slct3['bad_ind'], exclude=ex_lis)
val_woe = t.transform(val3[dev_slct3.columns])
off_woe = t.transform(off3[dev_slct3.columns])
WOETransformer
根据分箱后数据和目标变量拟合 WOE,并将特征转换为 WOE 值。exclude=ex_lis
确保不转换 uid
, samp_type
, bad_ind
等列。2.7 PSI 计算与特征再次过滤
psi_df = toad.metrics.PSI(dev_slct3_woe, val_woe).sort_values()
...
psi_013 = list(psi_df[psi_df.psi < 0.13].feature)
2.8 进一步的特征筛选 & Stepwise
dev_woe_psi2, drop_lst = toad.selection.select(
dev_woe_psi, dev_woe_psi['bad_ind'],
empty=0.6, iv=0.001, corr=0.5,
...
)
dev_woe_psi_stp = toad.selection.stepwise(
dev_woe_psi2, dev_woe_psi2['bad_ind'],
exclude=ex_lis, direction='both',
criterion='aic', estimator='ols', intercept=False
)
'both'
方向、'aic'
标准、'ols'
估计器来精简特征。2.9 模型训练与评估
def lr_model(x, y, valx, valy, offx, offy, C):
model = LogisticRegression(C=C, class_weight='balanced')
model.fit(x, y)
...
lr_model
/ xgb_model
分别训练逻辑回归和 XGBoost,并绘制 ROC 曲线。2.10 最终评分卡(ScoreCard)
from toad.scorecard import ScoreCard
card = ScoreCard(
combiner=combiner,
transer=t,
C=0.1,
class_weight='balanced',
base_score=600,
base_odds=35,
pdo=60,
rate=2
)
card.fit(x, y)
final_card = card.export(to_frame=True)
print(final_card)
- 初始化:将先前的
combiner
与WOETransformer
传入,指定pdo
,base_odds
,base_score
,rate
等评分体系参数。 - fit:在最后保留的特征上训练逻辑回归,并同时将结果映射为 ScoreCard 规则。
- export:将评分卡导出为易读的 DataFrame,查看每个特征在不同分箱下对应的分数区间。
3. 可以收获什么
通过这份示例,可以学习到:
- 多阶段特征筛选:空值比、IV、相关性、PSI、stepwise 等多重维度地删选特征,逐步保留最优特征。
- 分箱与可视化:使用
toad.transform.Combiner
自动(或手动微调)分箱,并通过bin_plot
、badrate_plot
检验分箱合理性。 - WOE 转换与信息值:应用
WOETransformer
将分箱映射为 WOE,增强逻辑回归模型对变量的区分能力。 - PSI 与稳定性检查:对特征或模型预测值进行 PSI 测试,排查不稳定特征或漂移风险。
- 模型训练与评估:在开发集、验证集、时间外集上观测 AUC、KS、F1 等指标,保证模型稳健。
- 构建评分卡:利用
toad.scorecard.ScoreCard
将最终模型转化为可落地、可解释、可打分的“评分卡”,并可把概率与分值互相转换。
4. 小结
该示例代码贯穿了信用评分模型开发的完整流程,包括:
- 特征工程(数据清洗、空值/IV/相关性筛选、PSI、stepwise)。
- 分箱与WOE(Combiner + WOETransformer)。
- 逻辑回归/XGBoost 对比及评估(KS、AUC、F1、模型正反向训练)。
- 评分卡(ScoreCard)建立与可视化导出。
这对于从事金融风控、信用评分卡开发或数据分析的从业者是非常典型且实用的工作流程示例,能在实际项目中作参考或直接复用。
如果想进一步深度学习如何个性化评分卡体系,可以着重研究以下几点:
pdo
, base_odds
, base_score
, rate
等核心参数,使评分区间、翻倍 odds 的步长等符合业务要求;作者:彬彬侠