ARTICLE · INTELLIGENCE

战地情报 · 详情页

来自尧图项目组的一线实战观察与深度解析

多维特征融合的互联网贷款客户聚类与风险预警实战

多维特征融合的互联网贷款客户聚类与风险预警实战

27届计算机毕设源码:基于多维特征融合的互联网贷款客户聚类与风险预警研究

我27届毕业设计做的是这个题目:“基于多维特征融合的互联网贷款客户聚类与风险预警研究”,后来又延伸出了一个方向“基于数据挖掘的互联网贷款用途与风险定价可视化研究”。很多学弟学妹在选毕设题目的时候会纠结,这个题到底难不难、怎么做、能不能有源码参考。我的建议是——这个方向确实值得做,因为它的数据源规范、算法链条完整、可视化效果好,而且跟当下互联网金融风控的实际业务结合得很紧。这篇文章把我从开题到最后答辩的完整思路、技术选型、实操步骤和踩过的坑都整理出来,希望能给正在做同类题目的同学一点参考。

这个项目的核心是以客户多维特征为基础,先用聚类算法把互联网贷款客户划分成不同的群体,然后针对每个群体的违约特征做风险预警,最后用数据挖掘的结果驱动贷款用途分析和风险定价,整个过程用可视化大屏展示成果。说白了,就是让贷款平台能回答三个问题:客户是谁、风险多大、该怎么定价。

因为内容偏应用型,整个项目的难易程度对本科毕设来说是刚刚好的——算法不劝退、代码量足够证明工作量、可视化又能拉高整体答辩的观感分。如果你正在做“客户聚类”、“风险预测”、“Python数据分析与可视化”方向,这篇内容可以直接对应到你的技术栈和开发节奏。

1. 项目整体设计与需求拆解

1.1 核心需求与切入点

先说这个题目到底在解决什么问题。互联网贷款平台(比如消费金融、小贷产品、银行线上信贷)最大的痛点只有一个:不知道借款的人是谁,也不知道借了钱之后会干什么。传统的线下风控主要靠人工审核和征信报告,但互联网贷款完全线上化,客群覆盖面大、数据维度杂、单笔金额小,靠人工根本不现实。

所以“客户聚类”解决的是客群分层问题,“风险预警”解决的是坏账预测问题,而“贷款用途与风险定价”是把这两部分的结果变成业务决策——告诉平台:这笔钱借给谁、利率定在多少、需要不需要人工介入。

放到毕设语境里,我把它拆成了四个模块:

  1. 数据采集与清洗——拿到一份结构合理、字段完整的模拟贷款数据集
  2. 多维特征融合——把用户的基础信息、行为数据、借贷历史组合成可用特征
  3. 聚类分析与风险预警——用无监督学习分组,用有监督学习预测风险
  4. 可视化展示——把聚类结果、风险分布、贷款用途、定价区间呈现到Web大屏

1.2 为什么选择多维特征融合与聚类这个技术路线

这一点在开题答辩时一定会被问到,你把理由理清楚,一组题目就立得住。

多维特征融合的本质是:单一维度的特征无法描述一个复杂的人。比如只看收入,年收入50万的个体户可能不敢借5万,因为资金全压在货上;收入10万的打工人反而更敢借,因为工资稳定可预期。只看历史借款次数也不行,多借不等于高风险,还要看还款行为、间隔时间、用途等等。

聚类在这个场景里的好处是不需要标注数据。征信样本里真正的违约客户比例是很低的(好客户可能占95%以上),如果你一上来就做有监督分类,正负样本极度不均衡,训练效果通常很差。而聚类不用标签,直接把客户按特征相似度分成几群,每个群的违约率、行为偏好一目了然,在早期风控里是非常实用的客群划分手段。

所以说这个项目的组合逻辑是:无监督负责看清客群结构,有监督负责落地风险预测,可视化负责把模型结果翻译成人话。三条线互不冲突又互相支撑,答辩时很好讲。

1.3 数据集设计与模拟方案

校招毕设最大的麻烦在于:真实贷款数据拿不到。我当时的做法是自建模拟数据集,然后用人工“注入”违约模式,让数据看起来更接近真实。

模拟数据字段设计如下:

特征类别字段举例说明
基础属性年龄、性别、城市等级、学历客户画像底层数据
收入资产月收入、公积金基数、房产车辆标志还款能力的核心描述
借贷行为借款金额、期限、利率、历史借款次数平台最关心的行为记录
还款表现历史逾期天数、当前逾期次数、还款间隔风险判别的主要依据
消费行为消费频率、大额支出占比、夜间消费比例贷款用途推断的辅助维度

模拟数据的生成逻辑我用的是“正态分布 + 人为规则”。比如月收入取均值为1.2万、标准差为6000的正态分布,然后把收入低于5000且借款金额大于3万的样本,赋予更高的初始逾期概率。这样生成出的数据虽然纯属模拟,但特征之间的相关方向是对的,聚类模型和风险模型跑出来的结果也有业务解释力。

提示:如果你用的是公开的信贷数据集(比如Kaggle上的German Credit、Lending Club Loan Data),注意字段含义与国内语境差异,建议在报告中说明数据来源和处理方式,避免答辩被追问时答不上来。

2. 技术架构、算法选型与工具链

2.1 整体技术栈选型与分工

项目的技术栈划分得很简单,主语言就是Python,辅助Web展示用了HTML + ECharts。

模块技术选型负责内容
数据处理Pandas、NumPy、Scikit-learn清洗、归一化、特征工程
聚类模型K-Means、层次聚类(AgglomerativeClustering)客群划分
风险预警逻辑回归、XGBoost、LightGBM违约概率预测
可视化分析Flask后端 + ECharts前端大数据屏展示
数据探索Matplotlib、Seaborn前期EDA分析与论文配图

选择这套组合的原因很现实:Python生态对数据挖掘的支持最成熟,Sklearn一行代码就能跑通K-Means,XGBoost和LightGBM直接用pip装就能跑,ECharts做出来的可视化比Matplotlib好看一个数量级,而且不依赖付费BI工具,全是免费开源。

2.2 聚类算法选型:K-Means vs 层次聚类

做客户聚类,最终目标不是算法复杂度,而是“分出来的群体能不能解释”。我当时对比了两种方法:

K-Means:运行速度快、超参少、适合大数据量,但需要提前指定K值,且对初始质心和离群点敏感。对于10万条以内的数据量,性能完全够用。

层次聚类(AgglomerativeClustering):不需要预设K值(可以通过树状图观察),聚类结果更稳定。缺点是计算复杂度高,样本量大了之后内存占用会急剧上升,在Python里超过3万条就不太建议直接用。

我的做法是先用层次聚类对一小批样本(1万条)做树状图,确定大致的分群数量,再用K-Means把全部数据分群。这是实践里很常见的“混合策略”:

  1. 抽样1万条 → 层次聚类 → 画树状图 → 确定K的范围
  2. 用轮廓系数(Silhouette Score)和SSE(簇内平方和)交叉验证选K
  3. 用K-Means跑全量数据,输出中心点与分群结果

2.3 聚类效果评估指标

很多同学分完类就完事了,答辩时被问“你这个聚类结果怎么评价好坏”,就答不上来了。这里我整理了几个常用指标:

轮廓系数(Silhouette Score):值域[-1,1],越大说明样本越贴近自己的簇而远离其他的簇。一般超过0.25就可以接受,0.4以上说明聚类效果不错。这个在Sklearn里一行代码算出来。

SSE(簇内平方和):每个点到所属簇中心的距离平方和。选K的时候画肘部曲线,看拐点。SSE下降得越快,说明增加簇带来的收益越大;下降变平缓后,继续增加K的意义就不大了。

业务可解释性:这是我最强调的,也是论文里单独拿出一个章节写的。分出来的每个群,要能命名、能描述。比如“高薪短借客户”“低收长借高危客群”“稳定消费型客户”等等。如果群里的特征分布乱成一锅粥,指标再高也没有落地价值。

我当时选K=5,最终呈现的分群画像大概是:

客群标签占比核心特征
A群:白领短借群32%中高收入、借款金额小、期限短、历史逾期少
B群:小微经营群18%收入波动大、借款频率高、金额中等、笔数多
C群:高收入低风险群22%高收入高学历、资产丰富、还款极规律
D群:高风险长借群15%低收入、借款金额大、期限长、历史逾期明显
E群:消费主力群13%中收入、消费行为活跃、借款用途偏向购物和旅游

