简介这份资源面向计算机、人工智能、金融科技等专业的在校学生与教师以及需要完成课程设计、期末大作业或毕业设计的开发者提供一套基于逻辑回归的金融风险控制建模完整方案。包内共73个文件以jpg图表、xlsx中间结果、csv数据集和py脚本为主另含pdf说明文档与md操作指南压缩包约22.52MB。项目采用GiveMeSomeCredit数据集依次经过数据预处理、基线模型、WOE转换与LR_WOE建模四步流程最终模型AUC达到0.85并输出ROC、KS、CAP、PSI及混淆矩阵等评估图表便于理解信用评分卡从特征分箱到模型验证的完整链路。目前已有485人学习下载。读者可据此掌握WOE编码、变量筛选与逻辑回归调参思路也可在现有代码上修改以适配其他风控场景适合作为课设、毕设或项目立项的参考模板。1. 逻辑回归做金融风控建模一份课程设计里真正能跑通的路径金融风控建模听起来像大厂算法团队的活但落到课程设计层面绝大多数人卡住的地方根本不是模型本身而是数据怎么清洗、特征怎么对齐、评分怎么映射成业务能用的阈值。逻辑回归之所以在信用评分卡场景里活了二十多年还没被淘汰核心原因是它的输出天然是概率可解释性强监管能看懂业务能落地。这份课程设计围绕的就是用 Python 把逻辑回归从原始数据一路推到可部署的评分模型配套源码、操作说明和数据集。适合正在做金融方向课程设计的学生也适合想从零搭一套风控建模流程的初级数据从业者。读完你应该能独立跑通数据预处理、特征工程、模型训练、评估和评分卡转换的完整链路而不是只会在 Jupyter 里调一个LogisticRegression().fit()。2. 数据到手先别急着建模金融风控数据集的清洗与特征对齐2.1 先搞清楚数据集长什么样拿到一份金融风控数据集第一件事不是打开 Jupyter 写模型而是用 pandas 把数据的基本面貌摸清楚。典型的风控数据集包含用户基本信息年龄、职业、收入区间、借贷行为历史逾期次数、当前负债比、征信查询记录等字段标签列通常是二分类的违约标识。常见做法是先看行列数、缺失率、标签分布再决定后续策略。import pandas as pd import numpy as np # 加载数据集注意编码格式中文列名常见 gbk 或 utf-8 df pd.read_csv(credit_data.csv, encodingutf-8) # 基本信息摸底 print(数据形状:, df.shape) print(标签分布:\n, df[default].value_counts(normalizeTrue)) print(缺失率 Top10:\n, df.isnull().mean().sort_values(ascendingFalse).head(10)) print(数据类型:\n, df.dtypes.value_counts())这段代码做了四件事看数据规模、看标签是否均衡、看哪些列缺失严重、看字段类型分布。参数上唯一需要注意的是encoding国内金融数据集用 gbk 的概率不低读进来乱码就换编码。标签分布如果正负样本比例超过 1:5后面建模时就要考虑样本权重或者重采样否则模型会偏向多数类违约用户一个都抓不出来。2.2 缺失值处理风控场景不能无脑填均值风控数据的缺失往往不是随机的。比如“近半年征信查询次数”缺失可能意味着这个用户根本没有征信记录也就是俗称的白户。无脑填均值会把这个信息抹掉。我一般会分三步走先统计每列缺失率缺失超过 60% 的列直接考虑丢弃缺失在 5% 到 60% 之间的单独加一个缺失标识列再做填充缺失低于 5% 的数值列填中位数类别列填众数。# 缺失率分层处理 missing_rate df.isnull().mean() # 高缺失列丢弃 drop_cols missing_rate[missing_rate 0.6].index.tolist() df df.drop(columnsdrop_cols) # 中缺失列加标识再填充 mid_missing missing_rate[(missing_rate 0.05) (missing_rate 0.6)].index.tolist() for col in mid_missing: df[col _is_missing] df[col].isnull().astype(int) if df[col].dtype object: df[col] df[col].fillna(df[col].mode()[0]) else: df[col] df[col].fillna(df[col].median()) # 低缺失列直接填充 low_missing missing_rate[missing_rate 0.05].index.tolist() for col in low_missing: if df[col].dtype object: df[col] df[col].fillna(df[col].mode()[0]) else: df[col] df[col].fillna(df[col].median())这里的关键逻辑是缺失标识列保留了“曾经缺失”这个信息模型可以学到白户和正常用户的差异。填充值选中位数而不是均值是因为金融数据普遍右偏均值容易被极端值拉跑。类别列用众数填充是常规操作但如果某个类别列缺失率接近 60%加标识列后填充众数可能会引入噪声这时候可以考虑直接丢弃该列。2.3 类别变量编码WOE 还是独热风控建模里类别变量的处理有个行业惯例叫 WOEWeight of Evidence编码它把每个类别映射成该类别下违约与正常样本比值的对数。好处是编码后的值跟标签单调相关逻辑回归的系数解释起来很直观。但 WOE 编码有个坑必须基于训练集计算然后应用到验证集和测试集否则会数据泄露。def calc_woe(df, col, target): 计算 WOE 编码返回映射字典 grouped df.groupby(col)[target].agg([sum, count]) grouped.columns [bad, total] grouped[good] grouped[total] - grouped[bad] # 防止除零加平滑项 grouped[bad] grouped[bad].replace(0, 0.5) grouped[good] grouped[good].replace(0, 0.5) total_bad grouped[bad].sum() total_good grouped[good].sum() grouped[woe] np.log((grouped[bad] / total_bad) / (grouped[good] / total_good)) return grouped[woe].to_dict() # 只在训练集上计算 WOE 映射 train_df, test_df df.iloc[:int(len(df)*0.7)], df.iloc[int(len(df)*0.7):] woe_map calc_woe(train_df, occupation, default) train_df[occupation_woe] train_df[occupation].map(woe_map) test_df[occupation_woe] test_df[occupation].map(woe_map)参数说明replace(0, 0.5)是拉普拉斯平滑的简化版防止某个类别下没有违约样本导致 log 无穷大。total_bad和total_good是全量统计不是分组统计。WOE 映射必须保存下来线上推理时用同一套映射否则线上线下不一致这是风控模型上线翻车的经典原因之一。3. 逻辑回归建模的核心从损失函数到评分卡刻度3.1 逻辑回归损失函数到底在优化什么逻辑回归的损失函数是对数似然损失的负数也叫交叉熵损失。公式写出来是L(w) -1/N * Σ [y_i * log(p_i) (1-y_i) * log(1-p_i)]其中 p_i 1 / (1 exp(-w^T x_i))。这个函数的意义是当真实标签 y_i1 时模型预测概率 p_i 越接近 1log(p_i) 越接近 0损失越小反之 p_i 越接近 0log(p_i) 负得越多损失越大。逻辑回归没有闭式解实践中用梯度下降或者拟牛顿法L-BFGS迭代求解。sklearn 的LogisticRegression默认用 L-BFGS收敛快且稳定小数据集上基本不用调。from sklearn.linear_model import LogisticRegression from sklearn.preprocessing import StandardScaler from sklearn.model_selection import train_test_split from sklearn.metrics import roc_auc_score, classification_report # 特征与标签分离 feature_cols [c for c in train_df.columns if c not in [default, occupation]] X_train train_df[feature_cols].select_dtypes(include[np.number]) y_train train_df[default] X_test test_df[feature_cols].select_dtypes(include[np.number]) y_test test_df[default] # 标准化逻辑回归对特征尺度敏感 scaler StandardScaler() X_train_scaled scaler.fit_transform(X_train) X_test_scaled scaler.transform(X_test) # 训练模型加 L2 正则防止过拟合 model LogisticRegression( penaltyl2, C1.0, solverlbfgs, max_iter1000, class_weightbalanced ) model.fit(X_train_scaled, y_train) # 评估 y_prob model.predict_proba(X_test_scaled)[:, 1] print(AUC:, roc_auc_score(y_test, y_prob)) print(classification_report(y_test, model.predict(X_test_scaled)))参数上重点说三个C是正则化强度的倒数C 越小正则越强风控场景一般从 0.1 到 10 之间调AUC 变化不大就选小一点的防过拟合class_weightbalanced在样本不均衡时自动调整权重等价于按类别频率反比加权max_iter设 1000 是保险值如果收敛警告还在刷要么加迭代次数要么检查特征是不是有共线性。3.2 特征筛选VIF 和 IV 值怎么配合用逻辑回归对多重共线性敏感两个高度相关的特征同时入模会导致系数符号反转解释性直接崩掉。我一般先用 IVInformation Value值筛一遍单变量预测力IV 低于 0.02 的特征直接丢然后用 VIF方差膨胀因子检查共线性VIF 大于 10 的成对特征保留 IV 高的那个。from statsmodels.stats.outliers_influence import variance_inflation_factor def calc_iv(df, col, target): 计算单个特征的 IV 值 grouped df.groupby(col)[target].agg([sum, count]) grouped.columns [bad, total] grouped[good] grouped[total] - grouped[bad] grouped[bad] grouped[bad].replace(0, 0.5) grouped[good] grouped[good].replace(0, 0.5) grouped[dist_bad] grouped[bad] / grouped[bad].sum() grouped[dist_good] grouped[good] / grouped[good].sum() grouped[iv] (grouped[dist_bad] - grouped[dist_good]) * np.log(grouped[dist_bad] / grouped[dist_good]) return grouped[iv].sum() # 计算所有数值特征的 IV iv_dict {} for col in X_train.columns: try: iv_dict[col] calc_iv(train_df, col, default) except Exception: iv_dict[col] 0 # 筛选 IV 0.02 的特征 selected [k for k, v in iv_dict.items() if v 0.02] print(IV 筛选后特征数:, len(selected)) # VIF 检查 X_vif X_train[selected].copy() vif_data pd.DataFrame() vif_data[feature] X_vif.columns vif_data[VIF] [variance_inflation_factor(X_vif.values, i) for i in range(X_vif.shape[1])] print(vif_data.sort_values(VIF, ascendingFalse).head(10))IV 值的经验判断标准小于 0.02 预测力极弱0.02 到 0.1 弱0.1 到 0.3 中等0.3 到 0.5 强大于 0.5 要警惕过拟合。VIF 超过 10 说明共线性严重处理方式通常是删掉一个或者做 PCA 降维但风控场景为了可解释性一般选择删特征而不是降维。3.3 评分卡转换把概率变成业务能用的分数逻辑回归输出的是违约概率业务端用的是 300 到 850 的信用分。转换公式是score A - B * log(odds)其中 odds p / (1-p)A 和 B 是刻度参数。通常设定某个基准分对应某个基准 odds比如 600 分对应 odds50每增加 20 分 odds 翻倍就能解出 A 和 B。import numpy as np def prob_to_score(prob, A, B): 概率转评分 odds prob / (1 - prob) return A - B * np.log(odds) # 设定基准600 分对应 odds50每 20 分 odds 翻倍 base_score 600 base_odds 50 pdo 20 # points to double the odds B pdo / np.log(2) A base_score B * np.log(base_odds) # 对测试集打分 scores prob_to_score(y_prob, A, B) print(评分范围:, scores.min(), scores.max()) print(评分均值:, scores.mean())参数说明pdo是行业惯例20 或 50 都常见pdo 越小分数对概率变化越敏感。base_odds和base_score根据业务定位调整消费金融通常基准分设在 600 左右。转换后的分数要检查分布如果大量用户挤在 300 到 400 区间说明模型区分度不够得回头查特征工程。4. 避坑与排查逻辑回归风控建模最常见的五个翻车点4.1 现象AUC 0.85 但上线后坏账率没降原因训练集和测试集的时间分布不一致。金融数据有时间衰减效应用 2022 年数据训练、2023 年数据测试用户行为模式已经变了。更隐蔽的情况是测试集里混入了未来信息比如“近三个月逾期次数”这个特征在测试集里用了训练集之后的数据计算。解决按时间切分数据集训练集用早期数据测试集用后期数据。所有涉及时间窗口的特征计算窗口的截止时间必须早于标签生成时间。做时间外样本验证OOTAUC 下降超过 0.05 就要警惕。4.2 现象模型系数符号跟业务常识相反原因多重共线性导致系数反转。比如“收入”和“负债比”高度负相关同时入模后收入系数变成负的看起来像“收入越高违约率越高”。解决先算 VIF把 VIF 大于 10 的特征成对检查保留业务解释更合理的那个。另一个办法是用 L1 正则做特征选择L1 会把不重要的特征系数压到零自然处理共线性。4.3 现象WOE 编码后训练集 AUC 很高测试集崩了原因WOE 映射在全量数据上计算训练集和测试集共享了标签信息属于数据泄露。解决WOE 映射只在训练集上计算保存成字典或 JSON 文件测试集和线上推理用同一份映射。如果某个类别在测试集里没出现过映射不到就填 0 或者训练集的全局 WOE 均值。4.4 现象评分卡分数集中在 500 到 550 之间区分度差原因逻辑回归输出的概率本身集中在 0.5 附近经过 log-odds 转换后分数范围被压缩。解决检查特征工程是否充分IV 值筛选后保留的特征是不是预测力都不够。另一个可能是标准化没做逻辑回归对特征尺度敏感收入这种万元级别的特征和年龄这种十位数的特征混在一起梯度下降会偏向大尺度特征。用 StandardScaler 统一量纲后再训练。4.5 现象训练时收敛警告刷屏模型系数巨大原因特征没有标准化或者存在完全分离某个特征能完美区分正负样本。完全分离在风控小数据集里很常见比如某个职业类别下全是违约用户。解决先标准化。如果还有问题检查是否有特征的某个类别只对应一种标签有的话要么合并类别要么加正则。LogisticRegression的penaltyl2和C调小能缓解但根本解法是处理数据。5. 从课程设计到可复现项目我的参数调优习惯和验证套路课程设计做完能跑通只是第一步真正让这套东西有复现价值的是参数调优和验证流程的规范化。我自己的习惯是先把整个流程拆成配置文件驱动所有路径、超参数、阈值都写在一个 YAML 里换数据集只改配置不改代码。逻辑回归的可调参数不多但每个都值得认真对待。# config.yaml 示例结构 # data: # train_path: data/train.csv # test_path: data/test.csv # target_col: default # model: # C: 0.5 # penalty: l2 # class_weight: balanced # max_iter: 2000 # scorecard: # base_score: 600 # base_odds: 50 # pdo: 20参数调优我一般用网格搜索配合交叉验证但风控场景不能随机切分要用分层 K 折保证每折里正负样本比例一致。StratifiedKFold是标配折数选 5 折折太多小数据集每折样本不够折太少方差大。from sklearn.model_selection import StratifiedKFold, GridSearchCV from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(solverlbfgs, max_iter2000, class_weightbalanced)) ]) param_grid { clf__C: [0.01, 0.1, 0.5, 1.0, 5.0], clf__penalty: [l1, l2] } cv StratifiedKFold(n_splits5, shuffleTrue, random_state42) grid GridSearchCV(pipe, param_grid, cvcv, scoringroc_auc, n_jobs-1) grid.fit(X_train, y_train) print(最优参数:, grid.best_params_) print(最优 AUC:, grid.best_score_)这里有个细节penaltyl1需要solver支持lbfgs 不支持 L1得换成liblinear或saga。Pipeline 的好处是标准化和模型训练绑在一起交叉验证时每一折的标准化参数只在该折的训练部分计算避免了数据泄露。scoringroc_auc是因为风控场景正负样本不均衡准确率没有参考价值AUC 更能反映排序能力。验证套路我坚持三条第一时间外样本验证必须有按时间切分后训练集和测试集至少间隔一个月第二KS 值比 AUC 更贴近业务KS 大于 0.3 才算可用模型低于 0.2 基本没有区分度第三评分分布要画出来看好的评分卡应该呈正态分布大量用户集中在中间分数段两端稀疏。from scipy.stats import ks_2samp def calc_ks(y_true, y_prob): 计算 KS 值 good y_prob[y_true 0] bad y_prob[y_true 1] ks_stat, _ ks_2samp(good, bad) return ks_stat ks calc_ks(y_test.values, y_prob) print(KS 值:, ks)KS 的计算逻辑是拿好人样本的预测概率分布和坏人样本的预测概率分布做对比最大差距就是 KS。KS 大于 0.3 说明模型能把好人和坏人拉开距离业务上才有应用价值。如果 KS 低于 0.2别急着调参先回去看特征工程大概率是特征没选对。最后说一个我踩过的坑课程设计里数据集通常已经清洗过但真实场景拿到的数据脏得多。有一次我用课程设计里跑通的代码去处理一份真实信贷数据结果 WOE 编码时某个类别只有 3 个样本WOE 值算出来是正无穷模型直接崩了。后来加了最小样本量阈值类别样本少于 50 个就合并到“其他”类。这个经验写进了我的标准流程里每次做 WOE 之前先检查类别分布。希望帮到你。本文还有配套的精品资源点击获取