☰
真实信贷违约预测全流程:从缺失值处理到利率敏感性验证
2026/10/14 15:42:13 网站建设 项目流程

简介:本资源是一份面向Python初学者与机器学习入门者的个人信用评估课程设计实践项目,聚焦贷款违约预测这一典型风控场景,帮助学习者掌握从数据清洗、特征工程到模型训练与可视化的完整建模流程。压缩包共82个文件,含13个核心Python脚本(涵盖数据处理、PCA降维、网格搜索调参、多模型训练与可视化)、62张分析图表(如相关性热力图、变量分布图、特征组合散点图等)、2个关键数据集CSV文件、1份详尽的Word设计报告及README说明文档,整体仅5.16MB,轻量易解压。已有743人学习下载,内容基于阿里天池真实信贷数据(脱敏后80万训练样本),覆盖employmentTitle、grade、dti、ficoRangeLow等47维变量,预置了null值统计、高相关特征筛选、多维度分布可视化等实用模块,目录结构分层清晰(scripts/src/data/visualize等),便于按步骤复现与拓展。

1. 这不是“打分模型”,而是用真实信贷数据跑通违约预测全流程:从缺失值血泪填坑到子Grade级利率敏感性验证

你手头那份「个人信用评估」课程设计,大概率还在用UCI的German Credit数据集——30年老数据、1000条样本、7个特征,调个LogisticRegression就acc=0.75,答辩PPT上写着“准确率高、泛化性强”。但现实里,某信贷平台每天新增2万笔申请,逾期率浮动在8.3%~12.7%,风控模型差0.5个百分点,就是季度多坏账3700万。这份Python机器学习资源包,直接切进阿里天池「贷款违约预测」赛题实战内核:80万训练样本、47维原始字段(含15个匿名变量n1-n15)、脱敏但未失真的employmentTitle/purpose/postCode,连revolUtil(循环信用利用率)和dti(债务收入比)这种强业务信号都原样保留。它不教你画ROC曲线,而是让你亲手把n7这个匿名变量和openAcc(未结信用账户数)做交叉分布图,发现当n7>0.85且openAcc>=5时,违约率陡升至31.2%——这才是风控同学每天盯的“红点区域”。适合两类人:课程设计卡在特征工程不会下手的本科生,以及想用真实数据复现贷前模型但被缺失值和高基数分类变量劝退的转行者。


2. 数据加载与探查:为什么nullInfo.png比README.md更值得先打开

2.1 从dataCheck.py切入:三行代码定位致命空值模式

项目中src/dataCheck.py是整个流程的起点,它不只统计缺失率,而是按业务逻辑分层诊断。关键代码如下:

# src/dataCheck.py 第12-18行 def check_null_pattern(df): # 按缺失率分组:完全缺失/部分缺失/零缺失 null_stats = df.isnull().sum().sort_values(ascending=False) high_null_cols = null_stats[null_stats > 0.3 * len(df)].index.tolist() mid_null_cols = null_stats[(null_stats > 0.05 * len(df)) & (null_stats <= 0.3 * len(df))].index.tolist() # 特别检查:是否整列为空(如某些脱敏字段全为NaN) empty_cols = [col for col in df.columns if df[col].isnull().all()] return {"high": high_null_cols, "mid": mid_null_cols, "empty": empty_cols}

参数说明:0.3和0.05不是拍脑袋定的阈值。0.3对应30%缺失率——超过此线,用均值/众数填充会严重扭曲分布;0.05是5%临界点,这类字段可用随机森林等算法反推,但需警惕过拟合。empty_cols检测的是彻底失效字段(如脱敏后全为NaN的postCode),这类字段必须从特征集剔除,否则后续PCA会报错。

执行后输出nullInfo.png,图中横轴是字段名,纵轴是缺失率,你会立刻看到emp_title(脱敏后的employmentTitle)缺失率28.7%,title缺失率41.2%,而n12(某个匿名变量)缺失率0%——这说明n12是稳定信号源,应优先纳入建模。很多新手直接删掉所有高缺失字段,结果把revolUtil(循环信用利用率)这种核心指标也干掉了,实际它缺失率仅1.3%,用fillna(method='ffill')就能补全。

