☰
Python申请评分卡模型实战:从数据集到信用评分系统全流程
2026/10/20 18:40:11 网站建设 项目流程

简介:这份资源面向金融风控与数据挖掘方向的学习者,提供用Python实现申请评分卡模型的完整数据集与配套代码,帮助理解从原始申请数据到信用评分的全流程。压缩包共14个文件,约9.22MB,包含1个csv原始数据集、5个py脚本、7个pkl序列化文件及1个lr_classweight权重文件,覆盖数据读取、特征工程、模型训练与评分卡转换等环节。其中csv用于训练与测试,py脚本对应逻辑回归、GBDT、DNN等建模流程,pkl文件保存分箱字典、WOE映射与训练测试数据,便于直接复现。已有1696人学习下载,适合希望掌握评分卡原理、练习sklearn建模与特征分箱的读者参考,可借此熟悉违约预测、样本不平衡处理与模型评估等实际风控场景。

1. 申请评分卡模型:从一份数据集到一个能跑通的信用评分系统

很多做风控的朋友第一次接触评分卡,都是被“申请评分卡”这四个字吓住的——以为要懂多少金融业务、多少统计学。其实拆开来看,它就是一个用历史申请数据训练出来的二分类模型,输出一个分数,用来判断“这个人未来会不会逾期”。这份 Python 申请评分卡模型数据集,给的就是从原始特征到最终评分的一整套落地素材,适合正在做信贷风控、消费金融或者想入门信用评分的开发者。你拿到手之后,能直接跑通数据清洗、WOE 分箱、IV 筛选、逻辑回归训练、评分映射这条完整链路,而不是只对着公式发呆。下面我按自己拆项目的习惯,把这份资源从头到尾讲一遍。

2. 项目定位与技术选型:为什么是逻辑回归加 WOE 分箱

2.1 评分卡的本质是一个可解释的二分类器

申请评分卡要解决的核心问题很具体:给一笔新进来的贷款申请打分,分数越高信用越好,分数越低风险越大。它和普通二分类模型最大的区别在于可解释性——业务方要知道“为什么这个人被拒了”,监管也要能追溯每个特征的贡献。所以这个项目没有用 XGBoost 或神经网络,而是选了逻辑回归。逻辑回归的系数可以直接换算成每个特征的分数,这是评分卡行业的硬性要求,不是技术落后。

数据集里通常包含申请人的基本信息、征信查询记录、历史还款表现等字段,标签是“好/坏”二分类。整个流程走下来,你会得到一个评分卡表:每个特征分箱对应一个分数,加总就是最终评分。常见做法是把分数映射到 300 到 850 之间,分数越高违约概率越低。

2.2 技术栈与依赖清单

这个项目用到的库都是风控建模的标配,没有冷门依赖:

库名用途备注
pandas数据读取与清洗处理缺失值、类型转换
numpy数值计算分箱边界计算
scikit-learn逻辑回归、模型评估LogisticRegression、roc_auc_score
matplotlib分布图、ROC 曲线可视化分箱效果
scipy统计检验卡方分箱可用

安装命令很直接:

pip install pandas numpy scikit-learn matplotlib scipy

如果你的环境里已经有这些库,跳过就行。我一般会单独建一个虚拟环境,避免和别的项目串版本:

python -m venv scorecard_env source scorecard_env/bin/activate # Windows 用 scorecard_env\Scripts\activate pip install pandas numpy scikit-learn matplotlib scipy

提示:scikit-learn 版本建议 1.0 以上,旧版本里 LogisticRegression 的 solver 默认参数有差异,可能导致结果对不上。

2.3 数据集结构长什么样

数据集一般是一个 CSV 或者 Excel 文件,行是申请人,列是特征加标签。典型的字段包括:年龄、月收入、学历、婚姻状况、历史逾期次数、查询次数、贷款金额、期限等。标签列通常叫target或label,0 代表好客户,1 代表坏客户。有些数据集会额外给一列sample_weight,用于处理样本不平衡。

