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. 代码整体结构与功能

  1. 数据加载与准备

  2. scorecard.txt 文件中读取数据到 data_all
  3. 指定部分列(如 uid, samp_type, bad_ind)不参与训练,并将剩余列作为特征列 ft_lis
  4. 根据 samp_type 划分开发样本(dev)、验证样本(val)和时间外样本(off)。
  5. 缺陷检测与特征初步筛选

  6. toad.detector.detect(data_all) 检测数据中的异常值、缺失值等情况。
  7. toad.selection.select() 进行空值比例、IV 值、相关性等规则的特征筛选。
  8. 分箱

  9. 初始化 Combiner,使用卡方(chi)方法进行分箱。可指定 min_samples=0.05,意味着每个分箱至少包含 5% 的数据。
  10. combiner.export() 可导出分箱节点信息;然后通过 combiner.transform() 将分箱规则应用于 dev、val、off 三个样本。
  11. 使用 bin_plotbadrate_plot 等函数可视化分箱结果、坏账率分布。
  12. 分箱规则手动调整

  13. 将导出的分箱节点进行手动合并或切割(存入 adj_bin),再用 combiner.set_rules() 更新规则,以符合业务或统计需求。
  14. 再次可视化分箱结果,确认效果。
  15. WOE 转换

  16. 使用 WOETransformer 拟合并转换分箱后的数据,将特征映射为相应的 WOE 值,便于后续模型训练和可解释性分析。
  17. PSI 检测与再次特征筛选

  18. PSI(Population Stability Index)衡量不同样本(如 dev 与 val)之间的特征分布变化。
  19. 剔除 PSI 过高(如大于 0.13)的特征,保留 PSI 变化较小的特征。
  20. 再次用 toad.selection.select() 进行 IV、相关性等筛选。
  21. 使用 stepwise(逐步回归)对特征进一步精简。
  22. 模型训练与评估

  23. 定义 lr_modelxgb_model 等方法分别测试逻辑回归和 XGBoost 并绘制 ROC 曲线,观察在开发集(dev)、验证集(val)及时间外(off)上的表现。
  24. 通过 bi_train 做一次特征标准化(仅演示)并查看正向、反向训练的效果。
  25. 最后训练逻辑回归模型并输出 F1、KS、AUC 等指标,并计算特征 PSI、模型 PSI。
  26. 评分卡生成

  27. 使用 ScoreCard 类,结合 combinerWOETransformer,并传入一些核心参数(pdo, base_odds, base_score, etc.)构建信用评分卡。
  28. 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']
  • dev: 开发样本(训练集),用于特征工程和建模。
  • val: 验证样本,评估模型在相同时间/同分布下的泛化能力。
  • 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
    )
    
  • empty=0.7: 去除空值比例大于 70% 的特征。
  • iv=0.03: 剔除信息值(IV)低于 0.03 的特征,通常视为区分度不足。
  • corr=0.7: 剔除与已保留特征相关性大于 0.7 的特征,只保留 IV 更高的一个。
  • exclude=ex_lis: 保留不参与训练列于最终数据集中(或至少不删除它们),如 uidsamp_typebad_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])
    
  • method=‘chi’: 卡方分箱(基于目标变量分布,自动寻找最优分箱)。
  • min_samples=0.05: 单箱最少样本量比例 5%。
  • transform: 将分箱规则应用于 dev、val、off 样本,得到分箱后的离散特征(如 [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)
    
  • PSI(Population Stability Index)测量 dev 与 val 样本在同一特征上的分布差异。
  • 将 PSI >= 0.13 的特征视为分布变化较大,可能不稳定,需要剔除。
  • 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
    )
    
  • 进一步对 PSI 筛过的特征做空值、IV、相关性过滤;并调用 stepwise(逐步回归)用 '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 曲线。
  • 在 dev / val / off 上观测 KS、AUC 等指标,检验模型泛化和时间稳定性。
  • 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)
    
    1. 初始化:将先前的 combinerWOETransformer 传入,指定 pdo, base_odds, base_score, rate 等评分体系参数。
    2. fit:在最后保留的特征上训练逻辑回归,并同时将结果映射为 ScoreCard 规则。
    3. export:将评分卡导出为易读的 DataFrame,查看每个特征在不同分箱下对应的分数区间。

    3. 可以收获什么

    通过这份示例,可以学习到:

    1. 多阶段特征筛选:空值比、IV、相关性、PSI、stepwise 等多重维度地删选特征,逐步保留最优特征。
    2. 分箱与可视化:使用 toad.transform.Combiner 自动(或手动微调)分箱,并通过 bin_plotbadrate_plot 检验分箱合理性。
    3. WOE 转换与信息值:应用 WOETransformer 将分箱映射为 WOE,增强逻辑回归模型对变量的区分能力。
    4. PSI 与稳定性检查:对特征或模型预测值进行 PSI 测试,排查不稳定特征或漂移风险。
    5. 模型训练与评估:在开发集、验证集、时间外集上观测 AUC、KS、F1 等指标,保证模型稳健。
    6. 构建评分卡:利用 toad.scorecard.ScoreCard 将最终模型转化为可落地、可解释、可打分的“评分卡”,并可把概率与分值互相转换。

    4. 小结

    该示例代码贯穿了信用评分模型开发的完整流程,包括:

    1. 特征工程(数据清洗、空值/IV/相关性筛选、PSI、stepwise)。
    2. 分箱与WOE(Combiner + WOETransformer)。
    3. 逻辑回归/XGBoost 对比及评估(KS、AUC、F1、模型正反向训练)。
    4. 评分卡(ScoreCard)建立与可视化导出。

    这对于从事金融风控信用评分卡开发数据分析的从业者是非常典型且实用的工作流程示例,能在实际项目中作参考或直接复用。
    如果想进一步深度学习如何个性化评分卡体系,可以着重研究以下几点:

  • 调整 pdo, base_odds, base_score, rate 等核心参数,使评分区间、翻倍 odds 的步长等符合业务要求;
  • 在业务上精细化手动合并分箱,结合专家知识修正分箱后高坏账率区间的界限;
  • 对最终评分卡在真实线上数据进行后评估与监控,确保稳定性与可解释性。
  • 作者:彬彬侠

    物联沃分享整理
    物联沃-IOTWORD物联网 » Python基于toad实现生成评分卡 完整的示例代码和数据集

    发表回复