2.2plotDistribution.py:用直方图戳破“正态分布”幻觉

src/visualize/plotDistribution.py专治“以为数据都该标准化”的玄学。它对数值型字段生成双Y轴图:左轴密度曲线,右轴累计分布(CDF)。重点看loan_amnt(贷款金额)和annual_inc(年收入):

# src/visualize/plotDistribution.py 第33行起 def plot_numeric_dist(df, col, bins=50): fig, ax1 = plt.subplots(figsize=(10, 6)) # 密度曲线(左Y轴) sns.histplot(df[col].dropna(), kde=True, ax=ax1, bins=bins, color='steelblue', alpha=0.7) ax1.set_ylabel('Density', fontsize=12) # CDF曲线(右Y轴) ax2 = ax1.twinx() sorted_vals = np.sort(df[col].dropna()) cdf = np.arange(1, len(sorted_vals)+1) / len(sorted_vals) ax2.plot(sorted_vals, cdf, color='darkred', lw=2, label='CDF') ax2.set_ylabel('Cumulative Probability', fontsize=12) ax2.grid(False) plt.title(f'Distribution of {col}', fontsize=14) plt.show()

运行plotDistribution.py --col loan_amnt,你会看到:loan_amnt在$5000、$10000、$15000处有尖峰(平台放款额度档位),右尾极长(最高达$35000),根本不是正态分布。此时若强行StandardScaler,小额度样本会被压缩到-2σ以下,大额样本挤在+3σ附近——模型根本学不到额度档位的业务含义。正确做法是:对loan_amnt做分箱(pd.cut切5档),再用OneHotEncoder编码,让模型明确感知“5k以下”“1w-1.5w”这些业务区间。

2.3plotCorr.py:相关系数矩阵里的“假朋友”与“真敌人”

src/visualize/plotCorr.py生成的corr.png热力图,表面看interest_rate(年化利率)和grade(信用等级)相关性高达0.89,但interest_rate和sub_grade(子等级)相关性只有0.63。这说明什么?grade是粗粒度标签(A/B/C/D/E/F/G),而sub_grade(A1/A2...G5)才是精细定价依据。如果只用grade建模,相当于把A1客户(利率5.2%)和A5客户(利率7.8%)混为一谈,模型必然在A级内部失效。项目中grade-subGrade.png散点图直接暴露这点:A级内sub_grade每降1档,利率平均升0.85%。所以特征工程必须拆解grade,用sub_grade替代,或构造grade + sub_grade的组合特征。


3. 特征工程实战:15个匿名变量n1-n15怎么用?别猜,用pca_dimensionReduce.py验

3.1 匿名变量不是“黑匣子”,是降维前的原始信号池

n1-n15是赛题脱敏后的15个连续型变量,原始含义不可知,但项目用pca_dimensionReduce.py证明它们携带强信息。关键步骤在PCA前的数据预处理:

# src/pca_dimensionReduce.py 第22行 # 对n1-n15单独标准化(避免被loan_amnt等大数值字段主导) n_cols = [f'n{i}' for i in range(1, 16)] scaler_n = StandardScaler() df_scaled_n = pd.DataFrame( scaler_n.fit_transform(df[n_cols].fillna(df[n_cols].median())), columns=n_cols, index=df.index ) # PCA降维:保留95%方差所需的主成分数量 pca = PCA(n_components=0.95) pca_result = pca.fit_transform(df_scaled_n) print(f"PCA保留95%方差需{pca.n_components_}个主成分") # 实际输出:11

参数说明:n_components=0.95表示保留95%累计方差,而非固定主成分数。实测n1-n15经PCA后只需11个主成分,说明其中4个变量冗余度高(可能互为线性组合)。fillna(df[n_cols].median())用中位数而非均值,因匿名变量可能存在长尾异常值,中位数更鲁棒。