拿到数据第一件事不是急着建模,而是先看分布。我习惯用下面这段代码快速摸清数据底细:

import pandas as pd # 读取数据集,注意编码格式,中文数据常见 gbk 或 utf-8 df = pd.read_csv('credit_data.csv', encoding='utf-8') # 查看行列数、标签分布、缺失情况 print('数据形状:', df.shape) print('标签分布:\n', df['target'].value_counts()) print('缺失值统计:\n', df.isnull().sum().sort_values(ascending=False).head(10)) print('数值型特征描述:\n', df.describe().T[['mean', 'std', 'min', 'max']])

这段代码输出四个关键信息:数据规模、好坏样本比例、哪些字段缺失严重、数值特征的量纲范围。如果坏样本占比低于 5%,后面训练时就要考虑样本权重或者过采样,否则模型会偏向预测“好客户”。缺失率超过 60% 的字段,我一般直接丢掉,补太多反而引入噪声。

3. 核心模块拆解:从原始数据到评分卡的完整链路

3.1 数据清洗与特征初筛

原始数据里最常见的脏东西是:年龄填成 0 或 999、收入为负数、类别字段有拼写不一致。这些不处理,后面分箱会直接报错。清洗逻辑我一般写成一个函数,按列类型分别处理:

import numpy as np def clean_data(df): # 数值型字段:异常值截断到合理分位 num_cols = ['age', 'income', 'loan_amount'] for col in num_cols: if col in df.columns: lower = df[col].quantile(0.01) upper = df[col].quantile(0.99) df[col] = df[col].clip(lower, upper) # 类别型字段:统一大小写、去除空格 cat_cols = ['education', 'marriage', 'housing'] for col in cat_cols: if col in df.columns: df[col] = df[col].astype(str).str.strip().str.lower() # 缺失值:数值用中位数,类别用众数 for col in df.columns: if df[col].dtype == 'object': df[col] = df[col].fillna(df[col].mode()[0]) else: df[col] = df[col].fillna(df[col].median()) return df df_clean = clean_data(df)

这里有几个参数值得说清楚。quantile(0.01)和quantile(0.99)是截断边界,把极端值拉回正常范围,避免一个收入 999999 的样本把分箱边界带偏。类别字段统一转小写再去空格,是因为“本科”和“本科 ”在 pandas 里是两个不同的类别,不处理会多出无意义的分箱。缺失值填充用中位数而不是均值,是因为金融数据里收入、金额这类字段分布右偏严重,均值容易被极端值拉高。

清洗完之后,还要做一次特征初筛。方差接近 0 的字段(比如 99% 的人都是同一个值)直接删掉,它对区分好坏客户没有贡献。相关系数高于 0.9 的两个数值特征,保留 IV 值更高的那个,避免多重共线性让逻辑回归系数不稳定。

3.2 WOE 分箱与 IV 值计算

这是评分卡最核心的一步。WOE 叫证据权重,衡量的是“某个分箱里坏客户占比和好客户占比的差异”。IV 是信息值,衡量整个特征对好坏客户的区分能力。计算逻辑不复杂,但细节容易翻车。

def calc_woe_iv(df, feature, target): # 按特征分组统计好坏样本数 grouped = df.groupby(feature)[target].agg(['count', 'sum']) grouped.columns = ['total', 'bad'] grouped['good'] = grouped['total'] - grouped['bad'] # 计算整体好坏比例 total_bad = df[target].sum() total_good = df[target].count() - total_bad # 防止除零,加一个极小值 grouped['bad_rate'] = grouped['bad'] / total_bad + 1e-10 grouped['good_rate'] = grouped['good'] / total_good + 1e-10 # WOE = ln(坏客户占比 / 好客户占比) grouped['woe'] = np.log(grouped['bad_rate'] / grouped['good_rate']) # IV = sum((坏客户占比 - 好客户占比) * WOE) grouped['iv'] = (grouped['bad_rate'] - grouped['good_rate']) * grouped['woe'] iv_value = grouped['iv'].sum() return grouped, iv_value

