ARTICLE DETAIL

资讯详情

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

模式识别课程设计:Python信用卡风控四套评级模型实战

模式识别课程设计:Python信用卡风控四套评级模型实战 简介本资源面向模式识别课程学习者和金融风控入门者围绕银行信用卡场景设计了一套完整的风险评估模型方案涵盖申请人评级、行为评级、收款催收评级与欺诈评级四个方向帮助读者理解从数据到模型落地的全流程。压缩包共18个文件约7.67MB包含8个csv数据文件、4个Python源码、4个pkl模型文件以及字体与说明文档分别用于数据存储、模型训练、结果复现与运行说明。项目按数据预处理与分析、模型构建、模型评估与优化三阶段推进涉及缺失值与异常值处理、特征选择、可视化、降维及随机森林等机器学习方法并对准确度与稳定性进行交叉验证。已有130人学习适合希望掌握风控建模思路、对照源码复现实验或完成课程设计的读者参考。1. 模式识别课程设计里的信用卡风控四套评级模型到底在评什么银行信用卡业务里风险从来不是一个单点问题。申请那一刻要判断这个人该不该发卡、给多少额度这叫申请人评级发卡之后要盯着他会不会逾期、什么时候会逾期这叫行为评级真逾期了要判断这笔账还能不能收回来、该投入多少催收资源这叫收款评级催收评级还有一类人从一开始就是来骗钱的申请材料、消费行为全是伪造的这需要欺诈评级单独处理。模式识别课程把这四个场景打包成一个项目用 Python 从数据到模型跑一遍本质上是在训练一种能力面对一个真实的业务问题知道该选什么特征、用什么模型、怎么评估、怎么解释结果。这套东西适合正在做课程设计的学生也适合想从零搭一套风控评分卡原型的工程师。热搜里“模式识别”“python”“风险评估模型”“申请人评级模型”“欺诈评级模型”这几个词恰好对应了从特征工程到模型落地的完整链路。下面按实际动手的顺序拆开讲不绕弯子。2. 四套评级模型的数据底座特征怎么选、标签怎么定2.1 申请人评级与行为评级的特征差异申请人评级发生在贷前能拿到的只有申请表单和外部征信数据。常见特征包括年龄、收入、职业类型、学历、婚姻状况、居住时长、工作年限、已有信用卡数量、历史逾期次数、近半年查询次数。这些特征的特点是静态、稀疏、缺失率高。行为评级发生在贷后数据来自账户交易流水和还款记录特征变成动态的近 3 个月平均使用额度、近 6 个月逾期天数、还款率、取现比例、消费商户类型分布。行为评级的标签通常是“未来 6 个月是否逾期 30 天以上”申请人评级的标签是“放款后 12 个月内是否发生 90 天以上逾期”。选特征时有一个血泪经验不要直接把原始金额丢进模型。收入 5000 和 50000 的差异不是线性的通常要做分箱或者取对数。申请人评级里“年龄”这个特征22 岁和 45 岁的风险差异很大但 45 岁和 48 岁几乎没区别所以分箱比连续值更稳。2.2 收款评级与欺诈评级的标签构造收款评级催收评级的标签不是“是否逾期”而是“逾期后回收比例”。常见做法是把回收金额除以欠款金额分成高、中、低三档或者直接做回归预测回收率。欺诈评级的标签最特殊正样本极少通常不到 1%。这时候不能用准确率评估要看召回率和 AUC。欺诈特征包括申请时间集中在凌晨、设备 ID 频繁更换、联系人号码重复、单位地址与居住地址距离异常、首笔交易金额接近额度上限。import pandas as pd import numpy as np from sklearn.model_selection import train_test_split # 模拟申请人评级数据 np.random.seed(42) n 5000 data pd.DataFrame({ age: np.random.randint(20, 60, n), income: np.random.lognormal(8, 1, n), work_years: np.random.randint(0, 30, n), past_due_30d: np.random.poisson(0.3, n), query_6m: np.random.poisson(2, n), credit_count: np.random.randint(0, 10, n), }) # 标签12个月内是否逾期90天以上 logit -3 0.02*data[age] - 0.0001*data[income] 0.5*data[past_due_30d] 0.3*data[query_6m] prob 1 / (1 np.exp(-logit)) data[label] np.random.binomial(1, prob) # 分箱处理年龄 data[age_bin] pd.cut(data[age], bins[0, 25, 35, 45, 55, 100], labelsFalse) # 收入取对数 data[income_log] np.log1p(data[income]) X data[[age_bin, income_log, work_years, past_due_30d, query_6m, credit_count]] y data[label] X_train, X_test, y_train, y_test train_test_split(X, y, test_size0.3, stratifyy, random_state42) print(f训练集正样本比例: {y_train.mean():.3f})这段代码做了三件事生成模拟数据、构造标签、做基础特征变换。pd.cut把年龄切成五段np.log1p处理收入的长尾分布。stratifyy保证训练集和测试集的正负样本比例一致这在风控场景里很关键因为正样本本来就少随机切分可能导致测试集里一个正样本都没有。参数bins的边界不是拍脑袋定的通常要看业务含义25 岁以下刚毕业35 到 45 岁收入稳定55 岁以上临近退休。实际项目中这些边界要用卡方分箱或者决策树分箱自动找。2.3 缺失值处理与样本不平衡的实操选择申请人数据里“工作年限”缺失很常见因为自由职业者不填。处理方式有三种填中位数、填一个特殊值比如 -1、或者用模型预测。我一般会先填 -1 并加一个“是否缺失”的指示列让模型自己学。行为评级里“近 6 个月逾期天数”缺失往往意味着这个人没有信用卡这时候填 0 是合理的但欺诈评级里缺失可能本身就是信号。样本不平衡方面欺诈评级正样本可能只有 0.5%。不要一上来就 SMOTE先试试class_weightbalanced。如果还不行再用欠采样加集成。收款评级做回归时回收率分布通常双峰要么全收回来要么一分收不回这时候用分位数回归比普通线性回归更合适。3. 用 Python 把四套模型跑通从逻辑回归到 XGBoost 的选型与调参3.1 申请人评级逻辑回归评分卡为什么还是首选申请人评级模型在银行里必须可解释监管要求你能说清楚为什么拒绝一个人。逻辑回归的系数直接对应 WOE证据权重每个特征的分箱对应一个分数最后加起来就是总分。XGBoost 虽然 AUC 高但解释性差通常只用来做特征筛选或者和逻辑回归做融合。from sklearn.linear_model import LogisticRegression from sklearn.metrics import roc_auc_score, classification_report from sklearn.preprocessing import StandardScaler from sklearn.pipeline import Pipeline pipe Pipeline([ (scaler, StandardScaler()), (clf, LogisticRegression(class_weightbalanced, max_iter1000, C0.5)) ]) pipe.fit(X_train, y_train) y_prob pipe.predict_proba(X_test)[:, 1] print(fAUC: {roc_auc_score(y_test, y_prob):.4f}) print(classification_report(y_test, (y_prob 0.5).astype(int)))class_weightbalanced让正样本权重自动调高C0.5是正则化强度的倒数越小正则越强。风控场景里我一般会把 C 设在 0.1 到 1 之间防止过拟合。StandardScaler对逻辑回归不是必须的但加了之后系数大小可以直接比较方便做特征重要性排序。AUC 在 0.75 以上算可用0.8 以上算不错。如果 AUC 只有 0.6先检查标签定义是不是有问题再看特征有没有穿越用了未来信息。3.2 行为评级时间窗口滚动特征与 XGBoost 调参行为评级的特征需要按时间窗口滚动计算。比如“近 3 个月平均逾期天数”要用groupby加rolling。这里有个坑滚动窗口不能跨用户必须按用户 ID 分组后再滚。import xgboost as xgb from sklearn.metrics import roc_auc_score # 模拟行为数据 behavior pd.DataFrame({ user_id: np.repeat(range(500), 12), month: np.tile(range(12), 500), overdue_days: np.random.poisson(1, 6000), repay_ratio: np.random.beta(5, 2, 6000), }) # 按用户滚动计算近3个月平均逾期 behavior[overdue_3m] behavior.groupby(user_id)[overdue_days].transform( lambda x: x.rolling(3, min_periods1).mean() ) behavior[repay_3m] behavior.groupby(user_id)[repay_ratio].transform( lambda x: x.rolling(3, min_periods1).mean() ) # 取每个用户最后一个月作为样本 last behavior.groupby(user_id).tail(1).copy() last[label] (last[overdue_3m] 2).astype(int) Xb last[[overdue_3m, repay_3m]] yb last[label] Xb_train, Xb_test, yb_train, yb_test train_test_split(Xb, yb, test_size0.3, stratifyyb, random_state42) model xgb.XGBClassifier( n_estimators200, max_depth4, learning_rate0.05, subsample0.8, colsample_bytree0.8, scale_pos_weight3, eval_metricauc, use_label_encoderFalse ) model.fit(Xb_train, yb_train) print(f行为评级 AUC: {roc_auc_score(yb_test, model.predict_proba(Xb_test)[:,1]):.4f})rolling(3, min_periods1)表示窗口大小 3最少 1 个数据就计算。scale_pos_weight3是负样本数除以正样本数的近似值用来处理不平衡。max_depth4是风控场景的常用值树太深容易记住噪声。subsample和colsample_bytree都设 0.8增加随机性防止过拟合。如果 AUC 在训练集上 0.95、测试集上 0.7那就是过拟合了把max_depth降到 3n_estimators降到 100 再试。3.3 收款评级与欺诈评级的模型差异收款评级如果做回归预测回收率用XGBRegressor损失函数选reg:squarederror或者reg:quantileerror。欺诈评级用XGBClassifier但评估指标要看aucprPR 曲线下面积因为正样本太少ROC 曲线会过于乐观。from sklearn.metrics import average_precision_score # 欺诈评级模拟正样本比例约1% fraud pd.DataFrame({ device_change: np.random.poisson(0.5, 10000), night_apply: np.random.binomial(1, 0.1, 10000), contact_repeat: np.random.poisson(0.2, 10000), amount_ratio: np.random.beta(2, 5, 10000), }) fraud[label] ((fraud[device_change] 2) (fraud[night_apply] 1)).astype(int) Xf fraud[[device_change, night_apply, contact_repeat, amount_ratio]] yf fraud[label] Xf_train, Xf_test, yf_train, yf_test train_test_split(Xf, yf, test_size0.3, stratifyyf, random_state42) fraud_model xgb.XGBClassifier( n_estimators300, max_depth3, learning_rate0.03, scale_pos_weight50, eval_metricaucpr, use_label_encoderFalse ) fraud_model.fit(Xf_train, yf_train) yf_prob fraud_model.predict_proba(Xf_test)[:, 1] print(f欺诈评级 PR-AUC: {average_precision_score(yf_test, yf_prob):.4f})scale_pos_weight50是因为正样本大约 2%负正比约 50:1。eval_metricaucpr让模型在训练时直接优化 PR 曲线。max_depth3比行为评级更浅因为欺诈特征少树深了容易过拟合。PR-AUC 在 0.3 以上就值得进一步调0.5 以上算不错。如果 PR-AUC 只有 0.1先检查正样本定义是不是太宽泛把“欺诈”定义收紧一点再试。4. 避坑与排查四套模型落地时最容易翻车的五个地方4.1 标签穿越用了未来才知道的信息现象模型在测试集上 AUC 0.95上线后效果暴跌到 0.6。原因特征里混入了标签发生之后的数据。比如预测“未来 6 个月是否逾期”但特征里用了“近 6 个月逾期天数”这个特征在预测时点根本拿不到。解决画一张时间轴把每个特征的“可获取时间”标出来只保留预测时点之前能拿到的特征。申请人评级里“历史逾期次数”可以用但“本次申请后的查询次数”不能用。4.2 分箱边界过拟合现象训练集上每个分箱的坏样本率单调测试集上乱跳。原因分箱边界是在全量数据上找的包含了测试集信息。解决分箱只在训练集上做然后应用到测试集。用optbinning库或者手写卡方分箱确保边界来自训练集。如果某个分箱在测试集上样本数少于 5%合并到相邻箱。4.3 样本权重设置错误现象逻辑回归的截距项巨大预测概率全部偏向负样本。原因class_weightbalanced和手动sample_weight同时用了权重被乘了两次。解决二选一。如果用class_weight就不要在fit里再传sample_weight。另外评分卡通常要把概率校准到真实坏样本率CalibratedClassifierCV可以做这件事。4.4 滚动窗口跨用户现象行为评级的滚动特征在用户边界处出现异常值。原因rolling没有按用户分组把上一个用户的最后几个月和下一个用户的前几个月混在一起算了。解决先groupby(user_id)再transform或者用groupby.rolling。检查方法取一个用户的前 3 个月数据手动算平均值和代码结果对比。4.5 欺诈评级的评估指标选错现象欺诈模型准确率 99%但一个欺诈都没抓到。原因正样本只有 1%全预测为负样本就有 99% 准确率。解决看召回率和 PR-AUC。业务上通常要求召回率在 30% 到 50% 之间同时精确率不能太低否则人工审核量爆炸。可以画 PR 曲线找业务能接受的阈值点。5. 从课程设计到可复现的原型验证模型稳定性的三个技巧5.1 用 PSI 监控特征分布漂移PSI群体稳定性指标是风控里最常用的稳定性监控指标。计算方式把训练集和测试集或上线后的新数据按同一个分箱规则分箱算每个箱的样本占比差异再求和。PSI 小于 0.1 表示稳定0.1 到 0.25 表示轻微漂移大于 0.25 表示需要重新训练。def calculate_psi(expected, actual, bins10): # expected: 训练集特征值, actual: 测试集特征值 breakpoints np.percentile(expected, np.linspace(0, 100, bins1)) breakpoints[0] -np.inf breakpoints[-1] np.inf expected_counts np.histogram(expected, binsbreakpoints)[0] / len(expected) actual_counts np.histogram(actual, binsbreakpoints)[0] / len(actual) # 避免除零 expected_counts np.where(expected_counts 0, 0.0001, expected_counts) actual_counts np.where(actual_counts 0, 0.0001, actual_counts) psi np.sum((expected_counts - actual_counts) * np.log(expected_counts / actual_counts)) return psi # 对每个特征算 PSI for col in X_train.columns: psi_val calculate_psi(X_train[col].values, X_test[col].values) print(f{col}: PSI {psi_val:.4f})np.percentile用训练集的分位数做切分点保证分箱规则来自训练集。np.histogram统计每个箱的样本数。np.where把 0 替换成 0.0001 防止 log 报错。PSI 大于 0.25 的特征要重点看可能是业务变化导致也可能是数据采集出了问题。5.2 交叉验证的折数选择与时间序列切分风控数据有时间顺序不能用随机 K 折。要用TimeSeriesSplit保证训练集在测试集之前。折数一般选 5如果数据量少于 10000 条选 3 折。每折的 AUC 标准差如果大于 0.05说明模型不稳定需要检查特征或者增加数据。from sklearn.model_selection import TimeSeriesSplit, cross_val_score tscv TimeSeriesSplit(n_splits5) scores cross_val_score(pipe, X, y, cvtscv, scoringroc_auc) print(f时间序列交叉验证 AUC: {scores.mean():.4f} ± {scores.std():.4f})TimeSeriesSplit按时间顺序切分第一折用前 20% 训练、后 20% 测试第二折用前 40% 训练、后 20% 测试以此类推。scores.std()大于 0.05 就要警惕。如果标准差很大先看是不是某个时间段的正样本特别少。5.3 评分卡刻度转换与业务阈值设定逻辑回归输出的是概率业务上要转成 300 到 850 的分数。转换公式score offset factor * ln(odds)其中odds p / (1-p)。通常设定基准分 600 对应 odds 为 1:1每增加 20 分 odds 翻倍。这样算出来 factor 20 / ln(2) ≈ 28.85offset 600 - 28.85 * ln(1) 600。def prob_to_score(prob, base_score600, base_odds1, pdo20): factor pdo / np.log(2) offset base_score - factor * np.log(base_odds) odds prob / (1 - prob) return offset factor * np.log(odds) # 对测试集样本打分 scores prob_to_score(y_prob) print(f分数范围: {scores.min():.0f} - {scores.max():.0f}) print(f分数均值: {scores.mean():.0f})pdo20表示每 20 分 odds 翻倍。base_odds1表示基准分对应好坏比 1:1。实际业务里基准分和 pdo 要根据通过率来调。如果通过率要求 70%就把阈值设在分数分布的第 30 百分位。这套转换让业务方容易理解分数越高风险越低。做课程设计的时候我习惯先把四套模型的 AUC 和 PSI 都跑出来再挑一个特征做完整的评分卡转换。这样既能看到模型效果又能理解业务落地时分数是怎么用的。希望帮到你。本文还有配套的精品资源点击获取
返回列表