ARTICLE · INTELLIGENCE

战地情报 · 详情页

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

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

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

这两天帮一位27届的学弟整理计算机毕设源码,题目正好是《基于多维特征融合的互联网贷款客户聚类与风险预警研究》和配套的《基于数据挖掘的互联网贷款用途与风险定价可视化研究》。乍一看像是两个题目,实际是一个完整的风控数据挖掘闭环:先把客户按照多维特征聚成不同群体,再做风险预警,最后把贷款用途、利率、逾期率之间的关系用可视化大屏呈现出来。拿到这类题目容易发懵,范围太大,不知道该先做什么。我把从数据清洗到出具整套源码的全过程拆一遍,包括选型思路、参数调优、踩过的坑,尽量把能直接复现的细节都交代清楚。

做过金融风控类毕设的人都知道,这类项目最容易被卡住的不是模型,而是“业务逻辑与数据特征对不对得上”。比如为什么要把聚类和风险预警放一起,为什么贷款用途要和风险定价关联。先把这个题目的设计思路讲明白,后续每一步才走得顺。

1. 项目整体设计与拆解

1.1 题目到底在考什么

“多维特征融合”这个词听起来很高端,其实就是把客户画像里分散的信息整合起来,形成一个能同时反映还款能力、还款意愿、资金需求紧急度的特征集。传统做法可能只盯着收入、负债或者历史逾期某一个字段,效果往往不稳定。一个用户可能在单一维度上表现正常,但把收入负债比、近3个月征信查询次数、当前逾期天数放在一起看,风险特征立刻暴露出来。

客户聚类解决的是“客群分层”问题。不同客群的风险驱动因素不一样,比如刚毕业的学生群体负债低但收入稳定性差,经营类客群收入高但资金周转波动大,把这些人混在一起训练模型,预警准头会大打折扣。所以题目先要求聚类,再要求预警,本质上是希望用无监督方法先找出结构性差异,再针对不同分群搭建有监督的风险判断。

风险定价可视化的核心逻辑也很简单:风险越高的客户,贷款利率理应越高。但这还不够,还需要回答“贷款用途对违约风险有没有显著影响”这个问题。买房装修、日常消费、生意周转、教育医疗,不同用途背后的资金去向和真实还款来源不同,定价策略也应该有差异。把这个差异通过散点图、漏斗图、热力图展示出来,就是可视化研究的价值。

1.2 整体技术架构与代码组织

我的技术选型以Python为主,原因很简单:数据挖掘生态最成熟,sklearn直接提供聚类和分类模型,pandas处理表格数据非常顺手,可视化用ECharts做前端展示,后端用Flask暴露接口。整份源码分成四个模块:

  • data_process/:原始数据导入、缺失值处理、异常值截断、特征工程,最终输出标准化后的建模宽表。
  • cluster_model/:PCA降维、K-Means聚类、层次聚类辅助验证、轮廓系数评估、分群画像输出。
  • risk_model/:预警标签构造、训练集/测试集划分、逻辑回归与随机森林对比、AUC与KS指标计算。
  • visualization/:Flask后端接口,ECharts前端大屏,包括聚类散点图、风险漏斗图、用途利率散点图。

这个组织方式不是随便拍的,而是按照“数据 -> 人群 -> 风险 -> 表达”的顺序来安排。每个模块的产物会成为下一个模块的输入。比如聚类结果作为一个新字段喂给风控模型,风控模型的输出结果再作为可视化大屏的数据源。这样各模块之间耦合度低,论文里也容易画出层次清晰的架构图。

选择Flask而不直接用Django,是因为毕设场景下不需要用户登录、后台管理这些偏工程的能力,Flask轻量、接口逻辑简单,前端用原生HTML+ECharts就能跑。如果你已经装了Anaconda,直接跑pip install flask pandas scikit-learn matplotlib就能把环境凑齐,不需要额外折腾数据库。数据量在十万级以下时,CSV文件配合pandas足够支撑整个项目了。