这段代码里1e-10是防止某个分箱里坏客户数为 0 导致除零。WOE 的符号很关键:WOE 为正说明这个分箱坏客户占比高于整体,风险高;WOE 为负说明风险低。IV 值的经验判断标准是:小于 0.02 预测力弱,0.02 到 0.1 弱,0.1 到 0.3 中等,0.3 到 0.5 强,超过 0.5 要警惕过拟合。我一般保留 IV 大于 0.02 的特征,低于这个值的直接淘汰。

分箱本身可以用等频分箱、等距分箱或者卡方分箱。等频分箱保证每个箱样本量差不多,但可能把相同属性的样本切到不同箱;卡方分箱会合并相邻的相似箱,结果更稳定。这个项目里常见做法是先等频粗分 20 箱,再用卡方合并到 5 箱左右。

3.3 逻辑回归训练与评分映射

特征筛选完之后,把每个特征的 WOE 值替换原始值,然后喂给逻辑回归。注意:评分卡模型训练时用的是 WOE 转换后的数据,不是原始数据。

from sklearn.linear_model import LogisticRegression from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score # 假设 X 是 WOE 转换后的特征矩阵,y 是标签 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 训练逻辑回归,正则化用 L2,C 控制正则强度 lr = LogisticRegression( penalty='l2', C=1.0, solver='lbfgs', max_iter=1000, class_weight='balanced' ) lr.fit(X_train, y_train) # 评估 y_pred_proba = lr.predict_proba(X_test)[:, 1] auc = roc_auc_score(y_test, y_pred_proba) print(f'测试集 AUC: {auc:.4f}')

class_weight='balanced'是处理样本不平衡的关键参数,它会让模型自动给少数类更高的权重。C=1.0是正则化强度的倒数,C 越小正则越强,系数越保守。如果 AUC 在 0.75 以上,这个评分卡基本可用;低于 0.7 就要回头检查特征筛选和分箱是否合理。

训练完之后,把逻辑回归系数换算成评分。标准公式是:

# 评分映射:score = base_score + factor * ln(odds) # 其中 factor = pdo / ln(2),pdo 是分数翻倍所需的 odds 变化 pdo = 50 # 每 50 分 odds 翻倍 base_score = 600 # 基准分 base_odds = 50 # 基准 odds factor = pdo / np.log(2) offset = base_score - factor * np.log(base_odds) # 每个特征的分数 = -factor * 系数 * WOE # 最终评分 = offset + sum(各特征分数)

pdo=50意味着好坏比每翻一倍,分数变化 50 分。base_score=600和base_odds=50是行业常用的基准点,你可以根据业务需要调整。最终每个申请人得到一个分数,分数越高信用越好。

4. 避坑与常见问题:那些我踩过的坑

4.1 分箱后某个箱样本量为 0

现象:计算 WOE 时报RuntimeWarning: divide by zero,或者 WOE 出现inf。原因:等频分箱时如果某个特征值大量重复,分箱边界会把所有样本挤到同一个箱,相邻箱为空。解决:分箱前先检查特征唯一值数量,少于 10 个的类别特征直接按类别分箱,不要用等频。数值特征如果重复值超过 30%,改用卡方分箱或者手动指定边界。

4.2 训练集 AUC 很高但测试集崩了

现象:训练集 AUC 0.9,测试集只有 0.65。原因:通常是 IV 筛选时保留了太多高 IV 特征,或者分箱过细导致每个箱样本太少,模型记住了噪声。解决:把 IV 上限卡在 0.5,超过的要么是标签泄露,要么是过拟合。分箱后每个箱至少保证 5% 的样本量,不够就和相邻箱合并。

4.3 WOE 转换后逻辑回归系数符号反了