每个群的画像靠的是把聚类结果对应回原始特征的均值、众数做的统计汇总。这一步不复杂但很关键,它是后续风险定价和可视化的基础。

2.4 数据挖掘与风险建模的衔接

聚类做好了,怎么往风险预警上走?我当时的思路是:

把所有客户先分群,再把每个群内的数据打上“是否逾期”的标签(以“逾期天数>15”为坏客户定义)。然后用标签训练一个二分类模型,预测客户的违约概率。这个概率同时输出为风险评分(0-100分),映射规则可以简单定义为风险评分 = round(100 - 违约概率*100),也可用逻辑回归的系数做更规范的分数换算。

比较讨巧的是,模型里可以加入“所属聚类簇编号”作为特征,让聚类结果反哺有监督模型。比如D群客户的风险权重天然就高,模型会自动学到“处于这个簇的客户,违约概率要上调”。这一步让你的项目在逻辑上形成闭环,论文里也很好画框图。

3. 实操实现:从数据清洗到可视化大屏

3.1 数据清洗与预处理

第一件事永远是看数据长什么样。运行df.info()、df.describe()、df.isnull().sum(),把字段类型、缺失值、异常值摸个底。我的数据集是自建的,缺失值不多,但有几类处理思路必须记录下来:

连续特征做缺失值填补。分三类:均值填充(适用于近似正态分布的字段,比如收入)、中位数填充(适合右偏分布,如借款金额)、按分组填充(比如同一城市等级下,取该组的均值作为缺省值)。不要一股脑都用均值填充,答辩时被问到“为什么这里用中位数不用均值”,你要能说出分布形态的道理。

分类特征做编码。学历、城市等级、职业类型这类字段要转成数值。可以用LabelEncoder或pd.get_dummies()。注意:如果你后续要做逻辑回归,尽量用One-Hot编码,避免把类别之间的等级性质硬编码进去;如果用树模型,LabelEncoder问题不大。

异常值处理。对连续特征用IQR法做截尾:Q1 - 1.5*IQR和Q3 + 1.5*IQR之外的样本,不直接删除,而是截断到边界值。借贷场景里极端值可能代表真实的大客户或高风险客户,直接删会损失信息,截尾更稳妥。

归一化/标准化。K-Means这类基于距离的算法必须做,不然收入这个维度会直接碾压其他所有特征。我当时用的StandardScaler而不是MinMaxScaler,因为它对离群值不那么敏感。这是实操中不少人忽略的细节。

3.2 多维特征融合的具体做法

我这里说的“多维特征融合”不是深度学习里的特征拼接,而是工程化的特征衍生。原始字段是“收入”“消费金额”“借款次数”这种单点值,但真正有判别力的是它们之间的比例关系和派生指标。我实际用到的融合方式主要有三类:

比值类特征。比如“月供收入比 = 月还款金额 / 月收入”,“消费收入比 = 月消费 / 月收入”,“借款频率 = 历史借款次数 / 注册时长(月)”。这些比例特征比绝对值更稳定,也能直接解释客户的还款意愿和资金链条紧不紧张。

聚合类特征。按用户ID对历史借贷行为做汇总。比如“平均借款金额”“最大借款金额”“标准差借款金额”、“提前还款次数”、“逾期次数占比”。这些指标描述的是客户的资金使用习惯和风险惯性。

交叉类特征。两个特征相乘或相加组合而成。常见做法有“收入学历权重”,把学历映射成数值后再跟收入相乘,高分高收入客户的得分会显著高量级。又比如“借款金额期限”,无监督模型容易学到这类隐含模式。

我当时没有用PCA做降维,原因有两个:一是聚类的业务可解释性在论文里是加分项,PCA降维后特征变得不可解释;二是字段量几十个不算多,相关性也控制在合理范围。但如果你发现特征数量上百,可能存在共线性,可以用PCA或者相关性热力图筛一遍。

3.3 K-Means聚类的Python实现

直接给出我当时的核心代码框架,这份代码在Sklearn版本1.3以上都能直接跑:

import pandas as pd from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans, AgglomerativeClustering from sklearn.metrics import silhouette_score import matplotlib.pyplot as plt # 读取已预处理的数据 df = pd.read_csv('loan_customer_processed.csv') # 选择聚类特征列 feature_cols = ['age', 'monthly_income', 'loan_amount', 'loan_term', 'monthly_payment_income_ratio', 'consume_income_ratio', 'history_loan_counts', 'overdue_days_mean', 'loan_frequency', 'city_level_encoded', 'edu_level_encoded'] X = df[feature_cols] # 标准化 scaler = StandardScaler() X_scaled = scaler.fit_transform(X) # 层次聚类确认K值范围(抽样1万条) sample_df = X_scaled[:10000] agglom = AgglomerativeClustering(distance_threshold=0, n_clusters=None) agglom.fit(sample_df) # 画出不同K对应的轮廓系数 silhouette_scores = [] K_range = range(2, 11) for k in K_range: km = KMeans(n_clusters=k, n_init=10, random_state=42) labels = km.fit_predict(X_scaled) silhouette_scores.append(silhouette_score(X_scaled, labels)) plt.plot(K_range, silhouette_scores, marker='o') plt.xlabel('K') plt.ylabel('Silhouette Score') plt.show() # 正式聚类,K=5由轮廓系数和业务解释共同决定 kmeans = KMeans(n_clusters=5, n_init=10, random_state=42) df['cluster'] = kmeans.fit_predict(X_scaled) # 每个聚类的中心点(还原到原始尺度后看均值) cluster_profile = df.groupby('cluster')[feature_cols].mean() print(cluster_profile)

注意n_init=10要写。Sklearn的新版本里默认值已经变了,不指定它会弹警告。random_state也要固定住,不然每次跑出来的聚类结果标签编号会变动,后面做可视化的时候容易对不上颜色。

3.4 层次聚类的应用与树状图绘制

我在论文里展示了层次聚类的树状图,用来佐证K值选择的合理性。Sklearn自身不带画树状图的函数,是用的scipy.cluster.hierarchy:

from scipy.cluster.hierarchy import dendrogram, linkage # 对抽样数据用ward方法做层次聚类 Z = linkage(sample_df, method='ward') # 画树状图,只显示后30个合并点防止图像过密 plt.figure(figsize=(12, 8)) dendrogram(Z, truncate_mode='lastp', p=30) plt.axhline(y=某个阈值, color='red', linestyle='--') plt.show()

这个图的作用是让别人直观地看到“5簇”这个选择并非瞎拍脑袋。树状图中横线的高度就是簇间合并的代价,横线位置高、跨越大的地方就是天然的分群边界。

3.5 风险预警模型与评分映射

聚类完成之后,我接着做了风险预警。核心代码如下,这里先用逻辑回归做基线,再上XGBoost对比效果:

from sklearn.model_selection import train_test_split from sklearn.linear_model import LogisticRegression from xgboost import XGBClassifier from sklearn.metrics import classification_report, roc_auc_score # 构建标签:历史逾期天数均值>15 或 当前逾期标记=1 视为坏客户 df['is_bad'] = ((df['overdue_days_mean'] > 15) | (df['current_overdue'] == 1)).astype(int) features = feature_cols + ['cluster'] # 聚类结果作为特征参与建模 X = df[features] y = df['is_bad'] X_train, X_test, y_train, y_test = train_test_split( X, y, test_size=0.3, random_state=42, stratify=y ) # 逻辑回归基线 lr = LogisticRegression(max_iter=1000, random_state=42) lr.fit(X_train, y_train) y_pred_lr = lr.predict_proba(X_test)[:, 1] print('LR AUC:', roc_auc_score(y_test, y_pred_lr)) # XGBoost xgb = XGBClassifier(n_estimators=200, max_depth=4, learning_rate=0.1, scale_pos_weight=sum(y_train==0)/sum(y_train==1), random_state=42) xgb.fit(X_train, y_train) y_pred_xgb = xgb.predict_proba(X_test)[:, 1] print('XGB AUC:', roc_auc_score(y_test, y_pred_xgb)) # 输出风险评分(0-100) df['risk_score'] = (1 - y_pred_xgb) * 100 df['risk_level'] = pd.cut(df['risk_score'], bins=[0, 60, 75, 85, 100], labels=['高危', '中危', '低危', '优质'])