执行后生成pca.png,图中显示前3个主成分贡献率分别为38.2%、22.1%、15.7%,合计76%——这意味着仅用3个新特征,就能表达原始15维76%的信息量。项目中train.py直接将这11个PCA特征与业务字段拼接,使总特征数从47降至约40(剔除空值字段+PCA压缩),既降维又保信息。

3.2 分类变量编码:employmentLength的“< 1 year”不能当字符串填

employmentLength字段值为< 1 year,1 year,2 years, ...,10+ years,看似有序,但直接LabelEncoder会错误赋予< 1 year=0、10+ years=10的数值关系。项目用dataEncode.py中的encode_emp_length函数处理:

# src/dataEncode.py 第45行 def encode_emp_length(x): if pd.isna(x): return -1 elif x == '< 1 year': return 0.5 # 折中取0.5年 elif x == '10+ years': return 10.5 # 折中取10.5年 else: # 提取数字,如'2 years'→2.0 return float(re.search(r'(\d+)', str(x)).group(1)) # 应用编码 df['emp_length_encoded'] = df['employment_length'].apply(encode_emp_length)

逻辑说明:< 1 year不等于0年(刚入职),也不等于1年(已满),取0.5年更符合人力逻辑;10+ years同理取10.5年。这样编码后,emp_length_encoded可直接参与LinearRegression等需要数值连续性的模型,避免OneHotEncoder产生11个稀疏维度。

3.3 时间特征挖掘:issue_d(放款日期)藏着季度性违约规律

原始数据含issue_d字段(格式Jun-2015),项目在dataHandler.py中提取三个时间维度:

# src/utils/dataHandler.py 第78行 def extract_time_features(df): # 转datetime并提取 df['issue_date'] = pd.to_datetime(df['issue_d'], format='%b-%Y') df['issue_month'] = df['issue_date'].dt.month df['issue_quarter'] = df['issue_date'].dt.quarter df['issue_year'] = df['issue_date'].dt.year # 构造“是否年末”布尔特征(12月放款违约率高12.3%) df['is_december'] = (df['issue_month'] == 12).astype(int) # 构造“距首期放款月数”(反映平台发展阶段) first_issue = df['issue_date'].min() df['months_since_first'] = ((df['issue_date'] - first_issue) / np.timedelta64(1, 'M')).astype(int) return df

实测is_december特征在XGBoost中重要性排第7,months_since_first排第12——说明模型确实学到了“年末资金紧张导致还款意愿下降”和“平台早期风控规则不完善”这两条业务规律。


4. 模型训练与调参:GridSearchCV不是万能钥匙,BAG策略才是关键

4.1GridSearchCV.py:为什么只搜max_depth和learning_rate?

项目中src/GridSearchCV.py对XGBoost只网格搜索两个参数:

# src/GridSearchCV.py 第15行 param_grid = { 'max_depth': [3, 4, 5, 6], 'learning_rate': [0.01, 0.05, 0.1, 0.2], # 故意不搜n_estimators(树的数量),因早停机制更优 }

选型理由:n_estimators(树的数量)若参与网格搜索,会导致计算量爆炸(4×4×100=1600次训练)。项目改用xgb.XGBClassifier内置的early_stopping_rounds=50,在验证集上监控logloss,连续50轮不下降即停止。这样既防过拟合,又省算力。max_depth控制单棵树复杂度,learning_rate控制每棵树贡献度,二者协同决定模型容量,是XGBoost最敏感的两个超参。

4.2BAK目录里的train.py:Bagging集成的真实写法

src/BAK/train.py是项目核心,它不只训练单模型,而是实现三层Bagging:

# src/BAK/train.py 第89行 def bagging_train(X_train, y_train, X_val, y_val): models = [] val_preds = np.zeros(len(y_val)) for i in range(5): # 5折Bagging # 随机采样训练集(有放回) idx = np.random.choice(len(X_train), size=len(X_train), replace=True) X_boot = X_train.iloc[idx] y_boot = y_train.iloc[idx] # 训练XGBoost model = xgb.XGBClassifier( max_depth=4, learning_rate=0.1, subsample=0.8, colsample_bytree=0.8, random_state=i ) model.fit(X_boot, y_boot, eval_set=[(X_val, y_val)], early_stopping_rounds=50, verbose=False) models.append(model) val_preds += model.predict_proba(X_val)[:, 1] / 5 return models, val_preds

参数说明:subsample=0.8(每棵树用80%样本)、colsample_bytree=0.8(每棵树用80%特征)是Bagging的关键扰动项,确保各模型差异性。最终预测是5个模型概率均值,比单模型AUC高0.023(实测0.742→0.765)。

4.3judge.py:用f1_score而非accuracy评估,因为违约样本只占8.7%

src/judge.py中评估函数强制使用F1-score:

# src/judge.py 第22行 def evaluate_model(y_true, y_pred_proba, threshold=0.5): y_pred = (y_pred_proba >= threshold).astype(int) f1 = f1_score(y_true, y_pred) precision = precision_score(y_true, y_pred) recall = recall_score(y_true, y_pred) # 关键:计算KS值(风控核心指标) ks = ks_statistic(y_true, y_pred_proba) return { 'f1': f1, 'precision': precision, 'recall': recall, 'ks': ks, 'threshold': threshold } # KS统计量计算(区分好坏客户能力) def ks_statistic(y_true, y_proba): from sklearn.metrics import roc_curve fpr, tpr, _ = roc_curve(y_true, y_proba) return max(tpr - fpr)

逻辑说明:accuracy在不平衡数据中失效(全预测“不违约”就有91.3%准确率)。f1_score平衡精确率和召回率,KS值(最大区分度)直接反映模型对高风险客户的识别能力。项目中KS值达0.42,意味着模型能将42%的违约客户从全体中分离出来——这是风控上线硬指标。


5. 避坑指南:那些让模型AUC从0.75暴跌到0.61的5个致命细节

5.1 现象:pca_dimensionReduce.py报ValueError: Input contains NaN

原因:PCA前未处理n1-n15中的缺失值,StandardScaler无法处理NaN。
解决:在pca_dimensionReduce.py第22行前插入df[n_cols] = df[n_cols].fillna(df[n_cols].median()),必须用中位数(非均值),因匿名变量分布偏斜,均值易被异常值拉偏。

5.2 现象:train.py训练时内存溢出(OOM)

原因:employmentTitle(脱敏后仍有12万+唯一值)被OneHotEncoder全量编码,生成12万+稀疏列。
解决:改用TargetEncoder(在dataEncode.py中已实现),用employmentTitle分组的违约率替代原值,将12万维压缩为1维。代码见dataEncode.py第112行target_encode()函数。

5.3 现象:plotCorrPair.py生成的n1-n2.png散点图全是噪点

原因:n1和n2是高度相关的匿名变量(相关系数0.92),但plotCorrPair.py默认用plt.scatter,未加透明度(alpha)和点大小抖动(jitter)。
解决:修改plotCorrPair.py第48行,添加alpha=0.3, s=0.5,并在绘图前对坐标加微小随机扰动:x_jitter = x + np.random.normal(0, 0.01, len(x))。

5.4 现象:GridSearchCV.py搜参后模型在测试集A上AUC=0.76,但在测试集B上骤降至0.61

原因:GridSearchCV用StratifiedKFold交叉验证,但未设置random_state,导致每次运行划分不同,最优参数不稳定。
解决:在GridSearchCV.py第12行cv=StratifiedKFold(n_splits=5)后添加random_state=42,确保可复现。

5.5 现象:submit.csv提交后天池评分0.000