现象:某个特征 IV 很高,但逻辑回归系数是负的,和业务常识相反。原因:WOE 计算时好坏样本的定义搞反了,或者某个分箱的 WOE 方向和其他箱不一致。解决:统一用“坏客户/好客户”算 WOE,确保所有分箱的 WOE 单调性合理。如果某个箱 WOE 方向突兀,检查是不是样本量太少导致的波动,合并掉。

4.4 缺失值填充后分数分布异常

现象:评分卡上线后,大量申请人分数集中在某个区间,区分度很差。原因:缺失值用中位数填充后,所有缺失样本都落到同一个分箱,WOE 相同,分数自然一样。解决:缺失值单独作为一个分箱,不要和正常值混在一起。很多数据集里“缺失”本身就是一种信息,比如“没填收入”可能意味着收入不稳定。

4.5 评分映射后分数范围不对

现象:算出来的分数有负数,或者集中在 200 到 300 之间,和预期的 300 到 850 差很远。原因:base_score和base_odds设置不合理,或者factor计算时用了自然对数而不是以 2 为底。解决:先确认pdo和base_odds的业务含义,再用公式反推offset。如果分数整体偏低,调高base_score;如果分数跨度太小,调大pdo。

5. 二次开发与改造思路:让评分卡适配你自己的业务

拿到这份数据集跑通之后,下一步就是把它改成你自己的评分卡。我一般从三个地方下手。

第一,替换数据集但保留流程。把credit_data.csv换成你自己的申请数据,字段名对齐就行。如果字段类型不同,改clean_data函数里的列名列表。注意标签列的名字要统一,我习惯用target,0 是好客户,1 是坏客户。如果你的数据里标签是反的,在读取后加一行df['target'] = 1 - df['target']翻转过来。

第二,调整分箱策略。不同业务对分箱的偏好不一样。信贷申请评分卡通常要求每个箱样本量均衡,用等频分箱;但如果某个特征业务上必须按固定区间分(比如年龄按 18-25、26-35、36-45 分),就手动指定边界。下面这个函数可以让你传入自定义边界:

def custom_binning(df, feature, bins): # bins 是边界列表,比如 [0, 25, 35, 45, 100] df[feature + '_bin'] = pd.cut( df[feature], bins=bins, labels=False, include_lowest=True ) return df # 示例:年龄按业务规则分箱 df = custom_binning(df, 'age', [0, 25, 35, 45, 60, 100])

include_lowest=True保证最小值落在第一个箱里,不然 18 岁会被排除。labels=False返回整数编码,方便后续计算 WOE。

第三,换模型但保留评分映射。如果你不满足于逻辑回归的区分度,可以换成 LightGBM 或 XGBoost,但评分卡的可解释性会打折扣。折中方案是用树模型做特征筛选,再用逻辑回归做最终评分。我试过用 LightGBM 的 feature_importance 挑前 15 个特征,再走 WOE 加逻辑回归,AUC 能提升 0.03 左右,同时保留评分表的可解释性。

验证改造效果的方法很直接:留出最近三个月的申请数据做时间外样本测试。如果时间外 AUC 和随机测试集 AUC 差距在 0.05 以内,说明模型没有过拟合到旧数据。差距太大,就要检查特征里有没有用到未来信息,比如“最近一次还款日期”这种字段在申请时点根本拿不到。

注意:评分卡上线后不是一劳永逸的。我一般每季度重新跑一次 IV 和 WOE,监控分数分布有没有漂移。如果某个特征的 IV 值从 0.3 掉到 0.05,说明这个特征在当前客群上已经失效,需要替换。

从那以后我每次做评分卡,都强制走一遍“清洗 → 分箱 → IV 筛选 → WOE 转换 → 逻辑回归 → 评分映射”的完整链路,中间任何一步跳过,后面都要花更多时间补窟窿。希望这份数据集和上面的拆解能帮你少走点弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询