这里有一个处理经验:正负样本不均衡问题,用了scale_pos_weight参数给坏客户加权,把“把坏人漏掉”的代价放大。在答辩时表现出这一层的思考,明显能拉开档次。

3.6 可视化大屏:Flask + ECharts 实现

可视化是很多同学最担心的一环,其实非常简单。我用的是Flask提供后端API,ECharts渲染前端图表,整个大屏是一张HTML页面,顶部是标题,中间排列四个图表:

  • 左侧:K-Means聚类散点图(基于PCA降维到2维展示)
  • 右侧:各客群逾期率柱状图
  • 左下:贷款用途分布玫瑰图(饼图变形)
  • 右下:各客群风险定价区间散点图

Flask代码骨架:

from flask import Flask, jsonify, render_template import pandas as pd import json app = Flask(__name__) @app.route('/') def index(): return render_template('dashboard.html') @app.route('/api/cluster_data') def cluster_data(): # 读取聚类结果并做PCA降维 from sklearn.decomposition import PCA pca = PCA(n_components=2) df[['x', 'y']] = pca.fit_transform(X_scaled) data = df[['x', 'y', 'cluster', 'risk_level']].to_dict('records') return jsonify(data) @app.route('/api/risk_stats') def risk_stats(): # 按客群聚合逾期率 stats = df.groupby('cluster')['is_bad'].mean().reset_index() return jsonify(stats.to_dict('records')) if __name__ == '__main__': app.run(debug=True)

ECharts前端示例(散点图核心配置):

$.getJSON('/api/cluster_data', function(data) { var clusters = [...new Set(data.map(d => d.cluster))]; var series = clusters.map(c => ({ name: '客群' + c, type: 'scatter', data: data.filter(d => d.cluster === c).map(d => [d.x, d.y]) })); var chart = echarts.init(document.getElementById('scatterChart')); chart.setOption({ tooltip: {}, legend: { top: 10 }, xAxis: { name: 'PC1' }, yAxis: { name: 'PC2' }, series: series }); });

这一套做下来,大屏效果确实好看。答辩的时候PPT里放一张实拍截图,在纯粹讲算法的项目里非常亮眼。

3.7 贷款用途与风险定价的关联分析

只看聚类和风险还不够,我的延伸题目还做了“贷款用途与风险定价”的关联分析。

贷款用途字段以枚举形式出现(装修、教育、医疗、购物、旅游、经营周转),我用pd.get_dummies展开,然后按聚类群组做交叉频次统计。实际结论非常清晰:

  • D群(高风险长借群)的贷款用途里“经营周转”和“医疗”占比显著偏高
  • A群(白领短借群)的贷款用途集中在“购物”和“旅游”
  • C群(高收入低风险群)的定价利率可以下调1个百分点,风险性价比最优
  • D群的定价利率需要上调2-3个百分点,并且搭配借款额度上限限制

风险定价我用的是“基准利率 + 风险溢价 + 客群调整系数”这么个简化公式:

最终贷款利率 = 基准利率(8.0%) + 客群风险溢价 + 用途调整系数

客群风险溢价由模型的违约概率映射得到,用途调整系数根据历史坏账率经验设定。比如医疗用途的历史坏账率偏高,系数相应增加;教育用途虽然金额不大,但违约率低,可以给优惠。

4. 实操中的常见问题、避坑指南与心得

4.1 常见问题速查表

问题现象原因分析解决方案
K-Means聚类结果每次跑都不一样没有固定random_state固定随机种子,同时设置n_init=10
普通人也能看懂的聚类图很乱,看不出群簇用高维特征直接画图先用PCA降到2维,再用散点图展示
轮廓系数很高但业务解释很差特征选择只看了数值没看含义结合业务重新筛选特征,删除噪声特征
模型AUC高达0.98,答辩时被质疑标签泄漏(用到了未来信息或目标字段作特征)检查特征列表中是否有与标签直接相关的字段
前端图表数据量大,页面卡死一次性加载了全部样本点后端抽样返回,散点图只展示5000个点
数据处理后收入均值异常未处理离群点使用IQR或分位数截断后再训练
逻辑回归迭代次数警告数据未标准化或max_iter过小用StandardScaler处理后,max_iter=1000
程序中文乱码编码问题,CSV读取未指定编码pd.read_csv('file.csv', encoding='utf-8')

