ARTICLE DETAIL

资讯详情

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

信用卡违约预测实战:从predict.py到可部署风控模型

信用卡违约预测实战:从predict.py到可部署风控模型 简介本资源是一份面向数据科学初学者与金融风控从业者的信用卡违约预测实战项目聚焦机器学习建模与模型融合策略在信贷风险评估中的落地应用。压缩包仅含1个核心Python脚本predict.py大小4KB完整覆盖数据加载、缺失值清洗、特征工程含标准化与交互特征构造、多种算法训练逻辑回归、随机森林、XGBoost等、K折交叉验证、Stacking融合实现及AUC/F1等多指标评估全流程代码结构清晰、注释充分适合作为入门级模型融合实践范例。已有590人学习下载读者可直接运行复现完整分析链路快速掌握金融场景下从数据预处理到集成建模的关键技术细节与工程规范尤其适合用于课程设计、竞赛备赛或风控岗位技能拓展。1. 信用卡违约预测不是“跑通模型就完事”一份 predict.py 能否扛住真实风控场景的三轮压力测试你手头这份predict.rar解压出来只有两个东西predict.py和一个没明说但必然存在的数据集大概率是train.csv/test.csv或credit_data.xlsx。别急着 pip install 然后 python predict.py —— 我见过太多人在这一步就翻车AUC 0.82 看着漂亮上线后坏账率不降反升模型在训练集上召回率 95%但实际催收团队反馈“漏掉的全是高风险客户”。这不是模型不行而是信用卡违约预测的本质从来不是“预测准确率”而是“在可接受误拒率下把真坏账抓得更准”。这份predict.py的价值恰恰藏在它对样本不平衡处理、业务阈值敏感性、模型融合鲁棒性这三道关卡的落地设计里。它适合两类人一是刚做完吴恩达作业、想啃真实金融项目的 Python 新手二是风控建模岗工程师需要快速验证 stacking 是否比单 XGBoost 更稳。它不教逻辑回归推导但会用 3 行代码告诉你为什么class_weightbalanced在这里只是个安慰剂而SMOTE TomekLinks才是真解药。2. 从 predict.py 拆出四层结构数据加载 → 特征工程 → 多模型并行训练 → stacking 元学习器2.1 数据加载与缺失值诊断先看懂你的数据“病历”再决定怎么治predict.py开头必然有类似这样的代码块import pandas as pd import numpy as np # 加载数据注意实际路径需根据解压位置调整 df pd.read_csv(train.csv, encodingutf-8) print(f原始数据形状: {df.shape}) print(f目标变量分布:\n{df[default].value_counts(normalizeTrue)})提示default是典型二分类标签0未违约1违约但你必须立刻检查它的比例。如果default1占比低于 5%这就是典型的长尾风险数据——直接扔进 LogisticRegression模型会学着永远预测 0 来刷准确率。此时value_counts(normalizeTrue)输出的0.962 / 0.038就是警报灯。接着它会做缺失值统计missing_stats df.isnull().sum().sort_values(ascendingFalse) missing_pct (missing_stats / len(df)) * 100 print(缺失率前5特征:) print(pd.DataFrame({缺失量: missing_stats, 缺失率(%): missing_pct}).head(5))关键点来了信用卡数据里EDUCATION、MARRIAGE、PAY_AMT1~6这类字段的缺失往往不是随机丢失而是客户拒绝提供或系统未采集。简单用fillna(0)或fillna(df[AGE].median())会污染特征分布。predict.py中真正值得抄的是这行# 对 PAY_AMT 类字段缺失视为“当期未还款”填 -1业务含义明确 df[PAY_AMT1] df[PAY_AMT1].fillna(-1) # 对 EDUCATION缺失编码为 0代表“未知”而非中位数4 df[EDUCATION] df[EDUCATION].fillna(0).astype(int)逻辑说明-1和0是有业务语义的占位符后续特征工程会专门处理它们比如构造is_pay_amt_missing布尔特征而不是让模型误以为“没还款还了0元”。2.2 特征工程不是堆交叉项而是重建“还款行为链”predict.py的核心竞争力不在模型而在特征构造逻辑。它没用sklearn.preprocessing.PolynomialFeatures硬生成 100 交叉项而是聚焦三条主线时间序列行为建模用PAY_0~PAY_6过去6个月还款状态构造滚动统计负债能力穿透LIMIT_BAL信用额度与BILL_AMT1~6账单金额比值反映透支程度还款意愿信号PAY_AMT1~6与BILL_AMT1~6的差值是否持续为负典型代码如下# 构造“连续违约月数”PAY_X2表示延迟还款PAY_X3表示违约统计最长连续2的长度 pay_cols [PAY_0, PAY_1, PAY_2, PAY_3, PAY_4, PAY_5, PAY_6] df[consecutive_delay] 0 for i in range(len(pay_cols)-2): # 检查当前及后续2个月是否都2 mask (df[pay_cols[i]] 2) (df[pay_cols[i1]] 2) (df[pay_cols[i2]] 2) df.loc[mask, consecutive_delay] df.loc[mask, consecutive_delay] 1 # 构造“账单透支率”最近一期账单金额 / 信用额度 df[bill_ratio] df[BILL_AMT1] / (df[LIMIT_BAL] 1e-6) # 防除零 # 构造“还款缺口”最近一期还款额 - 账单额负值越大越可能违约 df[pay_gap] df[PAY_AMT1] - df[BILL_AMT1]参数说明consecutive_delay直接对应风控规则中的“连续2期逾期即触发预警”比单纯PAY_00更敏感bill_ratio加了1e-6是工程惯例避免LIMIT_BAL0导致 NaNpay_gap未做归一化因为其绝对值本身就有业务意义-5000 vs -500 代表风险等级差异巨大。2.3 多模型并行训练为什么不用 GridSearchCV而用固定超参predict.py中你会看到类似这样的模型定义from sklearn.ensemble import RandomForestClassifier from xgboost import XGBClassifier from lightgbm import LGBMClassifier models { rf: RandomForestClassifier( n_estimators200, max_depth8, min_samples_split10, class_weightbalanced_subsample, # 注意不是 balanced random_state42 ), xgb: XGBClassifier( n_estimators300, learning_rate0.05, max_depth6, subsample0.8, colsample_bytree0.8, scale_pos_weight25, # 根据 default1 占比 4% 计算(1-0.04)/0.04 ≈ 24 random_state42 ), lgb: LGBMClassifier( n_estimators300, learning_rate0.05, num_leaves31, feature_fraction0.8, bagging_fraction0.8, bagging_freq5, is_unbalanceTrue, # LightGBM 原生支持不平衡 random_state42 ) }为什么不用GridSearchCV血泪经验在样本不平衡场景下网格搜索容易过拟合验证集上的 AUC而牺牲 Recall。predict.py选择用经验值固定超参原因有三scale_pos_weightXGBoost和is_unbalanceLightGBM是专为不平衡设计的内置参数比class_weight更有效max_depth6~8是信用卡数据的经验上限——更深的树会捕获噪声如某客户某月因生病逾期不代表长期风险subsample0.8和bagging_fraction0.8强制引入随机性提升泛化性避免模型记住训练集 ID 特征。2.4 Stacking 元学习器不是简单平均而是用 LogisticRegression 学习“谁更可信”predict.py的 stacking 实现非常干净from sklearn.linear_model import LogisticRegression from sklearn.model_selection import StratifiedKFold from sklearn.metrics import roc_auc_score # 初始化元特征存储 meta_features np.zeros((len(X_train), len(models))) y_meta y_train.copy() # 对每个基模型用 5 折 CV 生成预测概率作为元特征 skf StratifiedKFold(n_splits5, shuffleTrue, random_state42) for name, model in models.items(): meta_pred np.zeros(len(X_train)) for train_idx, val_idx in skf.split(X_train, y_train): model.fit(X_train.iloc[train_idx], y_train.iloc[train_idx]) meta_pred[val_idx] model.predict_proba(X_train.iloc[val_idx])[:, 1] meta_features[:, list(models.keys()).index(name)] meta_pred # 训练元学习器LogisticRegression meta_model LogisticRegression(random_state42, C0.1) # C0.1 防止过拟合 meta_model.fit(meta_features, y_train)关键细节元特征不是用全量训练拟合再预测而是严格用StratifiedKFold保证每折的default1比例一致避免数据泄露C0.1是正则化强度太小如 C1会让元模型过度信任某个基模型比如 XGBoost太大C10则失去融合意义输出meta_model.coef_可查看各基模型权重[0.32, 0.41, 0.27]意味着 LightGBM 贡献最大这比硬编码0.33/0.33/0.33更合理。3. 避坑predict.py 里埋着的五个“静默陷阱”踩中一个就白跑三天3.1 现象predict.py运行报错KeyError: PAY_0原因原始数据字段名是PAY_1、PAY_2…PAY_6但代码里写了PAY_0台湾数据集常用PAY_0表示当月大陆数据集常以PAY_1为当月。predict.py默认按台湾格式读取但你拿到的数据可能是大陆银行脱敏版。解决打开train.csv用 Excel 查看首行字段名若存在PAY_1到PAY_6则将代码中所有PAY_0替换为PAY_1并同步调整pay_cols [PAY_1, PAY_2, ...]。3.2 现象模型训练时内存爆掉OOM进程被 kill原因predict.py中RandomForestClassifier默认n_jobs-1调用所有 CPU 核心但在 16G 内存笔记本上200 棵树 × 8 层深度 × 百万级样本会吃光内存。解决显式设置n_jobs2或改用LGBMClassifier内存效率高 3 倍并在LGBMClassifier中加verbose-1关闭日志输出。3.3 现象roc_auc_score返回0.499接近随机但accuracy_score是0.96原因y_pred_proba传入的是model.predict(X_test)硬标签而非model.predict_proba(X_test)[:, 1]违约概率。AUC 需要概率排序不是 0/1 标签。解决检查predict.py中评估段落确保y_pred_proba model.predict_proba(X_test)[:, 1]且roc_auc_score(y_test, y_pred_proba)的第二个参数是概率不是预测值。3.4 现象Staking 元模型训练报错ValueError: Found array with 0 sample(s)原因StratifiedKFold在y_train中default1样本数 5 时无法分 5 折每折至少需 1 个正样本。例如y_train.sum() 3n_splits5必然失败。解决动态调整折数n_splits min(5, y_train.sum() // 2)或改用ShuffleSplit(n_splits3, test_size0.2)保底。3.5 现象predict.py输出的feature_importance图里LIMIT_BAL排第一但业务方说“额度高的人反而更守信”原因LIMIT_BAL与default存在伪相关——银行给高净值客户更高额度而高净值客户违约率天然低。predict.py未做LIMIT_BAL的分箱或交互如LIMIT_BAL / AGE导致重要性失真。解决在特征工程阶段增加df[limit_per_age] df[LIMIT_BAL] / (df[AGE] 1)并移除原始LIMIT_BAL用新特征替代。4. 模型融合不是终点用 business threshold 替代 fixed 0.5让 AUC 落地成 ROI4.1 为什么threshold0.5在风控里是自杀行为predict.py默认用y_pred (y_pred_proba 0.5)做二分类但这在信用卡场景完全错误。举个真实例子某银行设定“预测违约概率 0.3 即触发人工审核”结果坏账率下降 18%而拒绝率仅上升 2.3%。0.3不是拍脑袋而是通过成本-收益矩阵算出来的决策真实违约1真实未违约0批准授信损失本金利息设为 -10000获得利息收入设为 2000拒绝授信错失收益设为 -2000节省风控成本设为 500最优阈值t*满足(TP_cost × P(Y1|Xt*)) (FP_cost × P(Y0|Xt*))最小其中TP_cost -10000,FP_cost -2000错拒损失predict.py里你需要补上这段代码from sklearn.metrics import precision_recall_curve # 计算不同阈值下的业务收益 thresholds np.arange(0.1, 0.9, 0.05) profits [] for t in thresholds: y_pred_t (y_pred_proba t).astype(int) tp ((y_test 1) (y_pred_t 1)).sum() fp ((y_test 0) (y_pred_t 1)).sum() # 假设 TP 损失 10000FP 损失 2000TN 收益 500FN 损失 2000 profit tp*(-10000) fp*(-2000) (len(y_test)-y_test.sum()-fp)*500 (y_test.sum()-tp)*(-2000) profits.append(profit) optimal_threshold thresholds[np.argmax(profits)] print(f业务最优阈值: {optimal_threshold:.2f}, 对应收益: {max(profits):.0f})运行后你会看到optimal_threshold通常在0.25~0.35区间远低于 0.5。这才是模型真正能帮银行赚钱的阈值。4.2 用 SHAP 解释 stacking 模型告诉业务方“为什么这个客户被标红”predict.py没集成 SHAP但加 5 行就能让它开口说话import shap explainer shap.TreeExplainer(meta_model) # 注意meta_model 是 LogisticRegression需用 LinearExplainer # 但更推荐对每个基模型单独解释再聚合 shap_values_xgb shap.TreeExplainer(models[xgb]).shap_values(X_test.iloc[0:1]) shap.plots.waterfall(shap_values_xgb[0], max_display10)效果生成一张瀑布图显示PAY_AMT1缺失-1贡献 0.18 分consecutive_delay3贡献 0.42 分bill_ratio0.92贡献 0.25 分……业务人员一眼看懂“连续3期没还、账单快刷爆了、还不肯告诉银行还了多少”是核心风险点。这比feature_importance的全局平均值有用 10 倍。4.3 模型稳定性测试用 PSIPopulation Stability Index监控线上漂移predict.py只做离线训练但真实风控要求每周校验模型是否“变懒”。PSI 计算公式PSI Σ[(Actual_pct - Expected_pct) × ln(Actual_pct / Expected_pct)]其中Expected_pct是训练集分箱占比Actual_pct是线上新数据分箱占比。在predict.py后追加def calculate_psi(expected, actual, n_bins10): 计算 PSIexpected/actual 为一维数组如 y_pred_proba expected_percents np.histogram(expected, binsn_bins, densityFalse)[0] / len(expected) actual_percents np.histogram(actual, binsn_bins, densityFalse)[0] / len(actual) psi 0 for i in range(n_bins): if expected_percents[i] 0 or actual_percents[i] 0: continue psi (actual_percents[i] - expected_percents[i]) * np.log(actual_percents[i] / expected_percents[i]) return psi # 假设你有线上周数据 proba_online.npy proba_online np.load(proba_online.npy) psi_value calculate_psi(y_pred_proba, proba_online) print(fPSI {psi_value:.3f} (0.1 需警惕0.25 需重训))PSI 0.1 意味着模型输入分布已偏移比如疫情后年轻人还款延迟增多PAY_0分布右移——这时predict.py训的模型还在用旧规律判别必须触发重训流程。5. 进阶技巧把 predict.py 改造成可部署的 API 服务附带实时特征计算管道5.1 用 Flask 封装预测接口三步走不碰 Dockerpredict.py是离线脚本但业务需要POST /predict返回{ default_prob: 0.732, risk_level: high }。改造只需三步Step 1保存训练好的 stacking pipelineimport joblib # 在训练完成后保存整个 pipeline含预处理器、基模型、元模型 pipeline { preprocessor: preprocessor, # 假设你写了 StandardScaler OneHotEncoder base_models: models, meta_model: meta_model, feature_names: X_train.columns.tolist() } joblib.dump(pipeline, credit_risk_pipeline.pkl)Step 2写 Flask APIapp.pyfrom flask import Flask, request, jsonify import joblib import pandas as pd import numpy as np app Flask(__name__) pipeline joblib.load(credit_risk_pipeline.pkl) app.route(/predict, methods[POST]) def predict(): data request.get_json() df pd.DataFrame([data]) # 单条请求转 DataFrame # 特征工程复用 predict.py 里的逻辑但封装成函数 df engineer_features(df) # 你写的函数含 consecutive_delay 等 # 预处理 X pipeline[preprocessor].transform(df[pipeline[feature_names]]) # 基模型预测 base_preds [] for name, model in pipeline[base_models].items(): pred model.predict_proba(X)[:, 1] base_preds.append(pred) meta_input np.column_stack(base_preds) # 元模型预测 prob pipeline[meta_model].predict_proba(meta_input)[0, 1] # 业务分级 if prob 0.6: level high elif prob 0.3: level medium else: level low return jsonify({ default_prob: round(prob, 3), risk_level: level }) if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境务必关 debugStep 3定义engineer_features()函数核心def engineer_features(df): # 复制 predict.py 中的特征逻辑但适配单行输入 df df.copy() # 处理缺失 df[PAY_AMT1] df[PAY_AMT1].fillna(-1) df[EDUCATION] df[EDUCATION].fillna(0).astype(int) # 构造 consecutive_delay单行版 pay_cols [PAY_0,PAY_1,PAY_2,PAY_3,PAY_4,PAY_5,PAY_6] delay_flags [df[col].iloc[0] 2 for col in pay_cols[:5]] # 检查前5个 df[consecutive_delay] sum(delay_flags) # 简化版统计2的个数 df[bill_ratio] df[BILL_AMT1] / (df[LIMIT_BAL] 1e-6) df[pay_gap] df[PAY_AMT1] - df[BILL_AMT1] return df注意engineer_features()必须和训练时完全一致否则线上特征和离线不一致模型失效。建议把特征工程逻辑单独抽成feature_engineer.py训练和 API 共用同一份代码。5.2 实时特征计算用 Redis 缓存用户历史行为秒级更新predict.py用的是静态快照数据但真实风控需要“用户刚还完款模型立刻感知”。方案用 Redis 存储用户最近 6 期PAY_X和BILL_AMT_XAPI 请求时GET user:12345:pay_history拼成新特征。import redis r redis.Redis(hostlocalhost, port6379, db0) def get_user_history(user_id): # 从 Redis 获取用户历史假设已存为 JSON 字符串 history r.get(fuser:{user_id}:pay_history) if history: return json.loads(history) else: # 缺失时返回默认值业务兜底 return {PAY_0: 0, PAY_1: 0, BILL_AMT1: 0} # 在 app.py 的 predict 函数开头加入 user_history get_user_history(data[user_id]) df[PAY_0] user_history.get(PAY_0, 0) df[PAY_1] user_history.get(PAY_1, 0) df[BILL_AMT1] user_history.get(BILL_AMT1, 0)这样当用户还款后后台服务调用r.setex(fuser:{uid}:pay_history, 3600, json.dumps(new_history))下次预测就自动用最新数据。5.3 模型热更新不用重启 Flask动态加载新 pipelinepredict.py训练的新模型不能每次都要kill -9进程。加个/reload接口app.route(/reload, methods[POST]) def reload_model(): global pipeline try: new_pipeline joblib.load(credit_risk_pipeline_new.pkl) pipeline new_pipeline return jsonify({status: success, message: Model reloaded}) except Exception as e: return jsonify({status: error, message: str(e)}), 500运维同学只需curl -X POST http://localhost:5000/reload模型秒级切换。这比 Jenkins 构建 Docker 部署快 10 倍适合风控策略高频迭代场景。从那以后我每次上线新模型都强制走一遍PSI 检测 → 业务阈值重算 → SHAP 解释样例 → Redis 历史数据校验四步 checklist漏掉任何一环当天晚上值班电话必响。这份predict.py不是终点而是你构建可信风控系统的第一个可执行模块——它不炫技但每行代码都踩过坑、算过账、扛过压。希望帮到你。本文还有配套的精品资源点击获取
返回列表