2. 数据预处理与多维特征融合

2.1 数据来源与核心字段梳理

这个题目没有公开的标准数据集,需要自己构造或合拼。常见的做法是参考Lending Club的公开字段结构,自己构造一份包含客户基本信息、历史行为、贷款字段的模拟数据。字段设计要贴合国内互联网贷款的业务习惯,至少要覆盖下面几类:

特征类别代表字段用途说明
客户基本盘年龄、性别、收入、学历、职业类型刻画还款能力的基础信息
负债与需求负债总额、贷款金额、贷款期限、申请用途判断资金压力与资金去向
历史行为征信查询次数、历史逾期次数、当前逾期天数、历史贷款笔数判断还款意愿与信用历史
行为融合特征收入负债比、近6个月平均查询次数、额度使用率、收入稳定性多维特征融合后的核心变量

我在做数据准备时特别注意“贷款用途”字段,题目里专门提到用途与风险定价可视化,所以这个字段必须保留,而且不能只做成哑变量,还要在后续聚类和预警里作为分群的特征之一。用途一般包含:经营周转、日常消费、装修、教育、医疗、购车、旅游等,建议统一编码成类别,再结合数值特征做Embedding式融合。

如果你用的是自己爬来的数据,格式肯定很乱。清洗的第一步是把所有字段统一命名、统一单位,金额统一换算为元,时间字段统一为YYYY-MM-DD。第二步是检查取值范围,比如年龄不可能超过100岁,贷款金额不能为负数,征信查询次数必须是整数,这些约束都要写进清洗函数里,而不是靠肉眼核对。

2.2 清洗、标准化与特征构造

数据清洗这一块最容易出问题是缺失值比例高的时候。贷款数据里收入字段经常缺失,直接删除会损失大量样本。我的做法是:先用缺失比例排序,低于5%的字段用中位数填充,高于30%的字段直接丢弃,中间的字段用“是否缺失”作为一个二值特征加入模型。这样既保住了样本量,又把缺失信息本身变成了特征。

异常值不能只靠describe()看一眼就完事。收入字段的99分位可能高达几十万,直接参与聚类会让K-Means的重心计算被极值带偏。稳妥的截断方法是取1%到99%分位,小于下分位或大于上分位的值统一压到边界。实际操作里可以这样写:

def winsorize_series(s, lower=0.01, upper=0.99): q_low = s.quantile(lower) q_high = s.quantile(upper) return s.clip(q_low, q_high)

特征工程是提高模型效果最划算的一步。原始字段只能提供单点信息,真正的风险信号往往藏在比值、频次、趋势里。我做过的项目中,下面这几个手工衍生特征对聚类和预警的帮助最大:

  • debt_to_income = 总负债 / 年收入:直接衡量还款压力,数值越高风险越大。
  • recent_query_6m = 近6个月征信查询次数:查询次数暴增通常代表多头借贷。
  • overdue_ratio = 历史逾期次数 / 历史贷款笔数:衡量用户还款习惯的稳定性。
  • usage_rate = 贷款金额 / 信用卡额度:代表资金紧张程度。
  • income_stability = 近6个月收入标准差 / 平均收入:收入波动越大,越可能断供。

这些衍生特征加进去之后,你会明显感觉到客户之间的差异性被拉出来了。后面K-Means分出来的人群不再只是“年龄大的人”和“年龄小的人”,而是“高负债、高查询、经营周转型”和“低负债、稳定收入、日常消费型”,业务解释性强得多。

2.3 多维特征融合策略

特征融合在工程上的常见做法不是把变量随便拼在一起,而是区分不同类型采用不同策略。数值特征用标准化,类别特征用独热编码或目标编码,时间特征拆成年、月、是否工作日等周期项,再把这些处理后的特征一起拼成宽表。拼接之后通常会有几十维,如果直接拿去聚类,稀疏性和噪声会干扰距离计算。

