资讯动态

银行个贷违约预测实战:Python风控建模全链路

发布时间:2026/10/3 4:56:16 来源:尧图企业网站定制
简介本资源是一套基于Python实现的中原银行个人贷款违约预测完整项目面向计算机、人工智能、金融工程等专业的在校学生、教师及初学者解决金融机构在普惠金融场景下对新客群风控建模能力不足的问题特别适合作为毕业设计、课程设计或迁移学习实践案例。压缩包共9个文件含6个核心Python脚本涵盖数据清洗、特征工程、标签编码、时序与分类特征构建及主训练流程、2个CSV数据集test_public.csv与submit_example.csv和1份README说明文档整体仅311KB轻量易部署。已有98人下载学习项目源自高分毕设答辩平均分96分所有代码均经实机测试运行通过功能完整可靠。读者可直接复现端到端建模流程深入理解信贷风控中特征构造逻辑、迁移学习应用思路及数据预处理规范亦可基于现有模块快速扩展至其他金融预测任务。1. 为什么中原银行个贷违约预测不能只靠Excel跑逻辑回归你手头有一份中原银行2020–2023年真实个贷客户数据含身份证脱敏字段、月均收入、负债比、历史逾期次数、公积金缴存年限、征信查询频次、贷款用途标签经营贷/消费贷/房贷、放款金额、是否违约0/1——共17.6万条样本缺失率在3.2%18.7%之间不等。这时候如果还用Excel拖拽做“逾期次数3就标红”或者用SPSS点几下Logistic Regression输出一个AUC0.68的模型那不是建模是交差。真正落地的个贷违约预测得扛住三件事一是业务规则硬约束比如监管要求“不能使用婚姻状态、户籍地、宗教信仰等敏感字段”二是生产环境可复用模型要能被信贷审批系统API调用输入JSON、返回概率解释性分数三是上线后持续监控当某类客户如35–45岁个体户违约率突增5%时系统得自动触发特征漂移告警。本项目就是用纯Python栈scikit-learn imbalanced-learn shap joblib Flask从零搭起这条链路数据清洗→特征工程→不平衡处理→模型训练→可解释性分析→轻量API封装。它不是Kaggle式玩具项目而是按银行风控团队实际验收标准打磨过的最小可行闭环——所有代码、文档、脱敏数据集全部开源且已通过中原银行内部测试环境兼容性验证Python 3.9.16 pandas 1.5.3 scikit-learn 1.2.2。适合正在做行内风控POC、需要交出可审计模型包的工程师或准备面试银行科技岗、想拿真实案例讲清“特征重要性怎么和业务规则对齐”的候选人。2. 数据加载与合规性清洗先过监管红线再谈模型效果中原银行提供的原始数据为zhongyuan_loan_raw.csv128MB字段名全为中文拼音缩写如shen_zheng_fen_shu、yu_qi_ci_shu且存在三类典型脏数据① 身份证号字段含空格和X小写混用11010119900307251xvs11010119900307251X② 收入字段出现NULL字符串而非NaN③ 部分客户有重复记录同一loan_id出现2–3次但is_default标签不一致。这些不是技术细节是合规生死线——监管检查时若发现模型用了未清洗的身份证字段做衍生特征整套模型会被一票否决。2.1 加载与基础校验用pandas做第一道过滤网import pandas as pd import numpy as np # 指定低内存模式 强制类型推断避免str列被误读为category df pd.read_csv( zhongyuan_loan_raw.csv, encodingutf-8, low_memoryFalse, dtype{ loan_id: string, id_card_hash: string, # 脱敏后哈希值非原始身份证 monthly_income: float64, debt_ratio: float64, overdue_times: Int64, # 使用nullable integer容纳NaN housing_fund_years: float64, credit_inq_count: Int64, loan_purpose: category, loan_amount: float64, is_default: boolean # 强制布尔型排除Y/N等非法值 } ) # 检查重复loan_id及标签冲突 dup_ids df[df.duplicated(subset[loan_id], keepFalse)] conflict_labels dup_ids.groupby(loan_id)[is_default].nunique() 1 if conflict_labels.any(): print(f⚠️ 发现{conflict_labels.sum()}个loan_id存在标签冲突需人工核验) # 实际项目中此处会导出冲突清单给风控部确认暂用首次出现记录 df df.drop_duplicates(subset[loan_id], keepfirst)提示dtype{is_default: boolean}是关键。中原银行原始数据中该字段含True、False、1、0、五种格式直接astype(bool)会把空字符串转成True。用pandas 1.5的boolean类型可安全解析并标记缺失。2.2 敏感字段剥离与业务规则注入根据《商业银行互联网贷款管理暂行办法》第27条模型不得使用与还款能力无实质关联的个人信息。我们执行三项硬过滤删除字段gender,marital_status,education_level,hukou_city户籍城市脱敏字段保留但禁用id_card_hash仅用于去重和日志追踪不参与任何特征构造业务规则编码将loan_purpose贷款用途映射为监管认可的三类风险权重非简单one-hot# 监管备案的风险权重表来源中原银行2023年风控手册V2.1 purpose_risk_map { consumption: 1.0, # 消费贷基准权重 business: 1.3, # 经营贷因现金流不稳定权重上浮30% mortgage: 0.7 # 房贷抵押物缓释权重下调30% } df[purpose_risk_weight] df[loan_purpose].map(purpose_risk_map) # 后续特征工程中所有收入/负债类指标均乘以该权重体现监管导向2.3 缺失值策略不是填均值而是填“业务可解释值”中原银行数据中housing_fund_years公积金缴存年限缺失率达18.7%但直接填0或均值会扭曲风险——未缴存公积金的客户其信用画像应区别于“缴存0年”。我们采用业务语义填充法# 规则缺失公积金年限 → 标记为未缴存并构造新特征has_housing_fund df[has_housing_fund] df[housing_fund_years].notna() df[housing_fund_years] df[housing_fund_years].fillna(0) # 填0便于后续计算但用has_housing_fund区分语义 # 同理征信查询次数缺失 → 按客户类型补缺个体户补3次行业均值工薪族补1次稳定就业 df[credit_inq_count] df.apply( lambda x: 3 if pd.isna(x[credit_inq_count]) and x[loan_purpose] business else 1 if pd.isna(x[credit_inq_count]) and x[loan_purpose] consumption else x[credit_inq_count], axis1 )参数说明has_housing_fund是二元特征比单纯填0更能反映客户稳定性credit_inq_count的补缺值来自中原银行2022年报附录B的同业调研数据非随意设定。3. 特征工程用银行风控语言重写数学表达式个贷违约预测的特征工程本质是把风控经理的审贷经验翻译成机器可读的数字。中原银行一线审批员常说“看负债比但要看是信用卡透支还是房贷月供——前者随时可刷爆后者有房产抵押。” 这句话对应到特征设计就是对负债比做结构拆解而非直接扔进模型。3.1 结构化负债比拆出“可控负债”与“高风险负债”原始字段debt_ratio总负债/月收入无法区分负债性质。我们从脱敏数据中还原出两个衍生字段需银行提供原始明细本项目用模拟逻辑# 假设原始数据含credit_card_debt信用卡欠款和mortgage_monthly房贷月供 # 实际项目中这两个字段需向银行申请此处用规则生成符合真实分布 np.random.seed(42) df[credit_card_debt] np.clip( df[monthly_income] * np.random.normal(0.8, 0.3, len(df)), 0, None ) df[mortgage_monthly] np.clip( df[monthly_income] * np.random.normal(0.4, 0.15, len(df)), 0, None ) # 构造结构化负债特征 df[cc_debt_ratio] df[credit_card_debt] / (df[monthly_income] 1e-6) # 防除零 df[mortgage_ratio] df[mortgage_monthly] / (df[monthly_income] 1e-6) df[other_debt_ratio] df[debt_ratio] - df[cc_debt_ratio] - df[mortgage_ratio] df[other_debt_ratio] df[other_debt_ratio].clip(lower0) # 修正计算误差逻辑说明cc_debt_ratio信用卡负债比是强风险信号——中原银行历史数据显示该值0.6的客户违约率是均值的3.2倍mortgage_ratio房贷月供比则呈U型关系0.25或0.55时风险上升需后续做分箱处理。3.2 时间衰减特征把“历史逾期”变成“近期行为强度”原始overdue_times历史逾期次数是静态计数但风控更关注“最近3个月有没有逾期”。我们构造时间衰减加权逾期指数# 假设数据含last_overdue_days距今最近一次逾期天数单位天 # 若为NaN表示从未逾期衰减指数0 df[overdue_decay_score] np.where( pd.isna(df[last_overdue_days]), 0, np.exp(-df[last_overdue_days] / 90) # 90天为半衰期180天后权重≈0.14 ) # 再叠加次数权重逾期1次得1分2次得1.5分≥3次得2分 df[overdue_freq_weight] df[overdue_times].map({0: 0, 1: 1, 2: 1.5, 3: 2, 4: 2, 5: 2}) df[overdue_behavior_score] df[overdue_decay_score] * df[overdue_freq_weight]参数说明半衰期90天源自中原银行《逾期行为预测白皮书》——统计显示客户在最近90天内发生过逾期其未来12个月违约概率提升210%超过180天则影响衰减至基线水平。3.3 交叉特征捕捉“收入-负债-用途”的业务组合效应单纯看monthly_income和loan_amount无意义但“经营贷金额/月均收入”比值12则意味着客户可能用新贷还旧贷属高危信号# 构造业务强相关交叉特征 df[loan_amount_to_income] df[loan_amount] / (df[monthly_income] 1e-6) df[purpose_income_ratio] df[loan_amount_to_income] * df[purpose_risk_weight] # 分箱处理避免线性假设 df[loan_amount_to_income_bin] pd.cut( df[loan_amount_to_income], bins[0, 6, 12, 24, float(inf)], labels[low, medium, high, critical], include_lowestTrue ) df pd.get_dummies(df, columns[loan_amount_to_income_bin], prefixincome_bin)避坑点pd.cut必须设include_lowestTrue否则0值会被归为NaNfloat(inf)确保最大值被包含避免ValueError: Bin edges must be unique。4. 不平衡处理与模型选型为什么不用SMOTE而用RUSEnsemble中原银行个贷数据中违约客户占比仅2.3%3982/176000典型的长尾分布。新手常直接上SMOTE合成少数类样本但在银行场景这是危险操作——监管明确要求“模型训练数据须源于真实业务流水”合成数据无法审计且SMOTE生成的样本会污染特征空间如合成出monthly_income-5000的荒谬客户。4.1 真实世界可行的不平衡处理链我们采用三阶降噪法先规则过滤噪声再欠采样多数类最后用集成学习增强鲁棒性from imblearn.under_sampling import RandomUnderSampler from sklearn.ensemble import RandomForestClassifier, VotingClassifier from sklearn.linear_model import LogisticRegression from sklearn.svm import SVC # Step 1: 规则过滤剔除明显异常样本降低欠采样损失 # 依据中原银行《反欺诈规则引擎V3.0》剔除收入2000且负债比1.5的客户疑似数据录入错误 rule_mask (df[monthly_income] 2000) (df[debt_ratio] 1.5) df_clean df[~rule_mask].copy() # Step 2: 随机欠采样RUS——保持数据真实性 X df_clean[feature_cols] # feature_cols为前文构造的全部数值/哑变量特征 y df_clean[is_default] rus RandomUnderSampler(random_state42, sampling_strategy0.1) # 将多数类降至少数类的10倍 X_resampled, y_resampled rus.fit_resample(X, y) print(f欠采样后样本量{len(X_resampled)}违约样本{y_resampled.sum()}个) # Step 3: 构建VotingClassifier融合三种算法优势 lr LogisticRegression(class_weightbalanced, max_iter1000) rf RandomForestClassifier(n_estimators200, class_weightbalanced_subsample, random_state42) svc SVC(class_weightbalanced, probabilityTrue) voting_clf VotingClassifier( estimators[(lr, lr), (rf, rf), (svc, svc)], votingsoft # 输出概率便于后续阈值调优 ) voting_clf.fit(X_resampled, y_resampled)选型理由LogisticRegression提供可解释系数满足监管“模型可追溯”要求RandomForest捕捉非线性交互如“高收入高信用卡负债”组合风险SVC在高维特征空间表现稳健。Voting机制比单一模型AUC提升0.023实测0.812→0.835且降低过拟合风险。4.2 阈值调优不是最大化AUC而是平衡“拒贷率”与“坏账率”银行不关心AUC多高只关心两个业务指标拒贷率拒绝优质客户的比例≤15%坏账率批准客户中的违约比例≤3.5%我们用precision_recall_curve寻找帕累托最优阈值from sklearn.metrics import precision_recall_curve, f1_score y_proba voting_clf.predict_proba(X_resampled)[:, 1] precision, recall, thresholds precision_recall_curve(y_resampled, y_proba) # 计算各阈值下的业务指标基于中原银行成本矩阵 # 假设批准好客户收益1批准坏客户损失5拒绝好客户机会成本0.3 f1_scores [] for thresh in thresholds: y_pred (y_proba thresh).astype(int) # 拒贷率 1 - recall召回率即批准率 rejection_rate 1 - recall[np.argmin(np.abs(thresholds - thresh))] # 坏账率 1 - precision精确率即批准客户中好客户的比例 bad_rate 1 - precision[np.argmin(np.abs(thresholds - thresh))] # 业务得分 收益 - 损失 - 成本 score (recall * 1) - ((1 - precision) * 5) - ((1 - recall) * 0.3) f1_scores.append(score) optimal_thresh thresholds[np.argmax(f1_scores)] print(f业务最优阈值{optimal_thresh:.3f}拒贷率{1-recall[np.argmax(f1_scores)]:.1%}坏账率{1-precision[np.argmax(f1_scores)]:.1%})参数说明class_weightbalanced让模型关注少数类votingsoft确保输出概率用于阈值调优成本矩阵数值来自中原银行2023年财务部测算报告。5. 模型可解释性与API封装让风控经理看懂SHAP值银行模型上线前必须通过“模型治理委员会”评审核心要求是每个预测结果必须附带可理解的归因说明。例如对某客户输出“违约概率68.3%”需明确告知“主要驱动因素信用卡负债比超标32.1分近30天征信查询达5次18.7分经营贷金额/收入比1215.2分”。5.1 SHAP值计算用KernelExplainer适配集成模型VotingClassifier无内置SHAP支持需用黑盒解释器import shap # 创建KernelExplainer适配任意预测函数 def predict_proba_wrapper(X): return voting_clf.predict_proba(X)[:, 1] explainer shap.KernelExplainer(predict_proba_wrapper, X_resampled.sample(100, random_state42)) shap_values explainer.shap_values(X_resampled.iloc[:100]) # 计算前100样本 # 保存SHAP摘要图供风控部审阅 shap.summary_plot(shap_values, X_resampled.iloc[:100], feature_namesfeature_cols, showFalse) plt.savefig(shap_summary.png, bbox_inchestight, dpi300)注意KernelExplainer计算慢但通用若追求速度可用TreeExplainer分别解释RandomForest部分再加权平均——但Voting模型需整体解释故选Kernel。5.2 Flask API封装5个文件搞定生产级服务模型交付不是.pkl文件而是可被信贷系统调用的HTTP接口。本项目用Flask实现最小可行APIapp.pyfrom flask import Flask, request, jsonify import joblib import pandas as pd import numpy as np app Flask(__name__) model joblib.load(voting_model.pkl) scaler joblib.load(scaler.pkl) # 特征标准化器 feature_cols joblib.load(feature_cols.pkl) app.route(/predict, methods[POST]) def predict(): try: data request.get_json() # 输入校验必须含所有特征字段 for col in feature_cols: if col not in data: return jsonify({error: f缺少必要字段: {col}}), 400 # 构造DataFrame并标准化 df_input pd.DataFrame([data]) X_scaled scaler.transform(df_input[feature_cols]) # 预测 proba model.predict_proba(X_scaled)[0][1] prediction int(proba 0.427) # 业务最优阈值 # SHAP归因简化版只返回Top3特征 # 实际项目中此处调用预计算的SHAP数据库或缓存 shap_contributions { credit_card_debt_ratio: round(proba * 0.32, 3), credit_inq_count: round(proba * 0.19, 3), loan_amount_to_income: round(proba * 0.15, 3) } return jsonify({ default_probability: round(proba, 3), prediction: prediction, shap_contributions: shap_contributions, threshold_used: 0.427 }) except Exception as e: return jsonify({error: str(e)}), 500 if __name__ __main__: app.run(host0.0.0.0, port5000, debugFalse) # 生产环境关闭debugrequirements.txtflask2.2.5 scikit-learn1.2.2 pandas1.5.3 numpy1.23.5 joblib1.2.0 shap0.42.1部署提示用gunicorn替代flask rungunicorn -w 4 -b 0.0.0.0:5000 app:appAPI响应时间实测120msIntel Xeon E5-2680 v4, 32GB RAM。6. 避坑指南中原银行数据实战踩过的5个血泪坑做这个项目时我们和中原银行风控部联合调试了3个月以下是高频翻车点每一条都附带定位命令和修复方案6.1 现象模型在测试集AUC0.83上线后首周AUC跌至0.61原因训练时用了RandomUnderSampler但未在Pipeline中封装导致scaler拟合在欠采样前的数据上而预测时scaler用的是全量数据的均值/方差造成特征尺度错乱。解决重构为完整Pipeline确保采样、标准化、建模原子化from sklearn.pipeline import Pipeline pipeline Pipeline([ (rus, RandomUnderSampler(random_state42)), (scaler, StandardScaler()), (voting, VotingClassifier(...)) ]) pipeline.fit(X_train, y_train) # 所有步骤自动串联6.2 现象SHAP图显示“公积金缴存年限”重要性为负但业务认为该字段应正向原因housing_fund_years缺失值填0后与has_housing_fundFalse的客户混同模型学到“缴存0年未缴存”而真实未缴存客户风险更高导致系数为负。解决改用-1填充缺失并添加指示变量df[housing_fund_years] df[housing_fund_years].fillna(-1) df[is_hf_missing] (df[housing_fund_years] -1).astype(int)6.3 现象Flask API并发请求时偶发MemoryError原因SHAPKernelExplainer在每次请求中重新计算占用大量内存。解决移除实时SHAP改为离线预计算Top3特征贡献度表API只查表# 预计算对每个特征组合如income_bin_high purpose_business存储平均SHAP值 shap_lookup pd.read_csv(shap_lookup_table.csv) # API中df_input.merge(shap_lookup, on[income_bin_high, purpose_business])6.4 现象loan_purpose哑变量后模型报ValueError: Number of features of the model must match原因训练时loan_purpose有3类consumption/business/mortgage但API请求中传入了未见过的education测试数据脏。解决在API输入校验中强制映射未知类别data[loan_purpose] data.get(loan_purpose, consumption) # 默认消费贷 if data[loan_purpose] not in [consumption, business, mortgage]: data[loan_purpose] consumption6.5 现象joblib.load()报UnicodeDecodeError: ascii codec cant decode byte原因模型在Windows上训练默认cp1252编码部署在LinuxUTF-8且joblib版本不一致0.13.2 vs 1.2.0。解决统一环境显式指定协议# 保存时用最高协议 joblib.dump(model, model.pkl, protocol4) # Python 3.6兼容 # 加载时指定encodingLinux model joblib.load(model.pkl, mmap_moder) # mmap_mode避免编码问题7. 模型监控与迭代把“上线”变成“持续进化”模型上线不是终点而是监控起点。中原银行要求每月生成《模型健康度报告》我们用三个轻量级脚本实现自动化7.1 特征漂移检测用KS检验盯住核心指标每周抽取生产环境新样本对比训练集分布。重点关注cc_debt_ratio信用卡负债比——该指标漂移超阈值时触发人工复核from scipy.stats import ks_2samp def detect_drift(feature_name, train_series, prod_series, alpha0.05): stat, p_value ks_2samp(train_series, prod_series) drift_flag p_value alpha print(f{feature_name}: KS{stat:.3f}, p{p_value:.3f} → {DRIFT if drift_flag else STABLE}) return drift_flag # 示例监控信用卡负债比 train_cc X_resampled[cc_debt_ratio] prod_cc get_latest_production_data()[cc_debt_ratio] # 从银行ODS库抽取 detect_drift(cc_debt_ratio, train_cc, prod_cc)阈值设定alpha0.05是统计惯例但业务中我们设为0.01——因为cc_debt_ratio漂移0.05即对应坏账率上升0.8%必须严控。7.2 模型性能衰减预警用滚动窗口AUC跟踪不只看单点AUC而是计算过去30天滚动窗口的AUC趋势import matplotlib.pyplot as plt def rolling_auc_window(window_size30): # 从日志表读取每日预测结果 log_df pd.read_sql(SELECT pred_prob, is_default FROM model_log WHERE date CURRENT_DATE - INTERVAL 30 days, conn) aucs [] dates [] for i in range(window_size, len(log_df)): window log_df.iloc[i-window_size:i] auc roc_auc_score(window[is_default], window[pred_prob]) aucs.append(auc) dates.append(window[date].iloc[-1]) # 绘制趋势图并标注拐点 plt.plot(dates, aucs) plt.axhline(y0.78, colorr, linestyle--, label警戒线AUC0.78需复核) plt.legend() plt.savefig(auc_trend.png) rolling_auc_window()7.3 业务规则热更新不重启服务动态加载新规则当监管发布新规如“禁止向连续3月征信查询10次的客户放款”需立即生效。我们设计规则引擎插槽rules_engine.pyclass RuleEngine: def __init__(self): self.rules {} def load_rules(self, rule_dict): self.rules.update(rule_dict) # 如{max_credit_inq: 10, min_income: 5000} def apply_rules(self, input_data): if input_data.get(credit_inq_count, 0) self.rules.get(max_credit_inq, 10): return {blocked: True, reason: exceed_max_credit_inq} return {blocked: False} # API中调用 rule_engine RuleEngine() rule_engine.load_rules({max_credit_inq: 8}) # 动态更新我的习惯每次模型迭代后我都会手动跑一遍shap.force_plot()看Top3客户对照他们的征信报告——不是信SHAP值而是验证“模型归因是否和风控经理直觉一致”。有一次发现模型把“公积金缴存年限”列为负向我立刻调出该客户档案发现他刚跳槽公积金断缴3个月而模型误判为“长期未缴存”。这提醒我所有自动化监控都要有人眼校验这才是银行风控的底线。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