资讯动态

银行客户细分可视化:K-Means聚类与PCA降维的工程实践

发布时间:2026/10/13 18:06:12 来源:尧图企业网站定制
简介一份面向银行客户细分场景的机器学习可视化项目适合金融数据分析初学者、高校相关专业学生以及准备入门机器学习的读者。资源基于德国信用数据集通过Python脚本完成数据清洗、特征编码、标准化、KMeans聚类与结果可视化可帮助理解如何按收入水平、信用历史、贷款金额等维度划分客户群体并利用散点图、热力图等图表直观呈现不同客群特征。压缩包共2个文件包含可直接运行的py源代码和配套csv数据集整体仅17KB体积小巧、依赖简单便于在本地快速复现完整分析流程。目前已有68人学习该资源。文件中还提供肘部法则选择聚类数等关键步骤可作为客户画像、精准营销和信用风险评估等业务场景的参考模板适合边做边学、二次改造。1. 银行客户细分可视化分群结果不画出来业务方根本没法用银行零售部或信用卡中心手里握着几十万甚至上百万客户数据存量经营的第一步就是把客户分开谁是高价值客群、谁是沉睡户、谁适合推理财、谁适合做交叉销售。机器学习里的聚类算法能完成这个粗分但聚类输出的只是一列「群编号」——业务经理不会去看一个编号他们要的是「这群人长什么样、有多少人、值不值得运营」。银行客户细分可视化做的就是把这套聚类结果翻译成散点图、雷达图、热力图最终落到一张业务能直接读的可视化大屏上。这篇文章面向的是刚接触机器学习的银行数据分析师和做数据报表的工程师按「选型→建模→绘图→排坑」的顺序把整套流程讲透。2. 先定算法再谈可视化为什么客户细分绕不开 K-Means 和 PCA 降维2.1 客户细分的目标是「分得开」还是「讲得清」先回答这两个问题再选算法做银行客户细分前我会先问业务方两个问题分出来的群是要做差异化运营还是要做风险分层这决定了算法选型。银行场景里最常见的做法是用 K-Means 做聚类原因很朴素它快、稳定、每个群能算出一个「平均画像」。K-Means 的目标函数是让簇内样本到质心的距离平方和最小聚类结果天然满足「同类相近、异类相远」这让后续画雷达图、对比各群特征均值变得非常自然。和层次聚类、GMM、DBSCAN 对比一下就有结论了。层次聚类要算全样本的距离矩阵百万级客户直接内存爆炸GMM 能给出概率归属但训练慢、参数多业务方听到「高斯混合」四个字就已经失去耐心DBSCAN 在银行数据上最容易翻车——客户行为的密度分布极不均匀普通客户的点挤在一起高价值客户稀稀拉拉DBSCAN 会把大量普通客户判成噪声点而银行要的是「每个客户都能落到某个群」不是把客户扔掉。所以 K-Means 不一定是最优算法但它是银行客户细分里最容易向业务解释、最容易落地、也最容易可视化的一种。聚类前有一个前置步骤不能省特征标准化。K-Means 基于欧氏距离计算相似度如果直接把「年龄」和「账户余额」放进模型余额的量纲会彻底压过年龄。余额从几千到几百万年龄只有 20 到 70距离计算里余额贡献了绝大部分方差聚类结果基本等于「按余额分段」。这个问题我会在后面的避坑章节再展开但你现在就要记住标准化不是可选步骤是必需步骤。2.2 高维聚类结果画不出来PCA 降维不是炫技而是刚需聚类模型通常在 815 个特征上训练但人眼只能直接看二维平面最多加一个颜色维度或大小维度看三维。要在图上呈现「客户被分成了几群」必须把高维特征投影到二维或三维PCA主成分分析就是做这件事的。PCA 做的事情是找到一组新的正交坐标轴让数据在新坐标轴上的方差依次递减第一个主成分捕获最大的信息量第二个捕获剩余信息里最大的部分。有人说 PCA 是「黑匣子」这个说法不准确。PCA 的每个主成分是原始特征的线性组合比如第一主成分可能是「0.7×余额 0.4×持有产品数 - 0.3×年龄」只要打印出主成分的权重就能解释这个坐标轴在说什么。相比之下t-SNE 和 UMAP 虽然画出来的散点图「族群感」更强点被拉开得更明显但它们的坐标没有任何业务含义参数perplexity、n_neighbors调一调图就变一个样不适合用在银行项目的正式交付里。我的常见做法是用 PCA 做主图如果两个群在投影图上重叠严重、肉眼无法区分再用 UMAP 做辅助探索图自己判断但给业务方看的永远是 PCA。还要提醒一个易混淆点PCA 降维是给「可视化」用的不是给「聚类」用的。有人先把 15 维特征 PCA 到 2 维再跑 K-Means然后抱怨分群效果差——这等于把剩余 13 个维度的信息全扔了再聚类能不差吗正确流程是先对标准化后的全量特征做 K-Means拿到聚类标签后再用 PCA 投影到二维平面上给散点图上色。2.3 可视化工具选型Matplotlib 只配自己看ECharts 才是交付级做数据分析和可视化的工程师对 Matplotlib 都不陌生但你要问自己一个问题这张图是给谁看的如果是自己在 Jupyter 里探索数据分布Matplotlib 够用如果是要放到周报里、塞进银行的可视化大屏、让业务经理自己拖拽筛选Matplotlib 就远远不够了。静态 PNG 图没法悬停看具体客户信息没法缩放看局部密度更没法做联动筛选这些都是银行客户细分可视化里的刚需。我一般用的组合是 Python 端做聚类和数据处理用 PyECharts 生成交互图表最后拼成一张可视化大屏。PyECharts 是 ECharts 的 Python 封装好处是你在 Python 里用字典和列表组织数据它帮你生成一个 HTML 页面里面自带缩放、悬停提示、图例筛选这些交互能力。对比同类的 Plotly、BokehPyECharts 和中国大陆的开发者更熟文档全、模板多遇到「百度可视化大屏」这种展示需求ECharts 生态几乎是默认答案。下一章我就用 PyECharts 走完整个流程。3. 从原始客户表到聚类标签一套可直接复现的银行客户分群代码3.1 先看清字段再谈建模银行客户表常见字段与特征选型拿到一张银行客户表第一步不是写聚类代码而是先做字段盘点。常见做法是把字段分成三类标识字段customer_id、姓名、手机号、特征字段年龄、余额、信用分、持有产品数、开卡时长、标签字段如果是存量运营可能还有历史响应记录但客户细分本身是无监督的不需要标签。标识字段必须剔除它们对聚类没有任何意义反而会拉大距离。特征字段类型说明age连续年龄注意缺失值和极端值balance连续账户余额量纲很大必须标准化credit_score连续信用评分量纲中等num_products连续持有产品数通常 18has_credit_card二值0/1是否持有信用卡is_active二值0/1近 6 个月是否有动账tenure_months连续开户时长月occupation_category类别职业类型需要编码处理这里有一个选型原则能用连续变量表达的就不转成类别变量。「职业类型」这类字段在银行数据里通常是几大类如果直接做 One-Hot 编码会在后文引入一个很隐蔽的陷阱我在第 3.2 节会讲处理办法。先读数据看结构和缺失情况import pandas as pd df pd.read_csv(bank_customers.csv, encodingutf-8) print(df.shape) print(df.info()) print(df.describe(percentiles[0.25, 0.5, 0.75, 0.9]))df.shape先确认行数银行客户表动辄几十万行如果数据超过内存就需要考虑抽样。df.info()看每一列的非空值和数据类型这一步能快速暴露两个问题某个字段填空太多或者导入后字段类型变成 object通常是日期字段被读成了字符串。df.describe()看量纲差异正常情况下 balance 的标准差一定是 age 的几十倍这就是后面要标准化的现实依据。3.2 特征标准化与哑变量处理让 K-Means 不欺负连续性变量标准化用 StandardScaler它把每列变成均值 0、标准差 1。对连续变量这是标准操作但对 One-Hot 编码产生的哑变量这里有个常见分歧0/1 哑变量到底要不要一起标准化我的做法是——同一变量拆分出来的哑变量不参与标准化没有拆分的二值字段has_credit_card、is_active也不标准化。因为 0/1 字段本身量纲固定标准化之后反而会变成一个「原始占比」相关的连续值可解释性变差。至于职业类型这种多类别字段我一般不会直接 One-Hot 后塞进聚类模型。原因很现实如果职业有 5 类One-Hot 会产生 5 列这 5 列在 15 维特征空间里的权重被放大聚类结果可能会从「按客户行为分群」变成「按职业分群」。职业标签应该在分群后的画像里看而不是作为聚类输入。处理办法是把高基数的类别字段做「目标编码」或「频次编码」更稳妥的做法是先不用它聚类等到分群画像阶段再展示各群职业分布这样既保留了信息的可解释性又避免污染距离计算from sklearn.preprocessing import StandardScaler # 参与聚类的特征列 feature_cols [age, balance, credit_score, num_products, has_credit_card, is_active, tenure_months] # 分两个步骤先拷贝一份只含特征的数据 X df[feature_cols].copy() # 缺失值处理银行数据一般用中位数填充避免均值被极端值拉偏 X X.fillna(X.median()) # 标准化只对连续列做 scaler StandardScaler() cont_cols [age, balance, credit_score, num_products, tenure_months] X[cont_cols] scaler.fit_transform(X[cont_cols]) print(X.head())fillna(X.median())是我处理银行数据的默认选项。银行数据里 balance 长尾很重个别千万级余额的客户会把均值拉高用中位数填充缺失值更稳。标准化完成后可以再打印一次X.describe()连续列的均值会接近 0标准差接近 1这时 K-Means 的距离计算才不会被单个字段绑架。3.3 用「手肘 轮廓系数」双指标定 K而不是拍脑袋K 值是 K-Means 唯一的「超参数」也是业务方最喜欢追着问的数字为什么是 4 群而不是 5 群我不能回答「拍的」所以要给出两个佐证肘部法则ELBOW和轮廓系数Silhouette Score。肘部法则看簇内平方和WSS随 K 增大而下降的曲线下陡变缓的拐点就是合适的 K。轮廓系数算每个样本与自身簇内样本的平均距离、与最近其他簇样本的平均距离取值范围 [-1, 1]接近 1 表示簇内紧凑且簇间分离。实操里有个经验之谈银行客户行为数据噪声很大轮廓系数普遍偏低一般 0.250.35 就算「能用的分群」不要纠结于论文里说的 0.7 以上。看下面这段代码import numpy as np from sklearn.cluster import KMeans from sklearn.metrics import silhouette_score K_range range(2, 9) wss_list [] sil_list [] for k in K_range: model KMeans(n_clustersk, random_state42, n_init10) labels model.fit_predict(X) wss_list.append(model.inertia_) sil_list.append(silhouette_score(X, labels)) # 打印出每个 K 的指标等一下画图手动判读 for k, wss, sil in zip(K_range, wss_list, sil_list): print(fK{k}, WSS{wss:.1f}, Silhouette{sil:.4f})这里有两个参数值得说明。n_init10是 KMeans 的多次初始化次数K-Means 的初始质心是随机的不同的初始质心可能收敛到不同的局部最优n_init 会重复跑 10 次并保留结果最好的一次银行数据量大时这个值可以降到 5 以节省时间但不建议用默认的 1那会随机得让人抓狂。random_state42保证每次跑的初始点一致这样你上午和下午跑出来的结果不会变——这个细节在给业务方复现结果时很重要。画手肘图用 Matplotlib 看一眼即可这张图不用交付。看拐点的方法不是找最陡的位置而是找一个「再增加 KWSS 下降幅度明显变小」的位置。轮廓系数则选最大值对应的 K两个指标冲突时我的取值口径是「偏保守选小 K」因为群多了业务方运营不过来群少了画不出差异银行客户细分的 K 我见过最多的区间就是 36。3.4 聚类回写与结果导出给每一类客户贴上可读标签选定 K 后重新跑一次完整的 K-Means把标签写回原始 DataFrame同时把每个群的特征均值算出来导出成一张「分群画像表」。这张表就是你后面画雷达图、热力图的数据来源也是和业务方沟通的核心材料K 4 model KMeans(n_clustersK, random_state42, n_init10) df[cluster] model.fit_predict(X) # 分群画像每个群的特征均值 profile df.groupby(cluster)[feature_cols].mean().round(2) profile[客户数] df[cluster].value_counts().sort_index().values # 保存画像表后续可视化都靠它 profile.to_csv(cluster_profile.csv, encodingutf-8-sig) print(profile)聚类出来的群编号是 03这个编号本身没有任何含义群 0 不等于「最差的群」。此时我先不看编号看画像表里每一行的数值如果群 0 余额均值最高、产品数最多、信用分最高那它是高价值客群如果群 1 余额极低、开户时间很长但近期没有动账那它是沉睡客户群。这一步做完你手上有了标签也有了每个群的气质描述接下来才算真正进入「可视化」阶段。4. 把分群结果画出来散点图、雷达图与热力图的 PyECharts 实现4.1 主成分投影加散点图让每个客户在二维平面上有坐标拿到聚类标签后第一张图我建议画「PCA 投影散点图」把每个客户的位置画在平面上用颜色区分群。这张图一眼能看出分群效果群与群之间有没有重叠每个群是紧凑的一坨还是散成一片。先做 PCA 投影from sklearn.decomposition import PCA # 对标准化后的特征做 PCA降到 2 维用于画图 pca PCA(n_components2) coords pca.fit_transform(X) df[pc1] coords[:, 0] df[pc2] coords[:, 1] # 打印主成分解释方差比例 print(Explained variance ratio:, pca.explained_variance_ratio_)explained_variance_ratio_是主成分的方差解释比例比如输出[0.31, 0.18]表示第一主成分包含了 31% 的信息两个主成分合计约 49%。这个数字不是越高越好——它低了说明二维投影有信息损失但只要你不是拿它做预测只是给业务看「群的大致分界」解释力超过 40% 我就认为可以接受。如果低到 20%后面直接画三维散点图会更靠谱。接下来用 PyECharts 画散点图。几百万个点全部画上去浏览器会卡死所以先做一个等比例抽样每群抽 5000 个点既保证图不卡也能看出密度分布from pyecharts.charts import Scatter from pyecharts import options as opts # 等比例抽样每个群最多抽 5000 个点 sample_df df.groupby(cluster, group_keysFalse).apply( lambda x: x.sample(nmin(5000, len(x)), random_state42) ).reset_index(dropTrue) # 配置颜色 color_map [#5470c6, #91cc75, #fac858, #ee6666, #73c0de] groups sorted(sample_df[cluster].unique()) scatter Scatter(init_optsopts.InitOpts(width1000px, height600px)) for g in groups: sub sample_df[sample_df[cluster] g] data_pairs [[round(x, 2), round(y, 2)] for x, y in zip(sub[pc1], sub[pc2])] scatter.add_xaxis(data_pairs) # 实际按下方参数说明处理这里的代码写法在 PyECharts 里需要注意Scatter.add_yaxis才是传数据的接口add_xaxis接收的是类目轴值。正确习惯是把 x 和 y 组合成[[x1, y1], [x2, y2], ...]传入add_yaxisx 轴设置为「数值型」用opts.AxisOpts(type_value)。我当时第一次写 PyECharts 就在这里绕了弯先把数据准备好后续小标题专门画图时再演示完整写法。4.2 分群雷达图业务方真正能看懂的是这一张散点图证明「机器学习确实分出了几群」但业务方不一定看得懂主成分坐标轴。真正让业务方点头的是雷达图把每个群在「年龄」「余额」「产品数」「信用分」「活跃度」这些业务字段上的均值画成五边形或六边形几张雷达图叠在一起对比。这个图回答的是「这群人和其他群比到底哪里不一样」。画雷达图前要先归一化。余额均值可能是 30 万年龄均值是 45直接画会让余额撑爆整个图形其他特征全缩在圆心附近。归一化采用各群相对最大值的比例from pyecharts.charts import Radar # 选几个业务上最关心的维度 radar_features [age, balance, credit_score, num_products, tenure_months] # 每列除以该列最大值让数据落在 0~1 radar_data profile[radar_features].copy() for col in radar_data.columns: radar_data[col] radar_data[col] / radar_data[col].max()做完归一化后Radar图表接收的是每个群一个列表。pyecharts 的 Radar 图把 indicator 定义成「维度名 最大值」这里最大值就是 1。每个群对应一条折线4 个群就是 4 条不同颜色的折线业务方一眼就能看出「深蓝色的群余额缩在中心产品数也不高是低价值客户」「绿色群信用分拉满但余额也不低是优质客户」。这比散点图直观得多所以我做客户细分项目时汇报 PPT 第一张图一定是雷达图散点图只是辅助证明。4.3 分群热力图用全局视角验证「这些群有没有被分干净」热力图是第三个「验证向」的图。雷达图适合看单个群的特征形状热力图适合横向扫一眼所有群在所有特征上的高低关系——行是群列是特征颜色越深表示数值越高。画热力图之前要把 profile 表做一次 z-score 标准化不是原始值了是每个特征在各群之间做标准化这样每列的颜色分布才有对比意义from pyecharts.charts import HeatMap heat_data profile[feature_cols].copy() for col in heat_data.columns: std heat_data[col].std() heat_data[col] (heat_data[col] - heat_data[col].mean()) / std # 转成 pyecharts HeatMap 需要的 [x, y, value] 格式 heat_values [] for i, row_idx in enumerate(heat_data.index): for j, col in enumerate(heat_data.columns): heat_values.append([j, i, round(heat_data.loc[row_idx, col], 2)])热力图能暴露出一个散点图和雷达图都不容易发现的问题某两行两个群在所有列上的数值几乎没有差别。如果出现「群 0 和群 3 的 z-score 差异都小于 0.1」说明这两个群实际上是一个群被拆成了两半K 值选大了应该退回第 3.3 节重新跑更小的 K。热力图也是一个「排查向」工具它不直接给你结论但能逼你审视聚类结果的有效性。5. 银行客户细分可视化的五个常见坑特征陷阱、降维陷阱与展示陷阱5.1 不做标准化直接聚类所有客户挤成一个大团现象跑了 K-Means 之后散点图上所有客户不分颜色地堆成一团几个群的重叠面积超过 80%雷达图上各群均值几乎没有差异。原因这是最典型的翻车现场。balance 这一列的量纲比其他特征大几百倍欧氏距离完全由 balance 主导K-Means 实际上在按「余额大小」做一维切分其他所有特征都成了噪声。群之间除了余额均值不同其他画不出来。解决回到第 3.2 节确保连续特征全部经过 StandardScaler 标准化。标准化之后再跑一次聚类散点图会立即出现可辨识的簇。这个坑栽过的人最多血泪经验建议做成检查项每次建模前先打印X.describe()看量纲是否已被拉平。5.2 One-Hot 编码后聚类结果变成了「职业类型分组」现象分群画像里每个群对应一个职业类型群 0 全是公务员群 1 全是企业职员群 2 全是自由职业者但余额、产品数这些「银行行为特征」在群间差异不大。原因职业这个类别特征被 One-Hot 拆成 5 列后等于在特征空间里占了 5 个维度距离计算时它的话语权被放大到接近连续特征的总和K-Means 自然优先按职业切分。解决把高基数类别特征从聚类输入里拿掉或者做降权处理。更常见做法是聚类只用连续特征和二值特征职业分布留在分群画像阶段再统计作为「贴标签」的辅助信息而不是「分群」的驱动力。5.3 上百万个点在散点图上叠成一团浏览器直接卡死现象PyECharts 渲染几百万个散点页面打开要十几秒缩放时拖动一卡一卡散点重叠严重根本看不出群边界。原因浏览器渲染 DOM 节点数量有限百万级数据点超出了 ECharts 的舒适区另外大量点叠加在同一个像素位置时后画的点覆盖先画的点密度信息全部丢失。解决画图前做抽样每个群抽 30005000 个点足够展示分布形态。如果还想看密度把散点图换成「六边形分箱热力图」ECharts 的 heatmap 加 bmap 或 canvas 分箱或者给散点加opacity0.3让重叠区域的颜色比稀疏区域更深密度信息反而保留下来。5.4 手肘图没有明显拐点K 值纠结一整天现象WSS 曲线是一条平滑下降的线你盯着它看了半天也说不清拐点在哪轮廓系数在 K2 到 K6 之间一直在 0.2 上下浮动没有一个明显的高峰。原因银行客户数据本身噪声大内部结构不是清晰的球状簇K-Means 在这种数据上很难给出「数学上唯一正确」的 K。这种情况不是你的代码有 bug是数据本质如此。解决用业务约束替代数学最优。先问业务方运营团队能承接几个客群——一般 36 个超过 6 个运营计划就写不过来了。然后在这个范围内挑轮廓系数相对较高的值跑一遍看雷达图如果相邻两个群的画像差异不明显就减小 K。K 值不是纯数学问题是数学结果和业务可实施性之间的折中。5.5 群 0 不等于「最差的客户」把群编号直接当价值排序汇报现象汇报材料上写「群 0 客户需要重点挽留」但实际上群 0 是余额最高的高价值客户只是因为簇编号是 0 就被想当然了——更常见的错误是直接把 K-Means 输出的标签号当成排名0 当成差、3 当成好。原因K-Means 的簇编号是随机的0 到 3 只是内部索引不代表任何业务优先级。模型训练时根本没有「价值」这个概念它只看到了特征之间的距离。解决在输出分群画像表后先把每个群按关键业务指标余额均值、产品数、近 6 个月动账比例重新排序然后手动给群重命名「高价值活跃客群」「沉睡高潜力客群」「低价值流失风险群」「新开户成长群」。后续所有散点图、雷达图、大屏都显示这个名字而不是「群 2」。6. 从静态图到可交互筛选把分群结果做成业务说得清的可视化大屏做到第 5 章四张图已经齐了PCA 散点图、雷达图、热力图、画像表。但单张图是给「看」的可视化大屏是给「用」的。业务经理在大屏上选中「高价值活跃客群」散点图只高亮这群人雷达图切换成这群人的画像下面的明细表列出这群人的客户数、平均余额、平均产品数——这种联动筛选才是银行客户细分可视化的最终形态。PyECharts 实现联动不需要写前端用 page 或 tab 把多个图组合起来给每个图配置相同的图例组件即可要做到真正点击筛选常见做法是给散点图绑定事件把选中的群维度传给其他图表。这个方案在 ECharts 的官方示例里叫「联动的 dataZoom legend」银行项目里最常见的大屏方案。这里分享一个我自己的验证习惯图做完先别急着上大屏先做「业务校验」。把聚类结果和后续 36 个月的真实绩效数据关联算每个群的产品购买率或留存率如果高价值群的购买率确实明显高于低价值群说明分群是有效的如果各群绩效曲线完全重叠说明特征没选对——回第 3 章换特征重跑。我第一次做银行客户细分就只盯着散点图是否好看结果业务方问「这群人是不是真的会买理财产品」我答不上来只能回去补这步校验。现在我的习惯是任何分群项目先问清楚「分完怎么验证」再动手建模。希望帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