这时就需要降维。最常用的方案是PCA,但PCA本身是无监督方法,它只保留方差最大的方向,不一定对业务解释友好。我会同时保留两个层面的结果:聚类散点图用PCA的前两个主成分做展示,建模数据用用标准化后的全量特征,不强行压到极低维度。这样既照顾了可视化需求,又不损失建模信息。

真正意义上的多维融合,我还会做一步“特征选择”。通过随机森林的特征重要性排序,把重要性低于阈值的字段剔除,只保留对风险区分度最高的二十个左右特征。这样做的好处是防止聚类被无关变量带偏,比如“客户姓名长度”这种字段如果存在,一定会干扰距离计算,但它和风险没有关系。把所有特征无脑拼进去,是新手最常见的错误。

3. 客户聚类:从K值选择到分群画像

3.1 为什么先聚类再做风险预警

一个很容易被忽视的问题是:既然我们已经有监督模型可以做预警,为什么还要先花力气聚类?我的理解是,互联网贷款客户天生就是异质的,不同人群的风险驱动因素完全不同。单一模型用一个全局阈值判断所有客户,会牺牲不同群体的差异。

举个例子:一个年收入50万的个体户,偶尔逾期5天,但贷款用途是备货周转,还款来源清晰;一个刚入职月收入6000元的白领,从未逾期,但贷款用途是分期买手机,负债率已经超过60%。如果用全局模型打分,很可能个体户被标记为风险更高,但实际爆雷概率也许低于那个负债已经到顶的白领。聚类能帮我们先识别出这种结构性差异,再在每个客群内部分别评估风险。

同时,聚类结果本身就是很好的业务标签。银行和消金公司非常看重客群分层,不同层级用不同营销策略、不同利率、不同额度。毕设里能把这层意思讲透,评委通常会觉得你对业务有理解,而不是只会调包。

3.2 K-Means与层次聚类的实践

聚类算法我用的是K-Means主方案,原因是它在样本量几万到几十万时效率高,结果也相对好解释。但K-Means对初始中心敏感,对离群点敏感,所以前面提到的异常值截断和标准化必须做干净。为了避免随机种子带来的不稳定性,我固定了random_state=42,并且跑了多组K值取轮廓系数的平均值再选K。

K值的选择我同时用了两种方法。第一种是肘部法则:画出簇内误差平方和SSE随K值变化的曲线,找拐点;第二种是轮廓系数,直接计算不同K下的平均轮廓系数,选最高的K。两者结论不一致时,优先参考轮廓系数,因为SSE曲线很多时候没有明显拐点。

from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score scores = [] for k in range(2, 9): model = KMeans(n_clusters=k, random_state=42, n_init=10) labels = model.fit_predict(X_scaled) scores.append(silhouette_score(X_scaled, labels)) best_k = range(2, 9)[scores.index(max(scores))]

除了K-Means,我还会跑一次层次聚类作为交叉验证。层次聚类可以画出树状图,直观看出每个簇的分裂过程,特别适合在论文里展示聚类逻辑。但层次聚类的计算复杂度偏高,样本超过五万就慢得难受,所以只用它在一个小规模抽样子集上验证。两个算法得出的簇数一致时,结论会更有说服力。

聚类散点图一定是用PCA降维后画的,不然高维空间只能看两两组合,无法呈现群体边界。实操中可以先PCA降到二维,再以散点图着色展示聚类结果。每个簇的中心点可以用红色X标记,这样即使客户样本点重叠严重,也能看出分群重心的大致位置。

3.3 聚类结果画像与业务解读

聚类完成后,我习惯做一张“分群画像表”,把每个簇在各个特征上的均值列出来,然后根据均值特征给簇起一个有业务含义的名字。这一步很关键,论文里的分析深度全靠这里体现。

比如做出来的结果可能是这样:

分群收入均值负债率均值近6月查询次数逾期率主要用途业务标签
01.2万18%2.10.8%装修、购车优质稳健型
1800055%6.84.2%日常消费、教育资金紧张型
22.5万65%9.37.5%经营周转高杠杆经营型

