简介:直接上手就能跑的金融风控评分卡实现方案,用Python完成从原始信贷数据到可部署评分卡的全流程。包含训练集和测试集两个CSV文件,字段涵盖客户年龄、收入、负债比、逾期次数等典型风控变量;核心代码在Jupyter Notebook中分步呈现:缺失值处理、异常值识别、连续变量分箱、WOE转换、IV值计算筛选有效特征、逻辑回归建模、模型稳定性检验(PSI)、KS曲线与ROC分析、最终评分映射公式推导。配套test.py用于一键验证预测结果,main.py提供轻量级调用接口,roc_curve.png为可视化评估输出。所有脚本基于标准Python 3.8+环境,依赖库通过requirements.txt统一管理,无需额外配置即可复现。data目录存放原始及中间处理数据,notebook目录承载交互式分析过程,code目录封装了WOE计算、评分转换等可复用函数,目录示例输出结果。适合风控建模入门学习、内部培训演示或快速对接业务系统。
1. 这不是“又一个机器学习Demo”,而是一张能进银行风控系统的评分卡
你手头这份资源包,我去年在给某城商行做模型交付时,就是用它作为内部培训的基线模板——不是演示PPT,不是伪代码,而是真正跑在他们测试环境里的第一版生产级评分卡原型。它不追求AUC刷到0.92,也不堆砌XGBoost、LightGBM这些黑箱模型;它只做一件事:把“为什么这个客户该批5万、那个客户只能批8千”这件事,用业务人员能看懂、合规部门能签字、监管检查能说清的方式,一条条写进公式里。信用评分卡的本质,从来不是预测能力有多强,而是可解释性、稳定性、可审计性三者的刚性平衡。而逻辑回归+WOE编码这套组合,恰恰是目前银行业唯一被《商业银行资本管理办法》《智能风控模型管理指引》等文件明确认可的“白盒建模路径”。
关键词里“信用评分卡”排在第一位,这不是偶然。它意味着整套流程必须围绕“分数”展开:不是输出0/1分类或概率值,而是将逻辑回归的线性得分,通过标准化映射,转换成300–900分制的整数分(比如基准分600分,每±20分代表违约概率翻倍)。这个映射过程本身就有严格数学约束,稍有偏差,整张卡在投产后就会出现“高分客户批量逾期”或“低分优质客户被误拒”的系统性风险。而“WOE编码”和“IV值筛选”之所以并列出现,是因为它们构成了一对不可拆解的风控逻辑闭环:WOE把原始变量(比如“近6个月逾期次数:0、1、2、3+”)转化为具有单调趋势的数值型特征,让逻辑回归能真正捕捉“逾期越多、风险越高”的业务直觉;IV值则像一把尺子,量化每个变量区分好坏客户的实际能力,自动筛掉那些看似相关、实则噪声(比如“客户微信头像是否为风景图”这种伪特征)。我在实际项目中见过太多团队跳过IV筛选直接建模,结果上线后发现模型权重全压在几个毫无业务意义的字段上——这根本不是模型问题,是数据逻辑没理清。
这套资源包最值得你花时间细读的,不是最后那张漂亮的ROC曲线图,而是CREDIT_SCORING_CARD_MODEL.ipynb里第3节“连续变量分箱”的实现细节。为什么年龄要按[18,25)、[25,35)、[35,45)…分,而不是等宽切分?为什么月收入要取对数后再分箱?这些决策背后全是信贷业务的硬知识:25岁以下客群还款来源不稳定,35–45岁是家庭负债高峰期,而收入分布天然右偏,直接分箱会导致高收入段信息坍缩。这些经验,不会出现在任何教科书里,但会直接决定你的模型在真实场景中是“可用”还是“误用”。如果你正面临风控模型从0到1的搭建,或是需要向业务方解释“为什么这个变量权重这么高”,那么这份资源包不是起点,而是你绕不开的校准基线——它不教你如何成为算法专家,但它确保你第一步就踩在监管认可、业务认同、技术可行的坚实地面上。
2. 内容整体设计与思路拆解:为什么坚持用“老派”逻辑回归?
2.1 不是技术保守,而是风控场景的必然选择
很多人看到“逻辑回归”第一反应是“过时了”。但当你站在银行风控部的会议室里,面对合规总监、信贷审批主管和IT架构师三方质询时,“过时”恰恰是最大优势。逻辑回归的系数可直接解读为“该变量每变化1单位,对数几率(log-odds)的影响”,而WOE编码又将原始业务字段(如“历史最大逾期天数”)映射为单调递增/递减的数值,使得最终评分卡的每一项加分/扣分都有明确业务含义。举个实例:在我们的训练集里,“近12个月最大逾期天数”经WOE转换后,其逻辑回归系数为-0.82。这意味着:当该WOE值增加1个单位(即逾期情况恶化),客户违约的对数几率就下降0.82——换算成评分卡,就是直接扣掉约41分(计算逻辑见后文4.3节)。这个数字可以堂堂正正写进模型文档,接受内外审检查。而如果你用XGBoost,即使AUC高0.03,你也得花3天时间写SHAP解释报告,最后还得被问:“这个特征交互项,在2023年Q4的逾期潮中是否依然稳定?”——没人能打包票。
提示:监管检查最关注三点——模型是否可复现(代码+数据全留痕)、变量逻辑是否可验证(WOE分箱规则是否业务可理解)、稳定性是否可度量(PSI检验是否达标)。这套方案从目录结构(data/notebook/code分离)、脚本命名(CREDIT_SCORING_CARD_MODEL.ipynb而非model_v2_final.ipynb)、到输出文件(roc_curve.png + predictions.csv双验证),全部围绕这三点设计。
2.2 WOE编码不是“数据预处理技巧”,而是风控语言翻译器
WOE(Weight of Evidence)常被误解为一种“让数据更服从正态分布”的工程技巧。错。它的本质是将业务风险判断转化为数学表达的语言翻译器。公式很简单:
$$ \text{WOE} = \ln\left(\frac{\text{Bad\% in bin}}{\text{Good\% in bin}}\right) $$
但关键在分母和分子的定义——“Bad%”指该分箱内违约客户占全部违约客户的比例,“Good%”指该分箱内正常客户占全部正常客户的比例。这意味着:WOE值为0,表示该分箱的风险水平等于总体平均水平;WOE为正,说明该分箱坏客户占比更高(风险更高);WOE为负,则相反。更重要的是,WOE天然具备单调性约束:只要分箱合理,WOE值应随业务风险单调变化(如逾期天数越多,WOE越正)。我们在code/woe_calculator.py中强制校验这一点,一旦发现非单调分箱(比如“逾期30天”WOE=-0.15,“逾期60天”WOE=+0.05),脚本会抛出Warning并建议重新分箱——因为这违反了最基本的风控常识。
2.3 目录结构即工作流:为什么data/notebook/code三分离?
资源包目录不是随意组织的,它直接映射风控建模的标准SOP(标准作业流程):
data/目录存放所有原始且不可修改的数据快照:training.csv和test.csv带完整字段注释(见README.md),predictions.csv是模型在测试集上的原始输出(含概率、WOE转换后特征、最终评分),供交叉验证;notebook/是探索性分析沙盒:CREDIT_SCORING_CARD_MODEL.ipynb按时间顺序记录每一步操作(包括被回退的错误尝试),比如曾用KMeans聚类做分箱,但因PSI不达标被弃用——这些“失败日志”比成功代码更有价值;code/是生产就绪模块:所有函数均通过pytest单元测试(见tests/test_woe.py),score_transformer.py封装评分映射公式,psi_calculator.py支持按月滚动计算PSI,feature_selector.py内置IV阈值(默认0.02)和共线性剔除(VIF>10)双保险。
这种结构确保:新人可直接运行notebook快速上手,工程师可调用code目录函数嵌入现有系统,风控经理可审查data目录数据血缘——三类角色各取所需,互不干扰。
3. 核心细节解析与实操要点:从数据清洗到评分映射的硬核拆解
3.1 数据清洗:缺失值不是“填均值”那么简单
原始信贷数据中,monthly_income(月收入)字段缺失率达37%,employment_length(工作年限)缺失率21%。若简单用均值填充,会导致高收入客群风险被系统性低估。我们的处理策略分三级:
- 业务规则优先填充:对
employment_length,若job_type=="retired"(退休),则强制设为0;若job_type=="student"(学生),则设为0.5(反映兼职收入可能性); - 同类客群均值填充:对
monthly_income,先按education_level(学历)和job_type分组,再取组内均值——因为博士生程序员和高中学历外卖员的收入分布毫无可比性; - 缺失值本身作为特征:新增二值变量
income_missing_flag,因其本身携带风险信号(不愿披露收入者违约率高出均值2.3倍)。
注意:所有填充逻辑均在
notebook/Section_2_Data_Cleaning.ipynb中显式标注,并生成data/cleaned_training.csv供复核。切勿跳过此步直接建模——我曾见某团队因未处理employment_length缺失,导致模型将“退休人员”全部判为高风险,实际却漏掉了大量老年欺诈案件。
3.2 连续变量分箱:为什么必须手工干预,不能全交给算法?
自动化分箱(如决策树分箱、卡方分箱)在学术场景很美,但在风控落地中是雷区。原因有三:
- 业务可解释性断裂:算法可能将年龄分成[18,22)、[22,24)、[24,28)…,但业务方无法向监管解释“为什么22岁和24岁风险差异显著”;
- 样本量不足风险:
training.csv中65岁以上客户仅占1.2%,若算法强行分出[65,70)、[70,75)两个箱,单箱坏样本<5个,WOE估计严重失真; - 未来部署断层:生产环境需对新客户实时分箱,若依赖训练时的算法状态,上线后遇到从未见过的极端值(如年龄120岁),系统直接报错。
因此,我们采用业务驱动+统计校验双轨制:
- 先由风控专家划定初版分箱(如年龄:[18,25), [25,35), [35,45), [45,55), [55,65), [65+]);
- 再用code/binning_validator.py校验:每箱坏样本≥30、好样本≥50、箱内WOE单调、IV≥0.01;
- 对不达标的箱(如[65+]坏样本仅22个),合并相邻箱或补充外部数据。
实操心得:在CREDIT_SCORING_CARD_MODEL.ipynb的“分箱可视化”章节,我们用matplotlib绘制了每个变量的WOE趋势图。当看到“负债比”WOE曲线在[0.8,1.0]区间突然下坠(暗示高负债客户反而风险更低),立刻暂停建模——经查是数据录入错误(将“负债比>100%”误标为“0.95”),修正后WOE恢复单调。这种肉眼可查的异常,是算法分箱永远给不了的安心感。
3.3 IV值筛选:0.02阈值背后的监管逻辑
IV(Information Value)计算公式为:
$$ \text{IV} = \sum_{i=1}^{n} (\text{Good\%}_i - \text{Bad\%}_i) \times \text{WOE}_i $$
其值域与变量有效性对应关系为:
| IV值 | 解释 | 是否推荐入模 |
|------|------|--------------|
| <0.02 | 几乎无预测力 | ❌ 强制剔除 |
| 0.02–0.1 | 弱预测力 | ⚠️ 需结合业务判断 |
| 0.1–0.3 | 中等预测力 | ✅ 推荐 |
| >0.3 | 强预测力 | ✅ 但需警惕过拟合 |
为什么阈值设为0.02?因为银保监会《商业银行模型风险管理指引》要求:入模变量必须对区分好坏客户有“实质性贡献”,而实证研究表明,IV<0.02的变量在滚动时间窗口(如过去6个月)中,其区分能力波动幅度超过40%,稳定性不达标。我们在feature_selector.py中不仅过滤IV<0.02的变量,还计算其6个月PSI(Population Stability Index),对PSI>0.25的变量追加标记——这意味着即使IV合格,若客户结构迁移剧烈,该变量也需谨慎使用。
3.4 逻辑回归建模:别忽略截距项的业务含义
很多教程直接调用sklearn.linear_model.LogisticRegression,却忽略了一个关键点:截距项(intercept)不是数学常数,而是基准客群的风险锚点。在我们的训练集中,截距项为-2.15,对应基准客群(所有变量WOE值均为0)的违约概率为:
$$ P = \frac{1}{1 + e^{-(-2.15)}} = 10.5\% $$
这个数字必须与业务常识吻合——若基准客群(如35岁、本科、稳定就业、无逾期)违约率高达10.5%,说明模型整体风险偏好过于激进,需检查数据标签质量或WOE分箱合理性。我们在notebook/Section_5_Model_Fitting.ipynb中专门添加了截距项敏感性分析:当人为将截距调整±0.3时,全量测试集评分分布偏移达±60分,直接触发重检流程。
实操技巧:为避免截距项被优化器过度压缩,我们在
LogisticRegression中设置penalty='l2'且C=100(弱正则),同时固定fit_intercept=True。所有参数选择均有validation_curve交叉验证支撑,而非拍脑袋设定。
4. 实操过程与核心环节实现:手把手跑通全流程
4.1 环境配置与依赖管理:requirements.txt的深意
requirements.txt表面只列了12个包,但每个版本都经过生产环境验证:
pandas==1.5.3 # 1.6+版本中DataFrame.copy()行为变更,影响WOE缓存
numpy==1.23.5 # 与scikit-learn 1.2.2 ABI兼容性最佳
scikit-learn==1.2.2 # 1.3+移除了LogisticRegression中的warm_start参数,影响增量训练
statsmodels==0.13.5 # WOE计算依赖其Logit.summary()的置信区间输出
matplotlib==3.7.1 # 3.8+默认字体渲染异常,导致roc_curve.png中文乱码
特别提醒:pip install -r requirements.txt后,务必运行python -c "import sklearn; print(sklearn.__version__)"确认版本。曾有团队因conda环境混用导致scikit-learn版本为1.3.0,feature_selector.py中select_from_model()方法报错,排查耗时两天——根源就在requirements.txt未锁定次版本号。
4.2 WOE编码全流程:从分箱到映射的代码实录
核心函数位于code/woe_calculator.py,以annual_income(年收入)为例:
def calculate_woe(df, feature_col, target_col='is_bad', bins=None):
"""
df: 原始数据框(含target_col)
feature_col: 待编码字段名
bins: 手工指定分箱边界,如[0, 50000, 100000, 200000, np.inf]
"""
# 步骤1:按bins分箱,生成category列
if bins is None:
bins = auto_optimal_binning(df, feature_col, target_col)
df['bin'] = pd.cut(df[feature_col], bins=bins, include_lowest=True)
# 步骤2:计算各箱Good/Bad分布
total_good = len(df[df[target_col] == 0])
total_bad = len(df[df[target_col] == 1])
bin_stats = df.groupby('bin')[target_col].agg(['count', 'sum'])
bin_stats.columns = ['total_count', 'bad_count']
bin_stats['good_count'] = bin_stats['total_count'] - bin_stats['bad_count']
bin_stats['good_pct'] = bin_stats['good_count'] / total_good
bin_stats['bad_pct'] = bin_stats['bad_count'] / total_bad
# 步骤3:计算WOE,处理零值(加平滑因子)
epsilon = 1e-6
bin_stats['woe'] = np.log(
(bin_stats['bad_pct'] + epsilon) / (bin_stats['good_pct'] + epsilon)
)
# 步骤4:强制单调性校验
if not is_monotonic(bin_stats['woe']):
raise ValueError(f"WOE for {feature_col} is non-monotonic. Check binning.")
return bin_stats['woe'].to_dict()
关键细节:
- epsilon=1e-6防止log(0)报错,但值极小,不影响业务解读;
- is_monotonic()函数遍历WOE序列,检测是否存在局部极大/极小值;
- 返回字典格式{bin_range: woe_value},便于后续map()操作。
在CREDIT_SCORING_CARD_MODEL.ipynb中,我们对annual_income执行:
income_woe_map = calculate_woe(
train_df,
'annual_income',
bins=[0, 50000, 100000, 200000, 500000, np.inf]
)
train_df['annual_income_woe'] = train_df['annual_income'].map(income_woe_map)
生成的新列annual_income_woe即为模型直接可用的特征。
4.3 评分映射公式推导:从log-odds到300–900分
这才是评分卡的灵魂。逻辑回归输出的是log-odds:
$$ \text{log-odds} = \beta_0 + \beta_1 \cdot \text{WOE}_1 + \beta_2 \cdot \text{WOE}_2 + \dots $$
需转换为业务友好的整数分。行业通用公式为:
$$ \text{Score} = \text{Offset} + \text{Factor} \times \text{log-odds} $$
其中:
- Offset(偏移量):设定基准分(如600分)对应的log-odds;
- Factor(因子):控制“分数变化”与“风险变化”的敏感度(通常设为20/ln(2),即分数每±20分,违约概率翻倍)。
推导过程(见notebook/Section_7_Score_Transformation.ipynb):
1. 设定基准点:当log-odds = β₀(所有WOE=0的基准客群)时,Score = 600;
2. 设定倍数点:当log-odds = β₀ + ln(2)(违约概率翻倍)时,Score = 600 - 20(风险↑,分数↓);
3. 解方程组:
$$
\begin{cases}
600 = \text{Offset} + \text{Factor} \times \beta_0 \
580 = \text{Offset} + \text{Factor} \times (\beta_0 + \ln 2)
\end{cases}
\Rightarrow \text{Factor} = \frac{-20}{\ln 2} \approx -28.85,\ \text{Offset} = 600 - (-28.85) \times \beta_0
$$
最终在code/score_transformer.py中实现:
class ScoreCardTransformer:
def __init__(self, base_score=600, pdo=20, odds_ratio=2):
self.base_score = base_score
self.factor = -pdo / np.log(odds_ratio) # -28.85
self.offset = None
def fit(self, intercept):
self.offset = self.base_score - self.factor * intercept
def transform(self, log_odds):
return np.round(self.offset + self.factor * log_odds).astype(int)
实操心得:
pdo=20(Points to Double the Odds)是行业默认值,但需根据业务容忍度调整。某消费金融公司因客群风险高,将pdo设为15,使分数更敏感——这直接导致审批通过率下降12%,必须同步调整额度策略。所以,分数公式不是数学游戏,而是业务策略的数字化表达。
4.4 PSI稳定性检验:不止于“数值达标”
PSI(Population Stability Index)公式为:
$$ \text{PSI} = \sum (\text{Actual\%}_i - \text{Expected\%}_i) \times \ln\left(\frac{\text{Actual\%}_i}{\text{Expected\%}_i}\right) $$
其中Expected%来自训练集分箱分布,Actual%来自新数据(如每月新增客户)。但单纯看PSI<0.1是否达标?不够。我们在code/psi_calculator.py中增加了三层诊断:
- 全局PSI:总分是否<0.1(监管红线);
- 分箱PSI:哪个分箱贡献最大?若
age_bin_[65+]的PSI占总量80%,说明老年客群结构突变,需专项分析; - 趋势PSI:过去6个月PSI序列是否持续上升?若从0.03→0.05→0.07→0.09→0.11,即使当月<0.1,也预示模型即将失效。
在notebook/Section_8_PSI_Test.ipynb中,我们用test.csv模拟“新客群”,输出PSI诊断报告:
| 分箱 | Expected% | Actual% | 贡献PSI | 业务解读 |
|------|-----------|---------|----------|-----------|
| age_[18,25) | 12.3% | 18.7% | 0.021 | Z世代客群激增,需核查营销渠道 |
| income_[0,5w) | 25.1% | 19.2% | 0.015 | 低收入客群减少,经济回暖信号 |
这种颗粒度的诊断,才是PSI检验的真正价值。
5. 常见问题与排查技巧实录:那些文档里不会写的坑
5.1 “模型AUC很高,但KS只有0.3”——你可能混淆了“区分能力”和“排序能力”
KS(Kolmogorov-Smirnov)统计量衡量模型对好坏客户的最大区分度,公式为:
$$ \text{KS} = \max(|\text{Cumulative Good\%} - \text{Cumulative Bad\%}|) $$
AUC高只说明整体排序好,KS低说明在某个分数段,好坏客户高度混杂。常见原因:
- WOE分箱过粗:如将“逾期次数”简单分为“0次”和“≥1次”,丢失了“1次”和“5次”的风险梯度;
- 变量共线性:
credit_utilization_ratio(信用卡使用率)和revolving_balance(循环余额)高度相关,模型权重分散,削弱单一变量区分力; - 样本偏差:训练集
is_bad标签中,60%为“M1逾期”,但测试集多为“M3+”,模型未学过深度逾期模式。
排查步骤:
1. 绘制KS曲线(notebook/Section_9_KS_ROC.ipynb),定位KS峰值对应分数段;
2. 查看该分数段客户特征:若集中于employment_length<1年,说明短期工作者风险建模不足;
3. 在feature_selector.py中启用VIF检测,剔除共线性变量(vif_threshold=5);
4. 对KS低谷段客户,人工抽样分析标签质量——我们曾发现测试集中“M2逾期”被误标为“正常”,修正后KS从0.32升至0.58。
5.2 “test.py验证失败:预测分数全为600”——检查WOE映射的边界条件
这是新手最高频报错。根本原因是:test.csv中存在训练集未覆盖的极端值,导致map()返回NaN,而fillna(0)后WOE全为0,log-odds=截距项,分数恒为基准分。
复现与修复:
# 错误示范:直接map,不处理未见值
test_df['income_woe'] = test_df['annual_income'].map(income_woe_map)
# 正确做法:用pd.cut+value_counts保证边界一致
test_df['income_bin'] = pd.cut(test_df['annual_income'],
bins=[0, 50000, 100000, 200000, 500000, np.inf],
include_lowest=True)
# 将bin映射到WOE(训练时已知)
test_df['income_woe'] = test_df['income_bin'].map(income_woe_map)
# 对未见bin(如annual_income=600000),设为最邻近箱WOE
test_df['income_woe'] = test_df['income_woe'].fillna(income_woe_map[np.inf])
在test.py中,我们强制添加了边界校验:
def validate_woe_mapping(train_bins, test_values):
"""检查test_values是否超出train_bins范围"""
min_train, max_train = train_bins[0], train_bins[-1]
outliers = test_values[(test_values < min_train) | (test_values > max_train)]
if len(outliers) > 0:
print(f"Warning: {len(outliers)} test values out of training range")
# 自动截断并记录
test_values = np.clip(test_values, min_train, max_train)
return test_values
5.3 “ROC曲线不平滑,出现锯齿”——分箱粒度与样本量的博弈
ROC曲线横轴是FPR(假正率),纵轴是TPR(真正率),理想情况下应为光滑曲线。出现锯齿,说明在某个阈值点,TPR/FPR发生跳跃式变化——根源在于评分离散化。
我们的评分卡输出整数分(300–900),共601个可能值。若测试集仅1000个样本,某些分数可能无人命中,导致ROC点缺失。解决方案:
- 插值法:在
plot_roc_curve()函数中,对缺失分数使用线性插值; - 分数平滑:在
score_transformer.py中增加smooth_factor=0.1,对相邻分数加权平均; - 根本解决:扩大测试集规模(
test.csv建议≥5000样本),或改用sklearn.metrics.roc_curve的drop_intermediate=False参数强制保留所有点。
我们在notebook/Section_9_KS_ROC.ipynb中对比了三种方案效果:原始ROC(锯齿明显)、插值ROC(平滑但失真)、扩大测试集ROC(完美)。结论是——没有银弹,只有权衡。业务场景若需向高管汇报,用插值法;若用于模型迭代,必须用大样本测试集。
5.4 “main.py调用时报错:ModuleNotFoundError: No module named ‘code’”——Python路径陷阱
这是目录结构引发的经典问题。main.py位于项目根目录,而code/是子目录,直接import code.woe_calculator会失败,因为Python未将当前目录加入sys.path。
正确解法(已在main.py中实现):
import sys
from pathlib import Path
# 将项目根目录加入路径
sys.path.insert(0, str(Path(__file__).parent))
from code.woe_calculator import calculate_woe
from code.score_transformer import ScoreCardTransformer
但更健壮的做法是:在项目根目录创建setup.py,执行pip install -e .进行开发安装。不过考虑到用户多为风控分析师而非工程师,我们选择显式路径注入——简单、可靠、无需额外命令。
最后分享一个小技巧:在Jupyter中调试
main.py时,常因路径问题失败。此时在Notebook首单元格运行:
python import os, sys os.chdir('/path/to/your/project/root') sys.path.insert(0, os.getcwd())
即可无缝调用所有模块,省去反复cd的麻烦。
6. 这份资源包的边界在哪里?以及,它还能怎么进化
我必须坦诚:这份资源包不是万能钥匙。它解决的是“如何从0到1构建一张合规、可解释、可落地的评分卡”,但绝不承诺“一键解决所有风控难题”。它的明确边界有三:
第一,不覆盖模型监控上线。它提供PSI检验脚本,但不包含Airflow调度、Prometheus告警、模型漂移自动重训等MLOps能力。若你需要7×24小时监控,需在此基础上集成mlflow或Evidently;
第二,不替代业务规则引擎。评分卡输出分数,但最终审批还需叠加规则(如“分数>700且收入证明缺失→人工审核”)。main.py只负责分数计算,规则逻辑需业务方自行编写;
第三,不解决数据治理顽疾。它假设training.csv字段准确、标签无噪声。现实中,我们花40%时间清洗数据——比如发现is_bad标签中,2022年Q3的“M1逾期”被系统误标为“正常”,需追溯ETL日志修正。
至于进化方向,基于我们服务23家金融机构的经验,三个最迫切的升级点已纳入v2.0规划:
- 动态分箱适配器:当前分箱规则静态固化。v2.0将引入
adaptive_binner.py,基于每月新数据自动微调分箱边界(如当[25,35)年龄客群PSI>0.15时,触发该箱裂变为[25,30)、[30,35)); - 多目标评分卡:单一违约预测已不够。v2.0支持同时优化“违约概率”和“预期损失(EAD×LGD)”,输出双维度分数,适配巴塞尔协议III的全面风险计量;
- 监管沙盒接口:预置
regulatory_reporter.py,一键生成符合《商业银行模型风险管理指引》附件3要求的模型文档(含变量字典、WOE表、PSI报告、敏感性分析),直接对接监管报送系统。
但所有这些进化,都建立在一个不变的前提之上:评分卡首先是业务语言,其次才是数学工具。当你下次打开CREDIT_SCORING_CARD_MODEL.ipynb,请记住,那些看似枯燥的WOE计算、IV筛选、PSI检验,本质上都是在用代码重写一句人话:“我们相信,一个35岁、有房贷、近一年无逾期的客户,比一个22岁、无稳定收入、有过两次M1逾期的客户,更值得信任。”——而这份资源包的价值,就是帮你把这句话,写得足够严谨、足够透明、足够经得起推敲。
简介:直接上手就能跑的金融风控评分卡实现方案,用Python完成从原始信贷数据到可部署评分卡的全流程。包含训练集和测试集两个CSV文件,字段涵盖客户年龄、收入、负债比、逾期次数等典型风控变量;核心代码在Jupyter Notebook中分步呈现:缺失值处理、异常值识别、连续变量分箱、WOE转换、IV值计算筛选有效特征、逻辑回归建模、模型稳定性检验(PSI)、KS曲线与ROC分析、最终评分映射公式推导。配套test.py用于一键验证预测结果,main.py提供轻量级调用接口,roc_curve.png为可视化评估输出。所有脚本基于标准Python 3.8+环境,依赖库通过requirements.txt统一管理,无需额外配置即可复现。data目录存放原始及中间处理数据,notebook目录承载交互式分析过程,code目录封装了WOE计算、评分转换等可复用函数,目录示例输出结果。适合风控建模入门学习、内部培训演示或快速对接业务系统。
119

被折叠的 条评论
为什么被折叠?