4.2 一个非常容易翻车的地方:标签泄漏

我必须单独提这个坑,因为我在中期检查时差点翻车。

做风险预警的时候,数据表里有一个字段叫“历史逾期天数平均值”,我拿它跟“是否违约”标签同时建模,AUC直接到了0.99。答辩老师看了一眼就说:“你这个特征本身不就是标签吗?历史逾期天数均值本质定义了违约,再拿它去预测违约,这是自问自答。”

解决的方法是:把标签定义用到的字段从特征列表里剔除掉。比如标签是“逾期天数>15”,那建模时就不能用任何直接从逾期天数派生出来的字段。可以用历史借款次数、借款间隔、消费波动率等间接特征,而不是逾期情况本身。

我在论文里特意加了一节“特征合规性审查”,列出哪些特征可以进入模型、哪些必须排除,这个细节在答辩时非常加分,因为你提前堵住了老师想挑刺的嘴。

4.3 可视化大屏的实用经验

ECharts虽然用起来容易,但有几个实际经验值得记下来:

散点图会过度绘制。几千个点叠在一起就是一团黑,要限制样本量。我的做法是每个客群随机抽500个点,总计2500个点,视觉上既能看清分布又不卡页面,论文中再标注说明“展示部分样本”。

大屏配色建议统一。用深色背景(#0a0e27)加亮色系列(橙、蓝、绿),是数据大屏最稳的方案。不要用默认白色背景,会很“毕设”味。

玫瑰图的实现可以更直观地表达贷款用途结构。ECharts中把饼图的roseType设置为'radius',就自动变成玫瑰图。建议配合客群切换联动:点击某个客群,下方玫瑰图只显示这个群体的贷款用途分布。

4.4 关于源码和复现的说明

我做毕设时也网上找过透源码和报告,结果大部分不是残缺版就是需付费的引流项目,或者数据不匹配、装上跑不起来。后来索性自己动手从数据构造开始写,反而把整个流程吃透了。所以我的建议是:

参考源码可以,但必须照着数据把每一步跑通。如果你拿到了别人的项目,先确认三件事——数据能不能对齐,依赖包版本合不合适,字段命名是否一致。如果不一致,照着我的流程重新生成模拟数据即可,算法代码核心部分是通用的。

算法层面不需要贪多。本科毕设用K-Means+层次聚类+逻辑回归+XGBoost+ECharts,已经是完整且扎实的组合。在这个基础上把每个环节做深做透,比堆十几个算法效果更好。

5. 从项目延伸到能力沉淀

做完整套项目之后,最大的收获倒不是代码能力,而是把“数据挖掘”四个字从课本变成了一种解题习惯。

第一,你对数据的感觉会变。看到一张新表,第一反应不是“跑个模型”,而是先看分布、看缺失、看相关性,先想业务逻辑合不合理。

第二,你对无监督学习的认知会更务实。K-Means不是调个库,而是要理解不同人群的边界在哪里,特征维度怎么取舍才能让分群有意义。聚类最终服务于决策,而不是服务于美观的分簇图。

第三,你会对模型评估多一层敬畏。AUC、准确率高只代表你在这个数据集上拟合得好,不代表放到真实环境里一定有用。风控模型尤其如此,真实业务中模型还会面临客户偏移、数据质量波动、政策调整等复杂影响。

这个项目如果要继续扩展,我建议顺着两条线:一条是线上部署,把训练好的模型导出成PMML文件或pickle,用Flask搭一个简单的评分服务接口;另一条是引入时序,观察客户过去几个月的消费和还款趋势,把静态风险升级为动态风险。

我给学弟学妹最诚恳的建议是:选题选得再复杂,不如一个环节做得扎实。多维特征融合和可视化这两条线,是我做下来性价比最高、答辩效果最好的部分。如果你们时间紧,优先保数据质量+聚类效果+可视化展示这三块,风险模型可以适当简化,绝对不影响你的毕设评级。

纸上得来终觉浅,从我的实战经历来看,真正把整个流程跑通一遍,任何人的技术成长都会非常明显。祝你们毕设顺利,答辩顺利。

RELATED READING

延伸阅读

更多一线实战笔记与深度复盘,助您持续精进