“优质稳健型”客户收入不低、负债不高,逾期率极低,适合推低息大额产品;“资金紧张型”客户收入偏低但查询次数多,存在多头借贷迹象,应该控制额度并提高利率;“高杠杆经营型”客户收入高但负债率也高,风险缺口在于现金流波动,需要结合经营流水数据做二次核验。

这些画像结论看起来不复杂,但需要你真的去统计均值并观察差异。我通常会把每个簇的特征均值做成横向柱状对比图,再用雷达图展示三四个核心特征的相对水平。这些图放在毕设里比一堆数字直观得多,评委扫一眼就知道聚类是有效的。

4. 风险预警与风险定价模型

4.1 预警标签怎么打

风险预警属于有监督学习,第一步是确定“什么是坏用户”。公开数据不会直接给你一个违约标签,所以需要自己根据业务含义定义。我的定义规则是:当前逾期天数>30天标记为高风险;同时结合未来三个月的还款表现,如果用户后续逾期超过60天,也标记为坏样本。

这句话听起来简单,但要注意标签泄漏。如果直接用“当前是否逾期”作为标签,再把“当前逾期天数”作为特征,模型会学到一个恒等映射,看起来AUC接近1.0,实际毫无意义。正确做法是删除“当前逾期天数”这个字段,只保留历史行为特征,比如过去12个月的逾期次数、查询次数、负债变化等。

对于没有未来还款表现的数据,也可以用无监督的异常检测思路来辅助打标签。把异常分数排名前10%的用户视为潜在风险客户,再结合规则标签获得一个半监督训练集。但毕设场景下,我更推荐直接构造模拟坏客户:在正常客户中随机注入高逾期特征,再标记为高风险。这样模型学的是特征之间的组合关系,而不是某一列的复制粘贴。

4.2 预警模型的选择与训练

我同时跑了逻辑回归和随机森林,原因是两者各有优势,论文里可以做对比。逻辑回归是风控场景的基准模型,系数正负和大小能直接解释为“特征每增加一个单位,风险对数几率的变化”,可解释性强;随机森林能自动捕捉非线性关系,AUC通常更高,但黑盒感强。

训练时要特别注意类别不平衡。正常情况下好客户可能占90%以上,坏客户不到10%。如果直接训练,模型会倾向于把所有样本预测为好客户,虽然准确率很高,AUC却并不一定差,但坏客户一个也捞不上来。解决思路有三个:一是用class_weight='balanced'让模型自动调整类别权重,二是对训练集做SMOTE过采样,三是预测时把阈值从0.5下调到0.3或0.2,按业务需求在召回率与精确率之间找平衡。

评估指标不要只看准确率,风控场景看AUC、KS、混淆矩阵。KS值能刻画模型区分好坏样本的能力,大于0.3就基本可用了;AUC和KS一起看,比单看准确率靠谱得多。额外多说一句,做特征重要性分析时,随机森林能直接输出Gini importance,逻辑回归可以用系数绝对值排序;如果两边的top特征高度重合,那说明特征工程做得很扎实。

4.3 风险定价与贷款用途的关系

风险定价这块很多人不知道怎么做量化。我的处理方式是把预警概率映射为风险溢价,再与利率建立线性关系。假设基准资金成本是5%,客户的违约概率是p,预期损失率约等于p乘以一个固定损失回收系数,那么风险定价利率可以近似为:

base_rate = 0.05 loss_given_default = 0.7 pred_prob = model.predict_proba(X_test)[:, 1] risk_premium = pred_prob * loss_given_default loan_rate = base_rate + risk_premium + operation_cost

这里面的loss_given_default设为0.7,表示一旦违约,本金损失七成,这个数字可以按业务场景调整。最终算出的利率会自然地按预警概率高低排序,高风险客户利率更高。为了可视化,我们用贷款用途分组,统计每组客户的平均预警概率、平均利率、实际逾期率,然后画散点图和柱状图。

