ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

贷中风险预测Python实战:从特征工程到模型部署全流程

贷中风险预测Python实战:从特征工程到模型部署全流程 简介面向金融风控场景的机器学习贷中风险预测模型资料包覆盖数据预处理、特征工程、模型训练与评估全流程适合高校人工智能、金融科技、电子信息等专业学生、竞赛选手及从业者参考学习也可直接用于毕业设计、课程设计或项目立项演示。压缩包共49个文件含19个Python脚本、5个Jupyter Notebook、11个CSV数据文件并配套docx赛题说明、xlsx结果文件、PPT汇报报告整体大小约10.97MB目录结构层次分明便于按模块查找代码与数据。资源围绕“江苏银行杯”金融大数据建模挑战赛的贷中风险预测赛题展开提供初赛作品方案、数据字段示例说明、特征提取思路笔记以及随机森林、LightGBM、XGBoost、朴素贝叶斯等常用分类模型的Python实现帮助读者从原始流水表、客户信息表等数据处理做起完整走通贷中评分卡建模链路。目前已有69人学习下载代码经过严格测试可正常运行既适合小白进阶也方便中高级学习者在此基础上改进算法或扩展功能是一份工程完整性与学习价值兼备的实战资源。1. 贷中风险预测为什么说这个 Python 方案是风控岗的实用参考贷中风险预测在信贷业务里的位置很微妙。它不像贷前那样有充分的申请资料和数据可供建模也不像贷后那样已经进入催收流程、动作相对固定。它发生在授信之后、还款结束之前的整个存续期目标是回答一个问题这位客户在未来的某个时间窗口里违约概率有多高这个问题听起来简单但做起来牵扯到行为数据、还款表现、额度使用率、外部征信变动等一堆变量的交叉作用。拿到这个标题对应的压缩包你得到的其实是一套完整的机器学习建模流程从数据预处理、特征工程、模型训练到评估报告的代码实现外加文档和 PPT 报告。这类方案的价值不在于它用了多么前沿的算法而在于它把贷中风险预测这条链路走通了。它适合三类人一是刚转岗到风控建模的同学想看看完整的代码骨架长什么样二是已经在做贷前模型、想横向拓展到贷中场景的分析师三是需要快速给领导或业务方做一版贷中风险预测原型演示的从业者。我后续会把这个方案拆开讲包括数据怎么处理、特征怎么构造、模型怎么调、以及真实业务里最容易出问题的那些细节。有一点先说在前面任何贷中模型的代码包真正值钱的不是那几层神经网络或者 XGBoost 的几行调用而是数据定义、观察期和表现期的切分方式、以及标签的生成逻辑。这些决定了模型上限算法只决定你能多接近那个上限。2. 数据与特征工程为什么贷中模型一半的功夫在特征上2.1 贷中建模的数据窗口观察期、表现期与样本切分贷中风险预测和贷前最大的区别在于时间窗口的划分。贷前建模用的是申请时点的截面数据而贷中建模天然是纵向数据——同一个客户在每个月甚至每天的状态都不一样。理解这个区别是读懂整个源码包的关键。观察期Observation Window指的是用来计算特征的时间段。常规做法是取最近 3 个月或 6 个月的客户行为数据。表现期Performance Window则是用来定义标签的时间段贷中场景一般取未来 3 个月是否逾期有些机构也看未来 6 个月。观察期和表现期必须严格不重叠这是底线。import pandas as pd import numpy as np def make_labels(df, obs_end_date, perf_months3): 生成贷中表现期标签观察期结束后的 perf_months 个月内是否发生逾期 df 必须包含 customer_id, month, overdue_flag 三列 perf_start obs_end_date pd.DateOffset(months1) perf_end obs_end_date pd.DateOffset(monthsperf_months) perf df[(df[month] perf_start) (df[month] perf_end)] label perf.groupby(customer_id)[overdue_flag].max().reset_index() label.columns [customer_id, label] label[label] label[label].astype(int) return label # 使用示例以 2023 年 6 月为观察期截点预测未来 3 个月违约 # label_df make_labels(raw_df, obs_end_datepd.Timestamp(2023-06-30), perf_months3)这段代码的逻辑很直白以观察期结束日的下一个月作为表现期起点往后数 perf_months 个月只要其中任意一个月发生了逾期就拿这个客户标记为正样本。用 max() 函数聚合是因为我们关心的是“是否发生过”而不是“发生了几次”。这里的参数 perf_months 是贷中模型里最重要的一个旋钮。调成 1标签更贴近短期风险样本量更大但正样本可能变少调成 6标签更稳定但损失了时效性而且客户在这半年里可能已经还清又再借标签纯度下降。我见过不少团队在 3 和 6 之间反复横跳最终取 3 居多因为贷中模型服务于贷中管理动作——降额、止贷、加担保这些动作的决策周期通常是按月计的。2.2 特征构造行为特征、用信特征与趋势特征的组合贷中模型的特征体系一般分三大块。第一块是客户基本属性这个在贷前就有了但贷中模型会考虑它随时间的变化比如年龄增长、职业状态变更。第二块是行为特征这是贷中建模的核心增量——每个月的还款行为、逾期记录、提前还款次数、还款渠道变化。第三块是用信特征包括额度使用率、近三个月平均使用率、单笔最大用信金额、用信频次等。源码包里的特征工程部分通常会包含趋势类特征这是贷中区别于贷前的关键维度。比如近 3 个月额度使用率的斜率和波动率比时点值更能反映客户资金链紧张的趋势。def gen_trend_features(df, window3): 生成额度使用率趋势特征均值、标准差、线性回归斜率 df 需包含 customer_id, month, utilization 列month 已排序 from scipy import stats def _slope(y): if len(y) 2: return 0.0 x np.arange(len(y)) slope, _, _, _, _ stats.linregress(x, y) return slope g df.groupby(customer_id)[utilization] trend pd.DataFrame({ util_mean: g.transform(lambda x: x.rolling(window, min_periods1).mean()), util_std: g.transform(lambda x: x.rolling(window, min_periods1).std().fillna(0)), util_slope: g.transform(lambda x: x.rolling(window, min_periods2).apply(_slope, rawFalse)) }) return trend这段代码里有个细节值得注意rolling(window, min_periods1) 表示窗口不足 3 个月时不丢弃数据而是用已有的 1 个月或 2 个月凑合算。对于新开户客户来说强行要求满窗口会导致特征大面积缺失min_periods 给个 1 能保住样本量。但你要记住这样做出来的斜率在窗口很短时不具备统计显著性所以一般会额外加一个“在贷月数”的特征让模型自行学习不同样本宽度下的可信度。2.3 目标泄露贷中建模最隐蔽的坑我必须单独把目标泄露拿出来说因为这个坑在贷中模型里出现的频率远高于贷前。所谓目标泄露就是特征里包含了未来信息模型训练时表现极好一上线就崩。最常见的泄露来源有两个。第一个是特征表中包含了“当前逾期天数”这类变量。听着没问题对吧但如果你用未来 3 个月是否逾期做标签而特征计算日距离今天已经过去了 20 天那这 20 天里发生的逾期既在特征里又在标签里模型等于直接看到了答案。# 反面教材这段代码看起来很合理但包含了目标泄露 # feature_df[overdue_days] loan_df[current_overdue_days] # 绝对不能直接用 # 正确做法是在观察期截点当天计算且截点之后的逾期表现只能进入标签第二个泄露来源是对全量数据求统计量。比如你计算客户的“历史平均还款金额”如果用了他未来几个月的还款记录就泄露了。屠龙刀解法是严格按时间截断特征只用截点之前的数据标签只用截点之后的数据。源码包里如果代码写得规范你会看到一个 as_of_date 参数贯穿着特征计算的全过程这就是为了保证同一时间截点下特征和标签互不干扰。3. 训练与验证选对算法不如选对评估口径3.1 为什么贷中模型默认选择树模型而不是深度学习贷中风险预测的任务类型是典型的二分类问题而数风控场景默认的起手式就是梯度提升树。不管是 XGBoost、LightGBM 还是 CatBoost它们在贷中建模上的表现通常优于深度学习模型原因有三一是信贷特征大多是有业务含义的表格数据树模型对特征尺度不敏感不需要像神经网络那样做复杂的标准化二是树模型能天然处理缺失值而实际信贷数据里缺失率超过 30% 的特征比比皆是三是模型可解释性要求银行和消金公司的风控审批需要满足合规要求树模型可以输出每个特征的重要性深度学习很难做到这种程度的透明。from sklearn.model_selection import train_test_split from lightgbm import LGBMClassifier from sklearn.metrics import roc_auc_score, precision_recall_curve # 假设 X 是特征矩阵y 是标签0/1train_df 是建模样本 X_train, X_test, y_train, y_test train_test_split( X, y, test_size0.3, random_state42, stratifyy ) model LGBMClassifier( n_estimators300, learning_rate0.05, max_depth4, num_leaves16, min_child_samples50, subsample0.8, colsample_bytree0.8, random_state42 ) model.fit(X_train, y_train, eval_set[(X_test, y_test)], eval_metricauc)参数设置这块我多解释几句。max_depth 设 4、num_leaves 设 16是刻意压低模型复杂度。贷中模型的样本量通常在几万到几十万特征维度几十到几百如果 max_depth 拉到 8 以上树很容易学出针对个别样本的路径泛化能力反而变差。min_child_samples 设 50 是约束每个叶子的最小样本量避免叶子节点只有十几个样本导致的过拟合。3.2 评估指标AUC 是给领导看的KS 和 PR 曲线是给自己看的贷中模型评估有一个明显的分层现象。写 PPT 汇报时大家都讲 AUC但是实际调模型时懂行的人看的是 KS 和 PR 曲线。原因在于贷中场景的正样本比例极低通常只有 2% 到 5%这种极度不平衡的数据下AUC 会虚高——即使你把所有样本都预测为负样本准确率也有 95% 以上AUC 仍然不会太低但模型毫无区分能力。from sklearn.metrics import roc_curve # KS 统计量 TPR 与 FPR 的最大差值衡量模型区分正负样本的最大能力 y_prob model.predict_proba(X_test)[:, 1] fpr, tpr, thresholds roc_curve(y_test, y_prob) ks max(tpr - fpr) print(fKS: {ks:.4f}) # 在贷中业务中KS 在 0.3 以上才勉强可用0.4 以上算良好 # 如果 KS 低于 0.25优先回去查特征而不是继续调参这段代码的用途很直接——帮你在模型训练完之后算出一个核心指标。里面 KS 的计算方式是贷中模型和贷前模型共用的评估方法KS 值越高代表模型区分好坏客户的能力越强。业界有句话叫“KS 不过 3 不投产”意思就是 KS 达不到 0.3 的模型上线后不会有明显的业务价值。如果遇到 KS 偏低的情况树脂思路是回到特征工程环节重点看趋势特征和循环借贷特征是否构造完整。PR 曲线则是另一个视角它关注的是在给定召回率下精确率能保持多高。贷中模型的实际场景是你预测一万个客户会违约然后针对性地降额或止贷。这时候如果精确率太低意味着误伤了太多正常客户业务方会来找你麻烦如果召回率太低意味着漏掉了很多真正的坏客户风险损失照样发生。PR 曲线能在两者之间帮你找到平衡点这个平衡点最终会转化成业务上的拦截阈值。3.3 样本不均衡处理不要上来就过采样贷中模型的正样本比例低是常态但处理方式因人而异。新手最常见的错误是一上来就 SMOTE 过采样把正样本凑到和负样本一样多。这样做训练出来的模型概率值会被严重抬高实际上线时需要对概率做大幅度的校准而且过采样后的样本相关性会导致验证集上的指标虚高上线后表现缩水。处理贷中样本不平衡正确顺序是这样首先尝试不对样本做任何操作直接训练模型看 KS 是否能到 0.3。如果达不到第二步是调整 class_weight 参数给正样本更高的权重这个操作比过采样温和得多。只有这两步都无效时再考虑过采样但过采样比例不要追求 1:1一般调整到 1:3 或 1:5 就足够了。# 通过 scale_pos_weight 处理正负样本不平衡 # 经验值 负样本数 / 正样本数再乘以0.6~0.8 打个折扣避免过度矫正 neg_count (y_train 0).sum() pos_count (y_train 1).sum() model LGBMClassifier( scale_pos_weightneg_count / pos_count * 0.7, # 预留调优空间 n_estimators300, learning_rate0.05, max_depth4, num_leaves16, min_child_samples50, random_state42 )这里有个很实用的经验值scale_pos_weight 取负样本与正样本比值的 0.6 到 0.8 倍而不是直接用比值本身。因为直接用完整比值往往让模型过度关注少数类导致误杀率飙升。这个参数需要配合验证集的 PR 曲线一起看目标是让精确率和召回率的平衡点落在业务可接受的区间内。4. 工程化落地从离线训练到线上部署的最后一公里4.1 模型文件管理训练完不等于能用贷中模型从训练完成到真正上线中间隔着好几步工程化操作。首先是模型持久化——把训练好的模型保存成文件线上服务加载它来做预测。这个过程看似简单但有个很隐蔽的坑模型文件需要配套保存特征列表否则线上预测时特征顺序不一致LightGBM 会自动按特征名匹配但如果你用了一些奇奇怪怪的框架特征顺序错位会导致预测结果完全乱掉。import joblib # 保存模型文件和特征清单二者必须成对出现 joblib.dump(model, loan_risk_model.pkl) with open(feature_cols.txt, w, encodingutf-8) as f: f.write(\n.join(feature_names)) # 线上加载时校验特征一致性 loaded_model joblib.load(loan_risk_model.pkl) with open(feature_cols.txt, r, encodingutf-8) as f: saved_features [line.strip() for line in f.readlines()] assert saved_features feature_names, 特征列表不一致禁止上线这段代码的逻辑是把离线训练时用的特征列名存储为文件线上服务启动时先做一次校验确认当前请求的特征列表与训练时完全一致如果不一致就直接抛出异常。这段代码在实际生产环境里能救命的场景比你想的多得多——最典型的当你改了特征工程代码但忘记重新训练模型线上就会用新旧混杂的特征去做预测结果没人能看懂最后大家把锅甩给“模型漂移”。4.2 线上预测的输入输出设计概率值要经过校准才能给业务用模型输出的原始概率值直接扔给业务方是会被吐槽的。业务方不需要知道“违约概率 0.73”意味着什么他们需要的是一个明确的动作指令。贷中场景最常见的做法是把概率值映射成风险等级或直接给出决策建议。def risk_decision(prob, threshold_low0.3, threshold_high0.7): 将模型概率映射为贷中管理动作 if prob threshold_high: return stop_loan, 建议止贷触发人工复核 elif prob threshold_low: return limit_reduce, 建议降额50%短信提醒 else: return normal, 正常监控维持额度 # 示例新客户评分结果应用 # decision, note risk_decision(0.65) # print(decision, note) # limit_reduce 建议降额50%短信提醒threshold_low 和 threshold_high 这两个阈值不是拍脑袋定的它们应该来自训练集 PR 曲线上的关键拐点。比如你观察到精度在 0.3 概率以上显著上升、0.7 概率以上达到 90% 以上那就把这两个点作为分档阈值。初始化上线时可以先用固定阈值后面月度监控时再根据实际表现做调整。4.3 模型监控贷中模型为什么每月都要看一眼贷中场景的模型衰减速度通常比贷前快得多。原因很简单贷中模型的输入特征以客户行为数据为主而行为数据会随着宏观经济、信贷政策、产品规则的变化而剧烈波动。一个在疫情前训练的贷中模型放到疫情后的客群上大概率失效。源码包里如果包含监控相关的文档一般会让你维护一个监控看板核心看三样东西每日预测概率分布、每日 KS 或 AUC用 T1 个月确认标签滚动计算、特征分布漂移。# 用 PSIPopulation Stability Index检测特征漂移 def calculate_psi(expected, actual, bins10): 计算单个特征的 PSI 值超过 0.25 说明特征分布发生显著漂移 expected_counts np.histogram(expected, binsbins)[0] 1e-6 actual_counts np.histogram(actual, binsbins)[0] 1e-6 expected_pct expected_counts / expected_counts.sum() actual_pct actual_counts / actual_counts.sum() psi np.sum((actual_pct - expected_pct) * np.log(actual_pct / expected_pct)) return psi这段代码里用到的 PSI 指标是风控模型监控里非常常用的手段。它的原理是分箱计算两个时间段里特征分布的变化程度。PSI 小于 0.1 表示特征稳定0.1 到 0.25 表示轻度漂移大于 0.25 就说明这个特征已经完全变了模型预测结果不再可信。到 drift 严重的时候该做的不是微调参数而是重新跑一遍特征工程把当前月份的样本纳入训练集重新建模。5. 拆包、复现与踩坑实录这套方案常见的四个问题5.1 压缩包解压后文件路径报错根源在中文目录和编码这是拿到这类 zip 源码包后最高频的翻车现场。压缩包里的代码如果是在 Linux 或 Mac 环境中编写的默认编码是 UTF-8但很多人解压到 Windows 后直接用 Jupyter Notebook 打开Python 解释器在读取带有中文路径和文件名的数据时很容易报 UnicodeDecodeError。处理办法也很简单——统一在项目根目录下创建一个 config.py集中管理所有路径常量并且用 Pathlib 替代字符串拼接。from pathlib import Path BASE_DIR Path(__file__).parent.resolve() DATA_DIR BASE_DIR / data MODEL_DIR BASE_DIR / model FIGURE_DIR BASE_DIR / figures # 读取数据文件时不建议写死绝对路径 # train_df pd.read_csv(D:/下载/贷中模型/data/train.csv) # 换个电脑就崩 # 推荐写法用相对路径基于 BASE_DIR 拼接 train_df pd.read_csv(DATA_DIR / train.csv)这里把路径管理单独抽出来的意义在于当你把整个项目目录拷贝到服务器或者同事电脑上时不需要修改任何一行代码。绝大多数源码包自带的数据文件用相对路径就能跑通如果你发现怎么都报 FileNotFoundError先去检查当前工作目录是不是项目根目录而不是急着改代码。5.2 机器学习依赖包版本冲突Python 版本不兼容导致 LightGBM 装不上源码包里的 requirements.txt 通常只会列依赖名和版本号但 Python 版本本身的兼容性问题往往被忽略。比如 LightGBM 在 Python 3.12 上的安装就经常翻车需要提前编译或等待 wheel 包更新。如果遇到类似情况比较稳妥的做法是新建一个虚拟环境指定 Python 3.9 或 3.10 来跑项目。# 创建干净环境并安装依赖 conda create -n loan_risk python3.9 -y conda activate loan_risk pip install -r requirements.txt # 如果 LightGBM 安装失败尝试用 conda 装 conda install -c conda-forge lightgbm用 conda 而不是 pip 安装 LightGBM 的原因是 conda 会自动处理底层库的兼容性包括 Microsoft Visual C 运行库和 OpenMP 的依赖。代码包里如果涉及模型的可复现性还可以加一句随机种子的统一设置——把 random_state 固定成同一个值在所有涉及随机过程的环节传入否则每次训练出来的模型都不一样后续分析和汇报都没法对齐。5.3 “代码报错但找不到原因”先看数据再看代码这类源码包第一个常见的坑是数据文件不完整或者列名不一致。压缩包里如果是脱敏数据特征列常常叫 f1、f2、f3 这种匿名方式和代码里硬编码的 feature_name 对不上。跑之前先把数据路径下的 csv 文件读进来打印一下列名列表逐项和代码里的特征清单比对。# 数据列名校验脚本跑任何模型前先执行 expected_cols [customer_id, month, utilization, repay_amount, delq_flag] df pd.read_csv(DATA_DIR / raw_data.csv, nrows5) missing_cols [c for c in expected_cols if c not in df.columns] if missing_cols: raise ValueError(f缺少列: {missing_cols}, 请检查数据文件是否完整) else: print(列名校验通过)这个脚本的核心思路是在数据进入建模流程之前设置一道明确的防线。缺失列直接报错退出避免在乱序或缺失的数据上跑出结果后面才发现问题。数据校验在金融建模里不是可有可无的仪式是每一天都可能用到的手段。5.4 模型效果和文档描述差距大八成是时间窗口切分方式不同最后一条踩坑记录针对的正是这套源码包最常见的问题。读者在本地跑出来的 AUC 和 PPT 里展示的数字对不上原因大概率在于观察期和表现期的切分方式不同。PPT 里写的是 2022 年 1 月到 6 月做观察期、7 月到 9 月做表现期但代码默认参数可能是用最近 3 个月做观察期、下个月做表现期样本不同效果自然不同。遇到这种情况不用慌。先在代码里找到构建样本的函数把时间参数打印出来确认。确认后用和文档一致的参数重新跑一遍。如果还差很多再去检查是否有特征或标签被去重逻辑误伤。这种事常被归为玄学但按时间参数、特征逻辑、标签逻辑的排查顺序走一遍十有八九能找到原因和出路。6. 从样本加权到拦截阈值三个直接影响业务收益的调参技巧6.1 时间衰减权重让模型更“记仇”最近几个月的坏客户贷中客群的行为模式会随时间变化一个常见做法是在训练时给样本加时间衰减权重——最近月份的样本权重更高较早的样本权重低一些。这样模型学到的模式更贴近当下的客群状态而不是被半年前的旧数据拖后腿。def time_weight(month, current_month, half_life3): 时间衰减权重half_life3 表示每过3个月样本权重减半 month_diff (current_month - month).days / 30.0 return 0.5 ** (month_diff / half_life) # train_df[weight] train_df[month].apply( # lambda m: time_weight(m, obs_end_date) # ) # 然后把 weight 列传给 LightGBM 的 sample_weight 参数样本权重和 scale_pos_weight 是独立的维度前者控制时间远近的相对重要性后者控制正负样本的相对比例。两者可以同时使用互不干扰。在贷中模型的效果季度表现下滑时加上时间衰减权重往往会是有效手段而且它只需要改一个参数不像重新建模那样伤筋动骨。6.2 拦截阈值复盘用滚动验证找最优切点贷中模型的拦截阈值不是上线前定一次就完事。业务节奏在变、客群结构在变固定阈值必然导致次优决策。有一个相对轻量的做法是每个月月末做一次滚动验证用过去 6 个月的真实还款表现回算不同拦截阈值下的业务收益率挑本月收益率最高的阈值作为下个月的参数。def find_optimal_threshold(y_true, y_prob, cost_good1, cost_bad5): 简单损益模拟根据误杀成本与漏杀成本寻找最优阈值 from sklearn.metrics import precision_recall_curve precision, recall, thresholds precision_recall_curve(y_true, y_prob) best_thresh 0.5 best_profit -np.inf for p, r, t in zip(precision, recall, thresholds): profit r * cost_bad - (1 - p) * cost_good if profit best_profit: best_profit profit best_thresh t return best_thresh, best_profit这段代码背后的逻辑是实际业务的简化拦住一个坏客户能减少的损失记作 cost_bad误伤一个正常客户对应的机会成本记作 cost_good。当两者比值关系清晰时滚动计算能算出当前月的最优阈值。cost_bad 和 cost_good 需要业务方给数通常是贷中管理团队拍板或根据历史催收回收率推算模型团队不要自己假设数值。6.3 拒绝高分样本回捞给模型一个纠错通道贷中模型实际运行中要做到有进有出。当前周期被模型判断为高风险而降额的客户如果后续 3 个月还款表现良好是否应该恢复额度答案是应该。这个机制叫“高分回捞”在源码包里通常不会默认实现但它是贷中管理闭环的重要组成部分。实现方式不复杂每个月针对处于降额或止贷状态的客户单独跑一次轻量评估看最近 3 个月的行为特征是否有所改善。如果改善明显就自动恢复额度或者解除止贷。这个机制能在保证整体风险可控的前提下减少模型误伤带来的客户流失。这部分逻辑可以作为源码包基础上的二次开发方向而且业务价值容易被量化方便你向领导证明方案的长期用途。做了几年贷中模型我最大的习惯是每次训练完模型先问自己一句如果明天这个模型上线最可能在哪个环节出问题答案往往不是算法选型而是特征泄露、数据口径不一致或者阈值没校准。把自己代入业务场景去审视每一行代码模型才能真正成为可用的决策工具而不是停留在 Jupyter Notebook 里的成果。这套方案的价值正在于把这条路上的主要坑位都标了出来希望你跑通之后也能总结出自己的经验库。希望帮到你。本文还有配套的精品资源点击获取
返回列表