简介:这份资源是面向高校机器学习课程大作业场景的个贷违约预测完整项目源码,适合正在完成课程设计、需要参考完整建模流程的本科生与研究生。项目以ROC曲线下面积AUC作为核心评价指标,围绕描述性聚类到软聚类的思路展开,并实现了多层感知机、决策树(概率树)以及基于距离-概率转换的自定义模型三种方案,便于横向对比不同算法的预测效果。压缩包共59个文件,约142.31MB,包含csv数据集、py与go源码、md说明文档、png结果图、pdf与docx报告、pth模型权重及pptx汇报幻灯片等,覆盖数据、代码、报告与展示全链路。目前已有2871人学习下载。读者可据此获得可直接运行的完整赛题方案、模型训练与评估脚本、实验报告与答辩材料,并借助目录中按模型划分的模块结构快速定位代码,理解从特征处理到AUC评测的完整实现路径。
1. 个贷违约预测大作业:一份源码能跑通到什么程度
个贷违约预测这个题目,几乎每年都会出现在机器学习课程大作业的选题清单里。原因很直接:数据是结构化的、标签是二分类的、评价指标是明确的,看起来门槛不高,但真正动手做的时候,很多人会卡在几个具体的地方——类别极度不平衡导致模型全预测成"不违约"、特征里混着大量脱敏后的匿名变量不知道怎么处理、AUC 跑出来 0.9 但换一份数据就崩。这份"机器学习课程大作业个贷违约预测项目源码.zip"对应的正是这样一套完整流程:从原始表格数据读入、缺失值处理、特征工程、类别不平衡处理,到模型训练、阈值调优和结果输出。它适合两类人:一类是正在赶大作业、需要一份能跑通且能讲清楚每一步为什么这么做的参考实现;另一类是想拿一个真实结构化数据场景练手特征工程和模型评估的入门者。下面我按自己实际跑这类项目的顺序,把整条链路拆开讲。
2. 数据读入与标签定义:先把"违约"这件事说清楚
2.1 个贷违约预测的数据长什么样
个贷违约预测的原始数据通常是一张宽表,每行代表一个借款人的一笔贷款记录,列大致分几类:借款人基本信息(年龄、职业、收入水平等,但很多是脱敏后的编码值)、贷款属性(金额、期限、利率、还款方式)、历史行为(过往逾期次数、查询次数、授信额度使用率)、以及一个标签列表示是否违约。常见做法是标签列用 0/1 表示,1 代表违约。这里第一个容易翻车的地方是:不同来源的数据对"违约"的定义不一样——有的定义是逾期 90 天以上,有的定义是逾期 30 天以上,有的甚至把"提前还款"也算作某种负样本。你拿到数据后第一件事不是急着建模,而是确认标签列的含义和正负样本比例。
import pandas as pd import numpy as np # 读入原始数据,注意编码和分隔符要按实际文件调整 df = pd.read_csv('loan_data.csv', encoding='utf-8') # 查看基本信息 print(df.shape) print(df['default'].value_counts()) print(df['default'].value_counts(normalize=True)) # 确认标签列没有缺失 print(df['default'].isnull().sum())这段代码做三件事:看数据规模、看标签分布、确认标签列没有缺失。参数上,encoding要根据实际文件调整,中文环境常见gbk或utf-8;value_counts(normalize=True)给出的是比例而不是计数,方便你判断不平衡程度。如果正样本比例低于 5%,后面就必须认真处理不平衡问题,不能直接上默认参数的逻辑回归。
2.2 缺失值与异常值的处理策略
个贷数据里缺失值很常见,尤其是收入、职业这类字段。处理方式无非几种:删除、均值/中位数填充、众数填充、或者用模型预测填充。我的经验是,先看缺失比例——超过 60% 的列直接删掉,因为填充带来的噪声可能比信息还大;30% 到 60% 之间的列,考虑用模型预测填充或者单独标记一个"缺失"类别;30% 以下的列,中位数或众数填充通常够用。异常值方面,金额和利率这类字段经常有极端值,建议用分位数截断而不是直接删除,因为极端值本身可能就是违约信号。
# 计算每列缺失比例 missing_ratio = df.isnull().sum() / len(df) print(missing_ratio[missing_ratio > 0].sort_values(ascending=False)) # 删除缺失超过60%的列 cols_to_drop = missing_ratio[missing_ratio > 0.6].index.tolist() df = df.drop(columns=cols_to_drop) # 数值列用中位数填充,类别列用众数填充 num_cols = df.select_dtypes(include=[np.number]).columns.tolist() cat_cols = df.select_dtypes(include=['object']).columns.tolist() for col in num_cols: if df[col].isnull().sum() > 0: df[col] = df[col].fillna(df[col].median()) for col in cat_cols: if df[col].isnull().sum() > 0: df[col] = df[col].fillna(df[col].mode()[0]) # 对金额类字段做分位数截断 for col in ['loan_amount', 'interest_rate']: if col in df.columns: lower = df[col].quantile(0.01) upper = df[col].quantile(0.99) df[col] = df[col].clip(lower, upper)这里的关键参数是 0.6 这个阈值和 0.01/0.99 的分位数。阈值不是固定的,如果你的数据质量好,可以放宽到 0.7;分位数截断会改变原始分布,所以截断后建议重新看一下统计量,确认没有把重要信号截掉。类别列的众数填充在类别分布极度偏斜时可能引入偏差,如果某个类别列 90% 都是同一个值,填充众数等于没填,这时候可以考虑单独标记缺失。
3. 特征工程:把匿名变量变成模型能用的信号
3.1 数值特征的分箱与交叉
个贷数据里很多数值特征和标签的关系不是线性的,比如年龄和违约率往往是 U 型关系——太年轻和太年长的违约率都偏高。这时候直接放原始数值进模型,线性模型学不好,树模型虽然能自动切分,但分箱后往往更稳定。常见做法是对年龄、收入、贷款金额做等频分箱或自定义分箱,然后做 WOE 编码或者直接做 one-hot。另外,交叉特征在这个场景里很有效,比如"收入/贷款金额"这个比值,比单独看收入和贷款金额更能反映还款压力。
# 对年龄做自定义分箱 bins = [0, 25, 35, 45, 55, 100] labels = ['young', 'mid_young', 'mid', 'mid_old', 'old'] df['age_bin'] = pd.cut(df['age'], bins=bins, labels=labels) # 构造收入贷款比 df['income_loan_ratio'] = df['income'] / (df['loan_amount'] + 1) # 构造贷款期限与利率的交叉 df['term_rate_interaction'] = df['term'] * df['interest_rate'] # 查看分箱后的违约率 print(df.groupby('age_bin')['default'].mean())分箱的边界要根据实际数据的分布来定,上面给的边界只是示例。income_loan_ratio里加 1 是为了避免除零。交叉特征构造完后要检查一下和标签的相关性,相关性太低的可以直接去掉,避免维度爆炸。树模型对交叉特征不敏感,但线性模型和神经网络会明显受益。
3.2 类别特征的编码方式选择
类别特征编码是个老生常谈的问题,但在个贷场景里有几个具体注意点。高基数类别(比如职业代码有几百个取值)直接用 one-hot 会导致维度爆炸,常见做法是用目标编码(target encoding)或者频率编码。目标编码要注意用交叉验证的方式计算,否则会标签泄露。低基数类别(比如性别、婚姻状况)用 one-hot 就够了。另外,有些类别列虽然是数字编码,但实际是名义变量,不能当数值用,这个坑在脱敏数据里特别常见。
from sklearn.model_selection import KFold # 目标编码,用K折避免泄露 def target_encode(df, col, target, n_splits=5): df[col + '_te'] = np.nan kf = KFold(n_splits=n_splits, shuffle=True, random_state=42) for train_idx, val_idx in kf.split(df): train_fold = df.iloc[train_idx] val_fold = df.iloc[val_idx] mean_val = train_fold.groupby(col)[target].mean() df.loc[df.index[val_idx], col + '_te'] = val_fold[col].map(mean_val) # 用全局均值填充没覆盖到的类别 df[col + '_te'] = df[col + '_te'].fillna(df[target].mean()) return df # 对高基数类别做目标编码 for col in ['occupation_code', 'region_code']: if col in df.columns: df = target_encode(df, col, 'default') # 低基数类别做one-hot low_card_cols = ['gender', 'marital_status'] df = pd.get_dummies(df, columns=low_card_cols, drop_first=True)目标编码的 K 折数一般取 5 或 10,折数越多计算越慢但泄露越少。drop_first=True在 one-hot 里去掉一个类别避免共线性,对树模型其实无所谓,但线性模型需要。目标编码后建议看一下编码后的分布,如果某些类别的编码值极端偏离,可能是该类别样本太少导致的,可以考虑合并稀有类别。
4. 不平衡处理与模型训练:别让 AUC 骗了你
4.1 类别不平衡的三种处理路径
个贷违约预测里正样本通常只占几个百分点,不平衡处理直接决定模型能不能用。三条路径:一是重采样,包括过采样(SMOTE)和欠采样;二是改损失函数,比如给正样本更高权重;三是调阈值,模型输出概率后选一个使 F1 或 KS 最大的阈值。我的经验是,先试类别权重,因为最简单且不改变数据分布;如果效果不够,再试 SMOTE;欠采样在数据量大时可以用,但会丢信息。注意 SMOTE 不能直接用在有缺失值的数据上,也不能对类别特征做插值。
from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from imblearn.over_sampling import SMOTE from sklearn.model_selection import train_test_split # 准备特征和标签 feature_cols = [c for c in df.columns if c not in ['default', 'age_bin']] X = df[feature_cols].select_dtypes(include=[np.number]) y = df['default'] # 划分训练集和测试集,stratify保证分布一致 X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 路径一:类别权重 lr_weighted = LogisticRegression(class_weight='balanced', max_iter=1000) lr_weighted.fit(X_train, y_train) # 路径二:SMOTE过采样 smote = SMOTE(random_state=42) X_train_sm, y_train_sm = smote.fit_resample(X_train, y_train) rf_smote = RandomForestClassifier(n_estimators=200, random_state=42) rf_smote.fit(X_train_sm, y_train_sm)class_weight='balanced'会自动按类别频率反比设置权重。SMOTE 的random_state要固定,否则每次结果不一样。注意 SMOTE 只能在训练集上做,测试集必须保持原始分布,否则评估结果会虚高。stratify=y保证划分后训练集和测试集的正负比例一致,这个参数在不平衡场景里必加。
4.2 模型评估:AUC、KS 和阈值选择
个贷违约预测最常用的评估指标是 AUC 和 KS。AUC 衡量排序能力,KS 衡量区分度,两个都要看。但 AUC 高不代表业务上好用,因为业务上需要的是一个明确的阈值来区分"通过"和"拒绝"。所以训练完模型后,要在验证集上画 KS 曲线,找到 KS 最大的点作为阈值参考。另外,记得看混淆矩阵和查准率/查全率,尤其是在正样本少的时候,AUC 0.85 但查全率只有 0.3 的情况很常见。
from sklearn.metrics import roc_auc_score, roc_curve, confusion_matrix, classification_report # 预测概率 y_prob = rf_smote.predict_proba(X_test)[:, 1] # AUC auc = roc_auc_score(y_test, y_prob) print(f'AUC: {auc:.4f}') # KS fpr, tpr, thresholds = roc_curve(y_test, y_prob) ks = max(tpr - fpr) print(f'KS: {ks:.4f}') # 找KS最大对应的阈值 ks_idx = np.argmax(tpr - fpr) best_threshold = thresholds[ks_idx] print(f'Best threshold: {best_threshold:.4f}') # 按最优阈值输出分类报告 y_pred = (y_prob >= best_threshold).astype(int) print(confusion_matrix(y_test, y_pred)) print(classification_report(y_test, y_pred))predict_proba返回两列,第二列是正类概率。KS 的计算是max(tpr - fpr),对应的阈值就是业务上可以参考的切分点。classification_report里的 recall 对正类很重要,如果 recall 太低,说明模型漏掉了太多违约用户,业务上不可接受。阈值不是固定不变的,实际部署时还要根据通过率和坏账率的平衡来调整。
5. 避坑与排查:那些让大作业翻车的细节
5.1 数据泄露:AUC 0.99 的幻觉
现象:模型在验证集上 AUC 接近 1,但换一份数据或者交叉验证时结果大幅下降。原因:特征里混入了标签相关的信息,比如"是否催收"、"逾期天数"这类字段,这些字段在预测时根本拿不到。解决:建模前逐个检查特征,凡是和标签有直接因果关系的字段一律删掉;用时间切分而不是随机切分来验证,确保训练集的时间早于测试集。
5.2 目标编码泄露:编码后 AUC 虚高
现象:做了目标编码后 AUC 明显提升,但线上效果差。原因:目标编码时用了全量数据计算类别均值,导致验证集信息泄露到训练集。解决:必须用 K 折的方式计算目标编码,每一折的编码值只用训练折的数据计算;或者用平滑后的目标编码,给类别均值加一个先验权重。
5.3 缺失值填充引入偏差
现象:填充后模型在缺失率高的样本上表现特别差。原因:中位数填充把所有缺失样本都变成同一个值,模型无法区分"真的缺失"和"值就是中位数"。解决:对缺失率高的列,填充的同时加一个缺失标记列;或者用模型预测填充,但要注意预测模型本身不能用到标签。
5.4 阈值选择脱离业务
现象:模型 AUC 和 KS 都不错,但业务方说不能用。原因:阈值选的是 KS 最大点,但业务上要求通过率不能低于某个值,或者坏账率不能高于某个值。解决:在验证集上画出通过率-坏账率曲线,根据业务约束选阈值,而不是只看统计指标。
5.5 随机种子不固定导致结果不可复现
现象:每次跑代码结果都不一样,大作业报告里的数字和代码跑出来的对不上。原因:SMOTE、随机森林、数据划分等环节没有固定随机种子。解决:在所有涉及随机的环节都设置random_state,包括train_test_split、SMOTE、RandomForestClassifier等;如果用了 GPU,还要注意 CUDA 的随机性。
6. 从大作业到可复现:一个验证脚本和两条经验
最后一章说一个具体技巧:写一个端到端的验证脚本,把数据读入、特征工程、模型训练、评估串起来,每次改完代码跑一遍,确保结果可复现。这个脚本不需要多复杂,但要有固定的随机种子、固定的数据划分、固定的评估指标输出。下面是一个最小示例:
import pandas as pd import numpy as np from sklearn.model_selection import train_test_split from sklearn.ensemble import RandomForestClassifier from sklearn.metrics import roc_auc_score def run_pipeline(seed=42): df = pd.read_csv('loan_data.csv') # 省略特征工程细节,假设已经处理完 X = df.drop(columns=['default']) y = df['default'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=seed, stratify=y ) model = RandomForestClassifier(n_estimators=200, random_state=seed) model.fit(X_train, y_train) prob = model.predict_proba(X_test)[:, 1] auc = roc_auc_score(y_test, prob) return auc if __name__ == '__main__': for s in [42, 2024, 7]: print(f'seed={s}, AUC={run_pipeline(s):.4f}')这个脚本跑三个随机种子,如果 AUC 波动在 0.01 以内,说明模型比较稳定;如果波动超过 0.03,说明模型对数据划分太敏感,需要检查特征或增加数据量。两条经验:第一,大作业报告里不要只写一个 AUC 数字,要写多次运行的均值和标准差,这样才显得专业;第二,特征工程做完后一定要做特征重要性分析,把重要性接近零的特征删掉,既能提速又能减少过拟合。我自己做这类项目时,习惯在每次改完特征后重新跑一遍验证脚本,确认 AUC 没有下降才继续。希望帮到你。
本文还有配套的精品资源,点击获取