从业务角度看,经营周转类客户的金额通常较大、期限较短,但利率跨度也比较大;日常消费类客户金额小、利率相对稳定。如果发现某个用途组的实际逾期率和模型预测概率差距过大,就说明该组客户的还款行为特殊,需要单独调整定价策略。这种“用途-利率-逾期率”的三角关系,正好是题目副标题要求研究的内容。

5. 可视化大屏与数据呈现

5.1 可视化指标框架与页面结构

可视化部分不是随便扔几张统计图就完事,而是要有清晰的看板逻辑。我的大屏分四个核心区域:顶部放整体业务概览,包括客户总数、贷款总额、整体风险等级占比、平均利率;中间区域放聚类结果散点图和每个簇的用户画像指标;右侧放风险预警概览,包括高风险客户占比、风险等级漏斗图、逾期率走势;底部区域放贷款用途与利率、逾期率的关系图。

页面结构上,我参考了现在的企业级数据可视化大屏风格,以深色科技风为主,用ECharts自带的深色主题,配合红黄绿三色做风险等级提示。背景用深蓝灰色,卡片标题统一左上角,基础单位统一显示,避免图表数据量纲不同导致误读。也可以用现有的大屏框架类项目作为模板,但自己写HTML布局其实不复杂,四张卡片加四个图表容器就够了。

一屏放不下所有内容,就采用“总览筛选+同页切换”的方式。顶部放一个贷款用途的下拉筛选器,选择不同用途时,下面的聚类散点图、风险漏斗、利率分布全部联动刷新。交互式分析能体现你做了前端联动方案,在评审时也是加分项。

5.2 用ECharts实现核心图表

ECharts的图形渲染质量高,社区资料多,出图效果非常适合毕设。我在项目里用了三种核心图:PCA聚类散点图、风险等级漏斗图、用途-利率-逾期率散点图。

聚类散点图的实现逻辑是把后端返回的每个样本的x、y、cluster三个字段拿出来,遍历成ECharts的scatter数据。每个簇单独一个series,颜色用调好的色板区分,symbolSize按样本量做缩放,避免太多点重叠成一团糊。这样的图放在大屏中间,一眼就能看出人群分裂的边界。

用途-利率-逾期率散点图更讲究一点,横轴是平均利率,纵轴是实际逾期率,点的大小代表贷款金额,颜色代表用途类型。这张图最直观地回应了“风险定价可视化研究”这个题目:当看到某一个用途的点在右上方,说明利率高、逾期率也高,定价是合理的;如果某个用途的点在左上方,说明利率不高但逾期率不低,就存在定价偏低的嫌疑。

前端调用后端接口时,我的Flask路由统一返回jsonify({'status': 200, 'data': list_dict}),前端用fetch拉数据后直接setOption。需要注意ECharts实例要在数据返回后再初始化,否则容器宽度还没稳定,很容易出现图表发虚的毛病。

5.3 大屏的交互与部署注意

交互设计方面,我会给每个图表加上tooltip提示框、legend图例、dataZoom缩放条。尤其聚类散点图样本量大,不带缩放条的话局部细节完全看不清楚。筛选器不只是装饰,要真的绑定到后端的查询参数上,比如选择“经营周转”后,接口返回的数据全部按该用途过滤,前端图表刷新。

部署时我把Flask服务跑在本地9100端口,大屏页面通过相对路径访问,避免跨域问题。如果你要把项目打包交给老师,不要给MySQL依赖,最好把处理好的数据缓存在pickle或JSON文件里,前端直接读静态JSON。这样免去老师配数据库的麻烦,项目打开即用。

数据口径的一致性是可视化最容易翻车的地方。很多人在后端写统计SQL时只写了客户总数,到前端图表又是另一个总数,两张图对不上,答辩时会被一眼看穿。我的经验是全局只保留一份汇总好的JSON数据,所有图表都从这份数据里取值,不再各自计算。

6. 实操过程遇到的高频坑

6.1 标签泄漏:预警模型看起来效果爆表的原因