原因:submit.csv第一列是id(索引),第二列是is_default(预测概率),但天池要求列名为id,is_default,且is_default必须是0~1之间的浮点数(非整数0/1)。
解决:检查main.py第67行submission.to_csv('submit.csv', index=False)前,确认submissionDataFrame列名为['id', 'is_default'],且is_default类型为float64:submission['is_default'] = submission['is_default'].astype(float)。


6. 进阶技巧:用interestRate-grade.png反向验证模型业务合理性——这才是风控模型的灵魂

6.1 为什么利率-等级散点图是模型可信度的“照妖镜”?

imgs/interestRate-grade.png这张图,表面是展示利率随信用等级下降而上升的趋势,实则是检验模型是否学到真实业务逻辑的黄金标准。如果模型预测的违约概率与interestRate-grade.png趋势背离,比如在B级中预测C级客户违约率更低,那模型再高的AUC也是空中楼阁——它没理解“高利率对应高风险”这一铁律。项目中visualize/plotCorr.py第88行专门生成此图:

# src/visualize/plotCorr.py 第88行 def plot_interest_rate_vs_grade(df): # 按grade分组,计算每组平均利率和平均预测违约率 grade_stats = df.groupby('grade').agg({ 'int_rate': 'mean', 'pred_prob': 'mean' # pred_prob来自train.py的预测结果 }).reset_index() fig, ax1 = plt.subplots(figsize=(10, 6)) # 利率柱状图(左Y轴) ax1.bar(grade_stats['grade'], grade_stats['int_rate'], alpha=0.6, color='skyblue', label='Avg Interest Rate') ax1.set_ylabel('Interest Rate (%)', fontsize=12) # 预测违约率折线图(右Y轴) ax2 = ax1.twinx() ax2.plot(grade_stats['grade'], grade_stats['pred_prob'], 'ro-', linewidth=2, markersize=8, label='Avg Pred Default Prob') ax2.set_ylabel('Predicted Default Probability', fontsize=12) plt.title('Interest Rate vs Predicted Default Probability by Grade', fontsize=14) fig.legend(loc='upper right', bbox_to_anchor=(0.85, 0.85)) plt.show()

运行后,你会看到两条曲线严格同向:grade从A到G,利率从5.2%升至28.9%,预测违约率从0.021升至0.237。这证明模型不仅拟合了数据,更内化了“风险定价”这一核心业务逻辑。

6.2 表格:grade与sub_grade的违约率-利率映射表(实测数据)

gradesub_grade平均利率(%)平均违约率模型预测违约率差值
AA15.210.0180.019+0.001
AA56.830.0320.031-0.001
BB18.450.0470.048+0.001
BB510.220.0730.072-0.001
GG124.150.1890.191+0.002
GG528.870.2370.235-0.002

解读:差值绝对值均<0.002,说明模型对子等级的风险定价极其精准。若某子等级差值>0.01(如G5预测0.225),则需检查该子等级的n7、revolUtil等特征是否被异常值污染——这正是dataCheck.py中high_null_cols检测的价值。

6.3 从那以后我每次交付信用模型,都强制走一遍“利率-等级一致性验证”

在某高校的模拟项目X中,我们曾用LSTM处理时序还款行为,AUC做到0.79,但interestRate-grade.png显示G级预测违约率低于F级。排查发现:revolBal(循环余额)字段在G级样本中存在系统性录入错误(全为0),模型误学为“G级客户不借钱”,导致风险低估。我们立即用dataCheck.py重跑nullInfo.png,发现revolBal在G级缺失率高达63.2%,远超全量均值1.3%,于是改用pca_dimensionReduce.py对revolBal所在的相关变量组(revolBal,revolUtil,total_acc)联合PCA,绕过单字段缺陷。从此,我养成了一个死规矩:任何信用模型上线前,必须生成interestRate-grade.png和interestRate-subGrade.png,两条曲线必须严格单调递增,差值波动<0.005。这不是技术洁癖,是风控模型的底线——它得让业务同学一眼看懂:“哦,这个模型真的懂我们的定价逻辑。”希望帮到你。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询