简介:一份基于逻辑回归算法的信用评分卡模型构建资源,面向数据挖掘初学者及有一定基础的学习者,适用于毕业设计、课程实践、作业实训。内容覆盖数据预处理与特征处理,将变量转换为WOE形式,计算信息值完成变量筛选,再基于选定特征构建评分模型,可依据输入特征自动生成评分结果。资源包共10个文件,包含Python脚本、CSV数据、XLS字典及备份文件,整体大小7.68MB,配有评分主程序、测试数据集与数据字典,便于对照学习完整流程。数据字典提供字段级说明,便于理解原始变量含义;备份文件则便于复盘调试。已有41人浏览学习,适合快速掌握逻辑回归评分卡建模思路,可获得可运行或可改写的脚本、样例数据及变量说明,为课程设计或项目开发提供直接参考。
1. 信用评分模型还在用逻辑回归,不是因为它简单
在深度学习霸榜各种预测竞赛的今天,银行、持牌消金、互联网信贷的核心风控系统里,信用评分模型最常选用的算法依然是逻辑回归。不是开发团队做不出更复杂的模型,而是信用评分模型的交付物不只是预测结果,还要是一套能解释、能复核、能接受审计的决策依据。业务人员要能指着评分说“这个客户为什么被拒”,监管要求拒绝理由可回溯,风控策略要能把分数切分成精确到每个变量的加减分规则。逻辑回归算法天然具备系数可读、变量贡献可分解、输出单调稳定这些特性,这些恰恰是信用评分模型刚需。本文从特征分箱、WOE编码、逻辑回归训练到评分刻度转换,完整走一遍基于逻辑回归算法构建信用评分模型的方法,并重点说明那些在真实场景里最容易被忽略的参数选择和数据泄漏问题。适合风控建模、反欺诈策略、数据产品以及信贷审批系统的工程师阅读。
2. 逻辑回归算法在信用评分里的数学原理与参数含义
2.1 为什么信用评分选逻辑回归而不是直接预测违约金额
信用评分模型处理的问题是二分类:好客户还是坏客户,逾期是否发生。线性回归在数学上也能尝试做分类,但残差不满足正态性、预测值可能落在0到1之外,无法解释成概率。逻辑回归通过sigmoid函数把线性组合z压缩到(0, 1)区间,输出可以被解释为违约概率。
但在真实风控实践中,逻辑回归算法真正被看重的是它的可加性。信用评分模型可以展开成:
Score = offset + factor × (β0 + β1×x1 + β2×x2 + ... + βk×xk)每个特征对分数的贡献是独立可加的。年龄大5岁加多少分,近3个月查询次数超过10次减多少分,这些规则能直接写进审批策略和客户告知书。换成GBDT或神经网络,特征交互和隐藏层权重无法还原成这种可审计的规则,这是很多机构在核心审批链路上不敢轻易替换逻辑回归的主要原因。
2.2 极大似然估计、正则化与类别不平衡参数
逻辑回归使用极大似然估计求解参数,等价于最小化交叉熵损失函数:
L(β) = -Σ [yi × ln(pi) + (1 - yi) × ln(1 - pi)]风控数据天然存在类别不平衡,坏样本占比通常在1%到8%之间。直接训练会导致模型预测概率整体偏低,决策边界被好样本主导。常见做法是在逻辑回归中设置class_weight="balanced",对少数类样本赋予更高权重。注意这个参数只影响系数大小和截距,不影响最终评分卡好坏排序能力,因此上线前评分刻度转换时要基于修正截距重新做整体分数校准。
正则化参数C在信用评分模型里的选择也有讲究。C越小,惩罚越强,系数越向0收缩,模型越稳定。我一般先把候选特征经过IV筛选控制在10到15个以内,然后用带L2正则的逻辑回归配合5折交叉验证选C。L2的优点是所有系数都会被保留但不发散,系数符号更容易保持稳定;L1虽然会把弱特征系数压成0,但在评分卡场景会丢失一些可解释性,需要额外处理。参考范围如下:
| C值 | 正则化强度 | 适用场景 | 常见问题 |
|---|---|---|---|
| 0.01 | 强惩罚 | 特征多、样本少 | 系数过小,评分差不开 |
| 0.1 | 较强惩罚 | 常规风控建模 | 稳定性好,但可能欠拟合 |
| 1 | 默认,适中 | 特征质量高 | 需要做稳定性验证 |
| 10 | 弱惩罚 | 样本量大 | 系数波动大,线上偏移风险高 |
实际建模时不要只看AUC选C,还要观察不同C值下回归系数方向和业务逻辑是否一致。征信查询次数越多,违约风险越高,系数应该是正数;如果系数符号发生了翻转,多数情况是多重共线性或分箱方向出了问题。
2.3 从概率到对数几率:评分卡的数学桥 bridge
逻辑回归输出的概率p是非线性的,直接拿p做分数阈值会导致同一个特征在不同概率区间内对分数的边际影响不同。评分卡用的线性部分是:
z = ln(p / (1 - p)) = β0 + β1×x1 + β2×x2 + ... + βk×xk其中p/(1-p)称为违约几率(odds)。z每变化1个单位,odds扩大e倍。评分卡设定PDO(Points to Double the Odds),表示分数每增加PDO分,好客户相对坏客户的比率翻一倍。于是评分公式定义为:
Score = offset - factor × z注意这里是减号,保证分数越高、违约风险越低。factor和offset由下面两个业务参数决定:基准odds下对应的基准分,以及PDO值。这两个参数的标定方法会在第四章给出完整代码。
3. 特征分箱与WOE编码:构建信用评分模型的前置步骤
3.1 分箱是逻辑回归处理非线性关系的核心手段
逻辑回归是线性模型,输入和输出的关系在z空间被强制为线性。年龄与违约率明显是U形关系,收入和负债比的影响也是边际递减的,直接让原始值进入模型,要么拟合不动,要么引入多项式加剧共线性。分箱解决的就是这个问题:把连续变量切成几段离散区间,每段独立拥有一个风险系数。
信用评分模型最常用的分箱方法是等频分箱与手动合并的结合。等频分箱保证每个箱样本量接近,后续bad rate估计才稳定;等距分箱简单直观,但如果客户集中在某个年龄段,箱内样本会严重失衡;卡方分箱和决策树分箱能自动找边界,却容易构造出缺乏业务含义的切点。下表是这几种分箱方法在评分卡项目中的对比:
| 分箱方法 | 边界确定方式 | 优点 | 缺点 |
|---|---|---|---|
| 等频 | 按分位数切 | 样本量均匀,bad rate稳定 | 边界不是最有区分度的点 |
| 等距 | 按数值等宽切 | 简单直观 | 分布偏斜时多数箱样本过少 |
| 卡方合并 | 按卡方检验合并相邻箱 | 自动处理非线性 | 容易过拟合,边界奇怪 |
| 决策树 | 按信息增益/基尼切分 | 边界接近最优 | 对缺失值和异常值敏感 |
实际项目中,我一般先用等频初分出5到10箱,然后按bad rate趋势合并样本量极小或坏样本为零的箱,最终把变量控制在3到6个分箱区间。缺失值不做均值填充,而是单独成一个分箱,因为征信缺失往往代表客户没有信贷历史,这类客户的违约表现往往显著不同。
3.2 用Python实现等频分箱与WOE计算
下面这段代码给出完整的等频分箱和WOE计算过程,直接复用即可。
import pandas as pd import numpy as np def woe_iv_bin(df, feature, target, bins=5, min_samples=50): """ df: 数据集 feature: 需要分箱的连续特征 target: 好坏标签,1为坏客户,0为好客户 bins: 等频分箱数量 min_samples: 每个箱的最小样本数,低于该值的箱会被合并 """ data = df[[feature, target]].copy() # 缺失值单独成箱 data["bin"] = data[feature].map(lambda x: "missing" if pd.isna(x) else None) valid = data[data["bin"] != "missing"].copy() missing = data[data["bin"] == "missing"].copy() # 等频分箱 try: valid["bin"], cut_points = pd.qcut( valid[feature], q=bins, duplicates="drop", retbins=True ) except ValueError: # 样本中重复值过多时退化为等距分箱 valid["bin"], cut_points = pd.cut( valid[feature], bins=bins, retbins=True ) data = pd.concat([valid, missing], axis=0) # 统计每个箱的好坏样本数 grouped = ( data.groupby("bin", observed=False) .agg( good=("y", lambda x: (x == 0).sum()), bad=("y", lambda x: (x == 1).sum()), total=("y", "count"), ) .reset_index() ) # 加0.5平滑,避免除零 grouped["good"] = grouped["good"] + 0.5 grouped["bad"] = grouped["bad"] + 0.5 total_good = grouped["good"].sum() total_bad = grouped["bad"].sum() # WOE = ln(坏客户占比 / 好客户占比) grouped["bad_dist"] = grouped["bad"] / total_bad grouped["good_dist"] = grouped["good"] / total_good grouped["woe"] = np.log(grouped["bad_dist"] / grouped["good_dist"]) # IV = Σ (坏分布 - 好分布) × WOE grouped["iv"] = (grouped["bad_dist"] - grouped["good_dist"]) * grouped["woe"] grouped["bad_rate"] = grouped["bad"] / (grouped["good"] + grouped["bad"]) return grouped, cut_points # 使用示例 # train_df包含特征age和坏客户标签y woe_df, bins = woe_iv_bin(train_df, "age", "y", bins=5) print(woe_df[["bin", "good", "bad", "bad_rate", "woe", "iv"]])代码背后的逻辑:先对缺失值标记特殊分箱,再用pd.qcut完成等频切分,按箱聚合好坏样本后计算WOE。WOE的定义是“坏客户在某箱内的占比相对好客户占比的对数”,WOE为正表示该箱坏客户比例高于正常水平,负值表示该箱风险较低。IV是各箱WOE的加权和,直接度量该变量对好坏的区分能力。加0.5平滑是为了防止某个箱坏样本数为0时对数计算无意义,这种平滑在样本量小时特别重要。
运行后重点看两列:bad_rate应该随着分箱区间呈现单调趋势或清晰的U形趋势;woe的符号如果和业务方向相反,一般要手工检查该分箱的样本画像,比如收入变量高收入箱WOE为正,多半是收入数据里混入了缺失占位符之类的脏数据。
3.3 IV筛选特征与箱数控制
IV值是最常用的特征筛选指标,计算方式在参数说明里已经给出。参考标准:
| IV范围 | 预测能力 | 处理建议 |
|---|---|---|
| < 0.02 | 几乎无区分度 | 直接剔除 |
| 0.02 ~ 0.1 | 较弱 | 谨慎保留,结合业务判断 |
| 0.1 ~ 0.3 | 中等 | 保留,评分卡主力变量 |
| > 0.3 | 较强 | 保留但检查是否含未来信息和过度拟合 |
IV过高也要警惕。某个特征IV超过0.5且不是客户历史逾期次数这类强变量,很可能是有严重的数据泄漏:比如训练集和测试集跨越了贷后表现期,把未来才产生的结果并入了当前特征。常用防范方式是在时间维度上切分样本,使用表现期结束前能获得的数据构造特征,这一点在信用评分模型里比任何算法细节都重要。
分箱数量控制在3到6个区间比较合适。箱太少损失信息,箱太多则每个箱的样本量不够,线上评分也显得零碎。判断分箱是否有效,用groupby统计bad_rate即可:理想情况下每个箱的bad_rate应该不同,且相邻箱之间的差距能通过业务解释清楚。
3.4 特征分箱与数据集划分
分箱边界只能在训练集上计算,测试集直接套用已经训练好的边界和WOE映射表。这是评分卡项目中最容易发生的标签泄漏源头之一。完整流程是:先用训练集做等频分箱得到cut_points → 按cut_points给训练集和测试集分别打箱号 → 用训练集的好坏人计算WOE映射字典 → 测试集根据箱号索引到对应的WOE值。代码如下:
def apply_woe(df, feature, cut_points, woe_map, missing_woe): df = df.copy() df["bin"] = pd.cut(df[feature], bins=cut_points, include_lowest=True) df["bin"] = df["bin"].astype("str") df.loc[df[feature].isna(), "bin"] = "missing" df["woe"] = df["bin"].map(woe_map).fillna(missing_woe) return df["woe"] # woe_map由训练集分箱结果生成 woe_dict = dict(zip(woe_df["bin"].astype("str"), woe_df["woe"])) missing_woe = woe_df[woe_df["bin"] == "missing"]["woe"].iloc[0] train_df["age_woe"] = apply_woe(train_df, "age", bins, woe_dict, missing_woe) test_df["age_woe"] = apply_woe(test_df, "age", bins, woe_dict, missing_woe)这段代码保证了训练集和测试集使用完全相同的分箱边界与WOE映射。对测试集而言,分箱边界和WOE值都来自训练集,不包含任何测试集信息,这是信用评分模型评测有效性的前提。
4. 基于逻辑回归的信用评分卡构建全流程
4.1 用WOE替换原始特征训练逻辑回归模型
特征经过分箱和WOE编码后,全部变成连续型的WOE列,再进入逻辑回归。注意参与训练的必须是WOE,不是原始值,也不是箱号。箱号是类别变量,直接送入模型等于默认相邻箱之间风险差距相同,这不符合实际;WOE把箱子的风险强度量化成了连续值,保留箱间的序关系。训练代码:
import statsmodels.api as sm features = ["age_woe", "income_woe", "query_count_woe", "debt_ratio_woe"] X_train = sm.add_constant(train_df[features]) X_test = sm.add_constant(test_df[features]) model = sm.Logit(train_df["y"], X_train).fit() print(model.summary()) # 如果是sklearn方案,可以这样 # from sklearn.linear_model import LogisticRegression # lr = LogisticRegression(C=0.1, class_weight="balanced", max_iter=1000) # lr.fit(X_train, train_df["y"])statsmodels输出的summary里重点看每个变量的系数符号和P值。P值大于0.05的变量说明在逻辑回归里区分能力不足,考虑剔除后重新拟合。系数符号偏差判断:负债率越高,WOE越大,违约风险越高,系数应为正;如果符号相反,检查是否多重共线性或WOE方向计算反了。用sklearn时,C的值需要和业务稳定性要求匹配,我见过很多项目直接使用默认的C=1而不做验证,线上表现一般撑不过半年就出现评分分布偏移。
训练完成后的评估指标包括AUC、KS、布里尔分数。信用评分模型里KS一般要求大于0.3,AUC在0.75以上才具备基本可用价值,这一部分重点讲评分刻度转换,KS作为第5章的重点展开。
4.2 评分刻度转换:从逻辑回归系数到具体评分
分箱、训练都完成后,最后一步是把模型输出映射成业务熟悉的0到1000分。假设基准评分卡要求:基准odds为50(好客户与坏客户的比例50:1)对应600分,PDO为50(分数每增加50分,odds翻倍)。参数计算代码:
import numpy as np def cal_score_scale(base_score=600, base_odds=50, pdo=50): """ base_score: 基准分 base_odds: 基准odds,好客户/坏客户 pdo: odds翻倍所需的分值 """ factor = pdo / np.log(2) offset = base_score - factor * np.log(base_odds) return offset, factor offset, factor = cal_score_scale() def score_from_probability(prob, offset, factor): """把违约概率转成分数""" odds = prob / (1 - prob) score = offset - factor * np.log(odds) return score train_df["score"] = train_df["y_pred_prob"].map( lambda p: score_from_probability(p, offset, factor) )每个单一变量箱的贡献分可以按这个公式拆解:
def scorecard_points(woe_value, coef, offset_contrib, factor): """ 单个变量的某个分箱贡献分 分数 = 基准分 + 各变量箱贡献分的累加 """ return -factor * coef * woe_value # 生成评分卡明细表 scorecard_rows = [] for var in features: coef = model.params[var] # statsmodels系数 for _, row in woe_info[var].iterrows(): points = scorecard_points(row["woe"], coef, offset, factor) scorecard_rows.append( {"variable": var, "bin": row["bin"], "woe": round(row["woe"], 4), "points": round(points, 2)} ) scorecard = pd.DataFrame(scorecard_rows)score_from_probability函数的逻辑是:先把违约概率转成odds,取对数后乘factor再做减法。因为信用评分模型约定分数越高信用越好,而odds越大坏账风险越低,所以是offset减factor×ln(odds),符号不要反了。scorecard_points中再乘一次负号,是为了把逻辑回归系数本身的正负号还原到业务方向上:当某箱WOE为正且回归系数为正时,该箱风险偏高,points是负值,从总分中扣分,符合直觉。
4.3 评分卡落地上线与策略应用
生成的scorecard表最终会变成一份可以写入审批系统的配置:
| variable | 分箱区间 | WOE | 逻辑回归系数 | 对分数贡献 |
|---|---|---|---|---|
| age | 18~25 | 0.3872 | 0.0431 | -2.41 |
| age | 26~35 | 0.0241 | 0.0431 | -0.15 |
| age | 36~50 | -0.1827 | 0.0431 | 1.14 |
| ... | ... | ... | ... | ... |
线上计算单个客户评分时,不需要部署模型文件,只需要把客户的特征值定位到对应分箱,查询该箱的points并累加,再加上基准分600即可。这种查表式计算延迟可以做到毫秒级,也方便风控策略同学独立维护和调整每个变量的分值。常见做法是导成JSON或数据库配置,由规则引擎读取执行。
评分卡构建完成后不是结束,还需要监控线上评分分布与训练集分布是否一致,如果客户群体发生变化,特征分箱边界可能需要重新标定。
5. 模型评估的一个落地技巧:用PSI监控评分稳定度
5.1 先看KS和AUC,再决定是否需要调模
KS的计算方式是:按预测分数排序后,累计好客户占比与累计坏客户占比差值的最大值。KS达到0.3以上说明区分能力合格,超过0.5则要警惕是否过拟合或数据泄漏。AUC和KS都只在离线数据集上有意义,它们描述的是模型排序能力,不能反映时间推移后客户结构变化的问题。信用评分模型上线后首先要监控的就是评分稳定度,最常见的方法是PSI。
5.2 PSI实现与阈值判断
PSI(Population Stability Index)用于检测两个样本分布之间的一致性。训练集评分分布作为基准expected,线上某个月的真实评分作为actual,把两者按相同分箱切分后计算。实现:
def calculate_psi(expected, actual, bins=10): expected = pd.Series(expected, name="expected") actual = pd.Series(actual, name="actual") # 以训练集分位数为切分点,对当月分数做等频切分 cut_points = expected.quantile([i / bins for i in range(1, bins)]) actual_bins = pd.cut(actual, bins=cut_points, include_lowest=True) actual_pct = actual_bins.value_counts(normalize=True).sort_index() expected_pct = pd.Series( [1 / bins] * bins, index=actual_pct.index ) psi = 0 for idx in actual_pct.index: a = actual_pct[idx] e = expected_pct[idx] # 分布占比为0时,用极小值替代避免除零 a = 0.0001 if a == 0 else a e = 0.0001 if e == 0 else e psi += (a - e) * np.log(a / e) return psi monthly_psi = calculate_psi(train_df["score"], current_month["score"], bins=10) print(f"本月PSI = {monthly_psi:.4f}")因为训练集按等频分10箱,expected每箱占比正好是10%,所以expected_pct直接使用均匀分布。实际月度分数的分箱边界沿用训练集分位数,保证两边的分箱口径一致。PSI阈值判断参考:小于0.1表示分布稳定,介于0.1到0.25之间表示有偏移需要排查原因,大于0.25说明评分分布发生显著变化,模型需要重新校准或重训。
5.3 审批阈值划定与评分卡维护的配合
评分卡分数的cutoff不是固定不变的,要根据逾期收益矩阵动态调整。以评分x为横轴、坏账率为纵轴画平滑曲线,在曲线斜率出现明显变化的位置切分,通常会选择分数阈值在600到650之间。阈值设低,审批通过率提升但坏账损失上升;阈值设高,好客户流失率上升。常见做法是把历史逾期损失和单客利润做成表格,按分数段计算总收益,取最大值对应分数作为cutoff。这个表格每季度更新一次,结合PSI监控结果判断是否需要重新标定基准分。
分数阈值调整后,要同步检查评分卡中每个变量分箱的bad rate是否和训练时保持一致。特别是某些分箱在线上的bad rate如果出现明显偏移,优先排查该特征对应的数据源是否变更了填写口径,数据质量引发的分布漂移在风控场景远比算法失效常见。
本文还有配套的精品资源,点击获取