我一开始直接把“当前逾期天数”当作特征保留了,结果逻辑回归AUC直接冲到0.97,心里还挺高兴。后来用特征重要性一看,排名第一的特征就是当前逾期天数,这明显不对。因为“当前逾期天数”和标签的定义本就是一回事,模型根本不需要学习,直接记住这个字段就能预测。

解决办法是把标签相关的字段从特征里全部剔除,包括当前逾期天数、是否逾期这些直接变量。保留的只能是与历史行为有关的时间滞后特征,比如过去6个月内的逾期次数、过去12个月的最高逾期天数。我反复提醒自己:凡是“到了今天才能知道”的信息,都不能用来预测“今天是否会发生风险”。

6.2 K-Means结果不稳定,换了几次数据都不一样

K-Means对初始中心敏感,换一套数据或者换一个随机种子,聚类结果可能面目全非。哪怕逻辑上没有错,答辩时老师让你复现一遍,结果对不上就很尴尬。我做了两件事:第一,固定所有随机种子,包括train_test_split、KMeans、模型初始化全部传random_state=42;第二,增加n_init参数,让KMeans跑十次取最优结果。再加上前面说的异常值截断和标准化,聚类结果基本稳定。

如果样本量大,还可以对数据做mini-batch K-Means,但毕设数据量一般不大,直接跑标准版就行,层次聚类验证也够用了。

6.3 类别不平衡让预警模型变成“全好客户分类器”

这个问题在训练初期折磨了我很久。当时模型预测的结果几乎全是“好客户”,因为好客户占了92%,哪怕全部预测为好,准确率也有92%。但风险预警完全失效。后面我改用class_weight='balanced',并把决策阈值下调,才让坏客户召回率从不到10%升到65%左右。阈值调节时需要同时看混淆矩阵,不能只看AUC。AUC只反映排序能力,阈值决定实际判断结果,这两个不是一回事。

我还试过用SMOTE过采样,效果也不错,但对高维稀疏数据容易生成一些离群样本。偶尔遇到数据和模型没问题的场景,加上SMOTE反而不如调阈值稳。所以我现在默认先用class_weight,再调阈值,过采样只是备选方案。

6.4 可视化大屏图表错位和数据对不上

图表容器在小屏端容易挤压变形,我是在setOption之前等容器完成布局,并且在初始化时给每个图表设置固定宽高百分比,不写死像素。数据对不上的问题,核心是统一数据源。我的方案是后端只生成一份dashboard_data.json,前端四个图表引用同一个对象,个别图表需要聚合数据就在前端临时计算,不做两次后端查询。

还有一个低级的坑:ECharts的formatter里拼接变量时,如果数值是None或者NaN,整个tooltip会显示undefined。我一度以为自己数据有问题,排查半天才发现是字符串模板里没做类型转换。所有输出数据在返给前端前,我都强制round(..., 2)或str(),避免这类玄学报错。

7. 写在最后的一点建议

这套源码做下来,我最大的体会是:数据分析类的毕设,重点不全在模型调参,而在于能不能把每一步决策背后的业务逻辑说通。为什么选K-Means而不是DBSCAN,为什么把贷款用途放进聚类特征,为什么风险定价要由预警概率驱动,这些问题比AUC多0.03更能体现你的工作量和理解深度。

源码交付时,我习惯把环境依赖写在一个requirements.txt里,附上一份“快速运行说明”,从数据生成到启动Flask服务,三步跑通。任何工具链都经不起折腾,能一键演示的项目在评审时天然占优势。

最后再分享一个小技巧:如果你的时间仓促,聚类部分只保留K-Means+轮廓系数,预警部分只保留逻辑回归+阈值调节,可视化只保留聚类散点图和风险漏斗图,也能形成一个完整闭环。演示时按“数据预处理 -> 分群 -> 预警 -> 定价 -> 可视化”的顺序讲,既有逻辑又有视觉冲击,比盲目堆模型靠谱得多。

RELATED READING

延伸阅读

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