1. 从两个标题说起:这个毕设到底要解决什么问题
先把标题拆开看。这个项目其实包含了两条主线,一条是“基于多维特征融合的互联网贷款客户聚类与风险预警研究”,另一条是“基于数据挖掘的互联网贷款用途与风险定价可视化研究”。很多同学看到这种双标题会懵,不知道到底该做哪一个。我的理解是:这两个标题本质上是同一个业务闭环的两个阶段——先用聚类把客户分群、识别风险,再用可视化的方式把贷款用途和风险定价的逻辑呈现出来。前者是“算”,后者是“看”,合在一起才是一个完整的信贷风控分析系统。
互联网贷款这个场景,数据维度天然就多。用户的基本属性(年龄、职业、收入区间)、行为数据(申请频次、还款记录、逾期天数)、设备信息(登录设备数、IP归属地变化)、征信衍生变量(多头借贷指数、负债收入比)等等,随便一列就是几十个字段。如果直接把这些字段丢进模型,维度灾难先不说,业务上也解释不通。所以“多维特征融合”这个点,核心要解决的是:怎么把散落在不同表、不同粒度、不同量纲的特征,整合成一套能用于聚类和预警的输入矩阵。
风险预警和风险定价又是两回事。预警关注的是“这个客户会不会逾期”,是一个二分类或者异常检测问题;定价关注的是“这个客户应该收多少利率才合理”,是一个回归或者分群映射问题。很多毕设把这两个混在一起做,最后逻辑就乱了。我的建议是:聚类负责分群,分群结果同时服务于预警和定价——高风险群进预警名单,不同风险等级的群映射到不同的利率区间。这样两条线就串起来了。
可视化部分,热词里出现了ECharts、百度可视化大屏、企业级数据可视化这些词,说明大家对这个项目的期待不只是跑个模型出个AUC,而是要有一个能看、能讲、能演示的前端界面。这一点在毕设答辩里非常关键,评委老师不一定懂你的GMM推导,但他一定看得懂大屏上跳动的风险分布图。
这个项目适合谁?如果你是计算机、数据科学、统计相关专业的本科或硕士,正在找毕设题目,或者你已经选了类似方向但不知道从哪下手,那这篇内容就是给你写的。我会从数据构造、特征工程、聚类选型、预警建模、可视化落地这几个环节,把每个关键决策背后的“为什么”讲清楚,同时给出可以直接复现的代码骨架和踩坑经验。
2. 数据从哪来:没有真实信贷数据时怎么构造一份能跑通的样本
2.1 公开数据集的可用性与局限
做互联网贷款相关的毕设,第一个卡点永远是数据。真实信贷数据涉及用户隐私,不可能拿到。那怎么办?常见的替代方案有三类:一是用Lending Club或Prosper的公开贷款数据,这两个数据集在Kaggle上都能找到,字段比较全,有贷款金额、利率、期限、借款人收入、FICO分数、逾期状态等;二是用德国信用数据集(German Credit Data)或澳大利亚信用数据集,规模小但干净,适合做算法验证;三是自己用Python的Faker库合成数据,完全可控但真实性差。
我的建议是:主实验用Lending Club的一个子集(比如2008-2015年的已完结贷款),因为它的字段结构和互联网贷款最接近,有利率(int_rate)、贷款用途(purpose)、风险等级(grade/sub_grade)这些关键列。然后用合成数据做补充实验,验证模型在不同分布下的稳定性。这样既保证了真实性,又能在答辩时说明“我有做鲁棒性测试”。
Lending Club数据有个坑要注意:它的利率字段是字符串格式,带百分号,比如“13.56%”,需要先去掉百分号再转float。另外,贷款状态(loan_status)有好几种取值,做二分类预警时通常把“Charged Off”和“Default”归为坏样本,“Fully Paid”归为好样本,“Current”和“Late”这类中间状态要么剔除要么单独处理。我一般会剔除“Current”,因为它的最终结果还没确定,强行标注会引入噪声。
2.2 合成数据的字段设计思路
如果要用合成数据,字段设计不能瞎编,要贴合互联网贷款的业务逻辑。我通常会构造这么几组变量:
- 用户基础属性:年龄(18-65)、月收入(3000-50000)、学历(有序分类)、婚姻状态、职业类型。
- 借贷行为特征:历史借款次数、历史逾期次数、当前待还笔数、平均借款金额、借款期限偏好。
- 征信衍生特征:多头借贷指数(近3个月申请平台数)、负债收入比(DTI)、信用历史长度(月)。
- 设备与行为特征:登录设备数、近7天登录次数、申请时段(是否深夜)、IP归属地变更次数。
这些字段之间要有相关性,不能完全独立生成。比如收入越高,DTI通常越低;逾期次数越多,多头借贷指数往往越高。可以用Copula或者简单的线性加噪声的方式来生成相关变量。这样聚类出来的群才有业务意义,不然就是一堆随机点的机械分割。
提示:合成数据一定要固定随机种子(比如np.random.seed(42)),否则每次跑出来的聚类结果都不一样,答辩演示时会很尴尬。
2.3 数据清洗中容易被忽略的细节
不管用哪种数据,清洗环节有几个坑是高频出现的。第一,缺失值处理不能一刀切用均值填充。比如“月收入”缺失,用均值填充会扭曲低收入和高收入两端的分布,更好的做法是分群填充或者用模型预测填充。第二,异常值要区分“错误值”和“真实极端值”。比如年龄填了200岁,这是错误值,直接删;但月收入填了50万,可能是真实的高净值用户,不能随便删,可以考虑截断或者单独分群。第三,类别型变量的编码方式会影响聚类距离的计算。如果直接用LabelEncoder把“职业类型”编成0-9,聚类算法会认为职业8和职业9的距离比职业0和职业1更近,这显然不合理。正确的做法是用One-Hot编码或者目标编码。
3. 多维特征融合:把几十个字段拧成一股绳
3.1 特征融合到底融合什么
“多维特征融合”这个词听起来很学术,拆开看其实就三件事:特征对齐、特征变换、特征降维。特征对齐解决的是不同表之间粒度不一致的问题,比如用户属性表是一人一行,但行为日志表是一人多行,需要先聚合到用户粒度。特征变换解决的是量纲和分布问题,比如收入是万元级,逾期次数是个位数,直接放一起聚类,收入会主导距离计算。特征降维解决的是维度灾难和共线性问题,几十个特征里有很多是高度相关的,比如“近3个月申请平台数”和“近6个月申请平台数”,留着两个只会增加噪声。
我一般会按这个流程走:先把所有特征按用户ID聚合,然后做缺失值填充和异常值处理,接着对连续变量做标准化(Z-Score或RobustScaler),对类别变量做One-Hot,最后用PCA或者特征重要性筛选做降维。注意,标准化要在训练集上fit,然后transform到测试集,不能全量fit,否则会有数据泄露。
3.2 连续变量的标准化:Z-Score还是RobustScaler
这个问题看起来小,但对聚类结果影响很大。Z-Score标准化后,数据的均值是0,标准差是1,适合近似正态分布的变量。但如果变量有极端异常值,比如收入里有几个几百万的,Z-Score会被拉偏,标准化后的分布仍然有长尾。RobustScaler用的是中位数和四分位距,对异常值更稳健。
我的经验是:收入、负债这类容易有极端值的变量用RobustScaler,年龄、逾期次数这类分布相对均匀的用Z-Score。如果懒得区分,统一用RobustScaler也不会出大错。代码上很简单:
from sklearn.preprocessing import RobustScaler, StandardScaler robust_cols = ['monthly_income', 'dti', 'loan_amount'] standard_cols = ['age', 'delinquency_count', 'credit_history_months'] scaler_robust = RobustScaler() scaler_standard = StandardScaler() df[robust_cols] = scaler_robust.fit_transform(df[robust_cols]) df[standard_cols] = scaler_standard.fit_transform(df[standard_cols])3.3 类别变量的编码陷阱
前面提了LabelEncoder的问题,这里展开说。One-Hot编码是 safest 的选择,但如果类别数很多(比如职业类型有50种),One-Hot后维度会爆炸。这时候可以用目标编码(Target Encoding),用该类别的坏样本率来替换类别值。但目标编码容易过拟合,必须配合交叉验证或者平滑处理。
还有一个折中方案:把高频类别保留,低频类别合并成“其他”。比如职业类型里,前10种占了90%的样本,剩下的40种合并成“其他”。这样既控制了维度,又保留了主要信息。
3.4 降维:PCA不是唯一选择
PCA是最常用的降维方法,但它假设主成分是原始特征的线性组合,解释性差。对于信贷场景,业务方往往想知道“这个客户为什么被分到高风险群”,PCA的主成分很难回答这个问题。替代方案有:
- 特征重要性筛选:用随机森林或XGBoost跑一遍预警模型,取重要性前20的特征。
- 方差膨胀因子(VIF)筛选:剔除共线性高的特征。
- UMAP或t-SNE:用于可视化降维,但不适合作为聚类输入,因为它们是非线性变换,距离含义会变。
我通常的做法是:先用VIF剔除共线性特征,再用随机森林重要性筛选到20-30个特征,最后如果维度还是高,再用PCA降到10维左右。这样既控制了维度,又保留了一定的解释性。
4. 聚类算法选型:K-Means、GMM还是层次聚类
4.1 三种聚类算法的本质差异
热词里出现了“层次聚类python”“sklearn 层次聚类”“k值聚类”“高斯混合模型gmm”,说明大家在这个环节纠结得最多。我把这三种算法的核心差异列一下:
| 算法 | 核心假设 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| K-Means | 簇是球形,方差相近 | 快、简单、可扩展 | 对初始点敏感,需指定K,对非球形簇效果差 | 样本量大、簇形状规则 |
| GMM | 数据由多个高斯分布混合生成 | 软聚类,给出概率,可处理椭圆形簇 | 计算慢,对初始化敏感,可能收敛到局部最优 | 簇有重叠、需要概率输出 |
| 层次聚类 | 簇有层次结构 | 不需要指定K,可可视化树状图 | 计算复杂度高(O(n^2)),不适合大样本 | 小样本、需要探索簇结构 |
对于互联网贷款客户聚类,我一般推荐GMM。原因是:客户群体之间的边界往往不是硬性的,一个客户可能同时具有“高收入但高负债”和“低收入但低负债”的特征,硬聚类会强行把他分到一边,而GMM可以给出“70%属于A群,30%属于B群”的概率输出,这对后续的风险定价更有参考价值。
4.2 K值怎么定:肘部法、轮廓系数和业务判断
K-Means和GMM都需要指定簇数。肘部法看的是簇内平方和(inertia)随K变化的拐点,轮廓系数看的是簇内紧密度和簇间分离度的综合指标。但这两个指标都只是参考,最终K值要结合业务判断。
比如你跑出来K=3时轮廓系数最高,但业务上你希望把客户分成“优质、次优、关注、高风险”四档,那K=4更合理。我通常会跑K=2到K=10,把肘部图和轮廓系数图画出来,然后选一个指标不太差、业务上能解释的K值。
from sklearn.mixture import GaussianMixture from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt k_range = range(2, 11) bic_scores = [] sil_scores = [] for k in k_range: gmm = GaussianMixture(n_components=k, random_state=42, n_init=5) labels = gmm.fit_predict(X_pca) bic_scores.append(gmm.bic(X_pca)) sil_scores.append(silhouette_score(X_pca, labels)) fig, ax1 = plt.subplots() ax1.plot(k_range, bic_scores, 'b-o', label='BIC') ax1.set_xlabel('K') ax1.set_ylabel('BIC', color='b') ax2 = ax1.twinx() ax2.plot(k_range, sil_scores, 'r-s', label='Silhouette') ax2.set_ylabel('Silhouette', color='r') plt.show()GMM有个好处是可以用BIC(贝叶斯信息准则)来选K,BIC越小越好。但BIC倾向于选简单的模型,有时候会低估K值,所以还是要结合轮廓系数和业务判断。
4.3 聚类结果的业务解读
聚类跑完不是终点,关键是怎么解读每个群的特征。我一般会做两件事:一是计算每个群在各个特征上的均值,和全局均值对比,找出这个群的“标签”;二是看每个群的坏样本率(逾期率),这直接决定了风险等级。
举个例子,假设K=4,跑出来四个群:
- 群0:平均年龄35岁,月收入1.5万,DTI 0.3,逾期率2%。标签:优质客户。
- 群1:平均年龄28岁,月收入8000,DTI 0.5,逾期率8%。标签:年轻成长型,中等风险。
- 群2:平均年龄42岁,月收入2万,DTI 0.6,逾期率15%。标签:高收入高负债,关注类。
- 群3:平均年龄30岁,月收入5000,DTI 0.8,逾期率35%。标签:高风险。
这样分完,预警名单就是群2和群3,定价上群0给最低利率,群3给最高利率或者直接拒贷。整个逻辑就通了。
5. 风险预警建模:从聚类标签到可解释的预警规则
5.1 预警模型选型:XGBoost还是逻辑回归
聚类分群之后,风险预警可以有两种做法:一种是把聚类标签作为特征,训练一个监督模型;另一种是直接用原始特征训练预警模型,聚类结果只用于分群展示。我倾向于两者结合:用聚类标签作为特征之一,同时保留原始特征,这样模型既能利用分群信息,又能捕捉群内的细微差异。
模型选型上,XGBoost和LightGBM是首选,因为信贷数据往往有非线性关系和高维稀疏特征,树模型天然适合。但逻辑回归也有优势:系数可解释,能直接输出概率,方便做评分卡。如果答辩时评委问“为什么这个客户被预警了”,逻辑回归可以给出“因为他的DTI系数是正的,且值很高”,XGBoost只能给特征重要性,解释性差一些。
我的建议是:主模型用XGBoost,同时训练一个逻辑回归作为可解释性补充。如果时间紧,只做XGBoost也够用,但一定要用SHAP值做解释。
5.2 样本不平衡的处理
信贷预警的坏样本率通常很低,Lending Club数据里逾期率大概在15%-20%,但真实互联网贷款可能只有5%甚至更低。样本不平衡会导致模型偏向多数类,坏样本的召回率很低。处理方法有:
- 过采样:SMOTE、ADASYN,合成少数类样本。优点是简单,缺点是可能引入噪声。
- 欠采样:随机删除多数类样本。优点是快,缺点是浪费数据。
- 调整类别权重:在XGBoost里设置scale_pos_weight,在逻辑回归里设置class_weight='balanced'。
- 阈值调整:不改变模型,只改变预测概率的截断阈值。
我一般先用类别权重,如果效果不够再用SMOTE。注意SMOTE只能在训练集上做,不能对测试集做,否则评估结果会虚高。
from xgboost import XGBClassifier from sklearn.model_selection import train_test_split from imblearn.over_sampling import SMOTE X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=42, stratify=y) # 先试类别权重 scale_pos_weight = (y_train == 0).sum() / (y_train == 1).sum() model = XGBClassifier(scale_pos_weight=scale_pos_weight, random_state=42) model.fit(X_train, y_train) # 如果效果不够,再用SMOTE smote = SMOTE(random_state=42) X_train_res, y_train_res = smote.fit_resample(X_train, y_train) model_smote = XGBClassifier(random_state=42) model_smote.fit(X_train_res, y_train_res)5.3 评估指标:AUC不是唯一标准
AUC是信贷预警最常用的指标,但它有个问题:它衡量的是排序能力,不关心具体的概率校准。在实际业务里,我们往往需要一个准确的概率值来做定价,这时候AUC就不够了。补充指标有:
- KS值:衡量模型区分好坏样本的能力,KS>0.3通常认为可用。
- PR曲线:在样本极度不平衡时,PR曲线比ROC曲线更能反映模型对少数类的识别能力。
- 概率校准:用Platt Scaling或Isotonic Regression校准概率,让预测概率更接近真实逾期率。
我一般会同时看AUC、KS和PR-AUC,如果三个指标都还行,模型基本可用。如果AUC高但KS低,说明模型排序能力有但区分度不够,可能需要调整特征或模型。
5.4 预警规则的可解释化
模型跑完,业务方最关心的是“哪些客户会被预警,为什么”。SHAP值是当前最流行的解释工具,它可以给出每个特征对每个样本预测值的贡献。对于信贷场景,我一般会输出两类解释:
- 全局解释:哪些特征对预警最重要,比如DTI、多头借贷指数、逾期次数。
- 局部解释:某个具体客户为什么被预警,比如“他的DTI=0.8,贡献了+0.3的预警分数”。
import shap explainer = shap.TreeExplainer(model) shap_values = explainer.shap_values(X_test) # 全局特征重要性 shap.summary_plot(shap_values, X_test, plot_type="bar") # 单个样本解释 shap.force_plot(explainer.expected_value, shap_values[0,:], X_test.iloc[0,:])这些图可以直接放到可视化大屏上,答辩时非常加分。
6. 可视化落地:从ECharts大屏到风险定价展示
6.1 可视化大屏的整体布局
热词里“百度可视化大屏”“企业级数据可视化”“ECharts数据可视化”反复出现,说明大家对这个项目的可视化期待很高。我建议大屏至少包含这几个模块:
- 顶部指标卡:总客户数、预警客户数、平均逾期率、平均利率。
- 左侧聚类分布:用饼图或环形图展示各群的客户占比。
- 中间风险地图:用热力图或散点图展示客户在“收入-DTI”平面上的分布,颜色代表风险等级。
- 右侧预警列表:滚动展示高风险客户ID和预警原因。
- 底部趋势图:展示近12个月的逾期率变化和利率变化。
ECharts是实现这些图表的最佳选择,因为它免费、文档全、社区活跃。如果要做大屏适配,可以用rem做单位,配合flex布局,保证在不同分辨率下不变形。
6.2 聚类结果的可视化:降维到二维再画
聚类是在高维空间做的,但可视化只能画二维或三维。所以需要先用PCA或t-SNE把数据降到二维,再画散点图,颜色代表聚类标签。注意,PCA降维后的坐标轴没有业务含义,不要标成“收入”和“年龄”,直接标“PC1”和“PC2”就行。
from sklearn.decomposition import PCA import plotly.express as px pca = PCA(n_components=2) X_2d = pca.fit_transform(X_pca) fig = px.scatter(x=X_2d[:,0], y=X_2d[:,1], color=labels.astype(str), title='客户聚类分布(PCA降维)', labels={'x': 'PC1', 'y': 'PC2', 'color': 'Cluster'}) fig.show()如果要用ECharts,可以把X_2d和labels导出成JSON,前端用ECharts的scatter图渲染。这样前后端分离,Python只负责算,前端只负责画。
6.3 风险定价的可视化:利率-风险等级映射
风险定价的可视化核心是展示“不同风险等级的客户对应什么利率区间”。可以用箱线图或者小提琴图,横轴是风险等级,纵轴是利率。这样一眼就能看出高风险群的利率明显高于低风险群。
如果要做更细的展示,可以画一个“风险分数-利率”的散点图,每个点代表一个客户,颜色代表实际逾期状态。这样既能展示定价策略,又能验证定价是否合理——如果高风险客户拿了低利率,说明定价模型有问题。
6.4 前后端联调中的常见坑
做可视化大屏最容易踩的坑是数据格式不匹配。Python导出的JSON里,numpy的int64和float64不能直接序列化,需要转成Python原生类型。解决办法是用json.dumps时指定default=str,或者在导出前用.astype(int)和.astype(float)转换。
另一个坑是跨域请求。如果前端和后端分开部署,浏览器会拦截跨域请求。解决办法是在后端加CORS头,或者用Nginx做反向代理。如果只是本地演示,直接用Flask的flask-cors库就行。
from flask import Flask, jsonify from flask_cors import CORS app = Flask(__name__) CORS(app) @app.route('/api/cluster_data') def cluster_data(): data = {'clusters': labels.tolist(), 'pca_x': X_2d[:,0].tolist(), 'pca_y': X_2d[:,1].tolist()} return jsonify(data)7. 答辩前必须检查的几件事
7.1 代码能不能一键跑通
毕设答辩最尴尬的场景是:老师让你现场跑一下代码,结果报错。所以答辩前一定要在一个干净的环境里从头跑一遍,确保requirements.txt里的依赖版本正确,数据文件路径没有硬编码,随机种子固定。我一般会写一个run_all.py,把数据加载、特征工程、聚类、预警、可视化导出串起来,一键执行。
7.2 业务逻辑能不能自圆其说
评委老师不一定会问算法细节,但一定会问“你这个东西有什么用”。所以你要准备好一套业务话术:聚类把客户分成几群,每群的特征是什么,预警模型抓出了哪些高风险客户,定价策略怎么定。最好能举一个具体客户的例子,从原始数据到最终预警和定价,完整走一遍。
7.3 可视化大屏能不能离线演示
答辩现场的网络不一定稳定,如果大屏依赖CDN加载ECharts,可能会加载失败。解决办法是把ECharts的JS文件下载到本地,用相对路径引用。数据也最好提前导出成JSON文件,不要现场调API。
7.4 论文和代码的一致性
很多同学代码跑的是A方案,论文写的是B方案,答辩时被老师发现就麻烦了。所以定稿前一定要核对:论文里的公式、参数、流程图,和代码里的实现是否一致。如果不一致,要么改代码,要么改论文,不能两边各说各话。
8. 我在这个项目里踩过的几个坑
第一个坑是聚类前忘了做标准化,结果收入变量主导了距离计算,聚类结果完全按收入分层,其他特征全被淹没。后来加了RobustScaler才正常。这个坑很隐蔽,因为代码不会报错,只是结果不对,如果不做业务解读很难发现。
第二个坑是GMM的初始化。GMM对初始均值向量很敏感,默认的kmeans初始化有时候会收敛到局部最优。解决办法是设置n_init=10,多跑几次取BIC最小的。这个参数在sklearn的GaussianMixture里默认是1,很多人不知道要改。
第三个坑是可视化大屏的适配。我用的是1920x1080的设计稿,结果答辩教室的投影是1024x768,大屏直接溢出,右侧的预警列表完全看不到。后来改成用百分比布局,才勉强适配。建议一开始就用响应式布局,别等到答辩前才发现。
第四个坑是SHAP值的计算速度。XGBoost模型跑SHAP值,如果样本量上万,计算会很慢。解决办法是用shap.sample()抽样计算,或者用TreeExplainer的feature_perturbation="interventional"模式,速度会快很多。
第五个坑是论文查重。信贷风控这个方向,很多术语和公式是通用的,比如AUC的定义、KS的计算公式,这些很容易重复。我的经验是:公式部分用MathType重新打一遍,不要直接复制;业务描述部分用自己的话重新组织,不要照搬参考文献。
9. 这个项目还能怎么扩展
如果你时间充裕,有几个方向可以继续挖。一是引入时间维度,做动态聚类,看客户群随时间怎么迁移。二是把风险定价做成一个优化问题,在利润最大化和风险最小化之间找平衡。三是接入实时数据流,用Kafka或Flume做在线预警。四是把模型部署成API,用FastAPI或Flask封装,前端直接调用。
不过对于毕设来说,把当前这套流程跑通、跑稳、讲清楚,已经足够了。贪多嚼不烂,答辩时老师更看重的是你对每个环节的理解深度,而不是堆了多少技术名词。