简介面向银行风控与营销团队的机器学习客户细分可视化项目以德国信用数据集为基础演示如何通过聚类、决策树等算法划分客户群体并结合柱状图、散点图等直观呈现不同群体的特征差异适合金融数据分析入门者与需要快速搭建客户细分原型的从业者参考。压缩包仅含两个文件包括一个Python主脚本与一个CSV数据文件总大小17KB脚本覆盖数据读取、特征处理、模型训练与可视化输出环节数据文件便于按需替换或扩展。该项目已有六十八人学习下载属于轻量级入门示例。通过阅读源码并实际运行可掌握银行客户细分的完整流程、常用聚类算法调用方式以及可视化展现场景还能根据自身数据调整特征维度与算法参数进一步理解机器学习在金融营销和风险管理中的应用逻辑。1. 银行客户细分可视化为什么图表比准确率更让业务买账银行客户细分并不是新鲜事但绝大多数团队做出来的结果都死在最后一步模型报告和数据表格堆在文件共享盘里业务部门点开看一眼就关掉了。机器学习把客户聚类成三四类并不难难的是让营销人员一眼看懂“这批高价值客群到底长什么样、该用什么策略去触达”。客户细分可视化就是这个把模型输出翻译成业务决策的环节——它不是让算法工程师自嗨的散点图而是能回答“谁值得配置资源”的数据传达过程。这篇文章适合正在做机器学习课程设计、银行数据分析项目或者刚接手客群运营的从业者我会把从特征工程、聚类选型到可视化看板的完整落地路径拆开讲全程按可复现的标准来写。2. 先做对细分再做图特征工程与聚类选型的三个决定2.1 特征不是越多越好RFM 结构与数据清洗边界银行客户数据最常被直接扔进模型的字段是年龄、性别、资产余额、交易次数、渠道活跃度。但这些字段堆在一起有个隐蔽问题——数值尺度差异巨大。年龄在 20 到 70 之间波动账户余额可能是几万到几百万交易次数又是另一套量纲。KMeans 这类基于距离的算法对量纲极其敏感如果不做标准化聚类结果实际上只被余额这个字段主导其他特征全部失效。我一般会先把客户数据收敛到 RFM 结构上。Recency 代表距离上次交易的天数Frequency 是近 90 天交易次数Monetary 是近 90 天累计交易金额再补两个银行特有维度账户平均余额和产品持有数。这个结构的价值在于它稳定刻画了客户的活跃度和价值而不是一次性的静态画像。清洗边界很容易被忽视。交易金额字段里的负值代表退款或冲正不能简单当作消费金额处理开户时间缺失的客户如果占比超过 3%直接把整列扔掉比重编码更安全因为插补出来的时间没有业务含义。还有一个容易踩的坑同一客户 ID 下有多张卡必须按客户维度聚合否则同一客户会被拆成多条记录进入聚类。import pandas as pd import numpy as np # 模拟一批银行客户交易数据仅供流程演示 rng np.random.default_rng(42) n 5000 df pd.DataFrame({ customer_id: range(1, n 1), last_txn_days_ago: rng.integers(0, 180, n), txn_count_90d: rng.integers(1, 80, n), txn_amount_90d: np.round(rng.lognormal(9, 1.2, n), 2), avg_balance: np.round(rng.lognormal(10, 1.5, n), 2), products_held: rng.integers(1, 8, n), age: rng.integers(22, 70, n) }) # 退款金额会低于零这里统一做截断处理 df[txn_amount_90d] df[txn_amount_90d].clip(lower0) # 按客户聚合确保一张卡一条记录的情况被合并 df_grouped df.groupby(customer_id).agg( recency(last_txn_days_ago, min), frequency(txn_count_90d, sum), monetary(txn_amount_90d, sum), avg_balance(avg_balance, mean), products(products_held, max), age(age, mean) ).reset_index() features [recency, frequency, monetary, avg_balance, products, age] X df_grouped[features]这段代码先用模拟数据跑通流程实际场景里直接替换成数仓查询结果即可。注意聚合时 recency 取 min 而不是 mean因为一个客户只要最近发生过交易他就算活跃客户frequency 取 sum 是统计总交易次数products 取 max 是因为客户持有产品的种类数不会因为多次交易而增加。clip(lower0) 是处理退款记录的最保守做法如果你的数据里退款占比很高要单独分析退款原因而不是简单截断。2.2 用 KMeans 还是 GMM基于客户分布形态的选型逻辑KMeans 是银行客户细分项目里最主流的算法原因很实际计算快、可解释性强、每个客户只属于一个客群业务部门能直接拿着客群编号去配置运营策略。但 KMeans 有个天然前提——它假设每个簇是凸形且大小相近的球状分布。银行客户数据里高净值客户往往是少数长尾群体这会让 KMeans 把长尾客户硬切成两半。GMM高斯混合模型能给出每个客户属于每个客群的概率是一种软聚类对长尾分布容忍度高但参数估计更复杂结果也不如 KMeans 直观。我的做法是先用 KMeans 跑出基准结果再用 GMM 做对比如果两个模型的客群特征分布差异不大就选 KMeans 上线因为它重训成本低、参数少、排错容易。from sklearn.preprocessing import StandardScaler from sklearn.cluster import KMeans from sklearn.mixture import GaussianMixture from sklearn.metrics import silhouette_score scaler StandardScaler() X_scaled scaler.fit_transform(X) # KMeans 基准 km KMeans(n_clusters4, n_init10, random_state42) km_labels km.fit_predict(X_scaled) print(KMeans 轮廓系数:, silhouette_score(X_scaled, km_labels).round(4)) # GMM 对比 gmm GaussianMixture(n_components4, covariance_typefull, random_state42) gmm_labels gmm.fit_predict(X_scaled) print(GMM 轮廓系数:, silhouette_score(X_scaled, gmm_labels).round(4))standard scaler 是必须的它会把所有特征压缩到均值为 0、方差为 1 的分布消除量纲影响。n_init10 意味着 KMeans 会从 10 个随机中心点出发找最优解避免陷入局部最优random_state 固定下来保证你每次跑出来的聚类标签一致否则上下游的报表对不上号。silhouette_score 的取值范围是 -1 到 1银行客群能跑到 0.2 到 0.35 已经算不错别指望 0.7 那种高分。2.3 定 K 的两种可复现做法肘部法和轮廓系数K 值定多少是客户细分项目里业务方最爱插手的问题。业务会说“我们以前分五层你就分五层”但历史分层未必符合当前数据分布。我习惯同时跑肘部法和轮廓系数两个指标交叉验证后再定 K。肘部法看的是各个 K 值下的簇内误差平方和inertia随着 K 增大inertia 必然下降但下降速度会在某个 K 值出现明显拐点这个拐点就是相对合理的簇数。轮廓系数则衡量每个样本与自身簇的匹配程度得分越高说明簇内紧密、簇间分离越清晰。两个方法都有局限肘部法的拐点并不总是锐利轮廓系数在数据量大时计算成本高所以同时看两者取一个折中的 K。K_range range(2, 11) inertias [] silhouettes [] for k in K_range: km KMeans(n_clustersk, n_init10, random_state42) labels km.fit_predict(X_scaled) inertias.append(km.inertia_) silhouettes.append(silhouette_score(X_scaled, labels)) result pd.DataFrame({k: list(K_range), inertia: inertias, silhouette: silhouettes}) print(result)跑完结果你会发现silhouette 分数最高的 K 值往往只有 2 或 3但实际业务里分成 2 类毫无意义——你无法针对“高价值”和“低价值”两类客群设计足够细的运营策略。而 K 取 5 以上时每个客群的样本量会变得很薄营销活动触达规模不够。银行场景里 K 落在 4 到 6 之间最合适具体数值要看每个客群的样本占比占比低于 10% 的客群基本不具备独立运营价值需要合并。3. 用 Python 把聚类结果画成业务能读懂的四种图3.1 散点图降维PCA 与 t-SNE 的取舍聚类完的数据有 6 个特征维度不能直接画图。常见做法是先降维再可视化PCA 是首选。PCA 是线性降维计算快而且投影方向是可复现的——这意味着你在训练集上拟合出的 PCA 模型可以保存下来以后新客户进来先标准化再投影直接落到你已经画好的图上不需要重新训练。t-SNE 的散点图看起来更美观簇与簇之间的边界更清晰但它有个让银行项目翻车的特性t-SNE 的坐标没有稳定含义每次运行结果都不同而且不能把新样本映射到已有的图上。所以 t-SNE 只适合做探索性分析不适合做客户细分的生产级可视化。from sklearn.decomposition import PCA import plotly.express as px pca PCA(n_components2) pca_coords pca.fit_transform(X_scaled) plot_df df_grouped.copy() plot_df[PC1] pca_coords[:, 0] plot_df[PC2] pca_coords[:, 1] plot_df[segment] km_labels fig px.scatter( plot_df, xPC1, yPC2, colorsegment, opacity0.6, hover_data[age, avg_balance], title客户细分 PCA 降维散点图 ) fig.update_layout(width900, height600) fig.show()绘制散点图时不要把 6 个原始特征都塞进 hover_data只保留业务方最关心的字段比如年龄和平均余额否则鼠标悬停时信息过载。opacity 设到 0.6 是为了处理重叠点——银行数据量大数千个客户叠在一起完全不透明就是一团黑。PC1 和 PC2 两个主成分的方差解释比例一般在 0.5 到 0.7 之间图上相邻的点不一定真的相似但至少能看到客群的分布边界。3.2 雷达图把每个客群画像画成五边形雷达图是银行客户细分项目里业务方接受度最高的图表类型因为它把“高价值客群的特征”压缩成一个直观的几何形状。一个客群是一个多边形多个客群叠在一起形状差异一眼可见。但雷达图有个极易被误用的细节不同特征的数值范围必须统一。如果你把“交易次数”和“账户余额”直接放进同一张雷达图余额那边的大数值会让交易次数的轮廓压扁成一条直线。正确做法是先对每个特征按客群取均值再做 min-max 归一化把每个维度的数值压到 0 到 1 之间这样雷达图才能真实反映每个客群在不同维度上的相对强弱。# 按客群聚合特征均值 seg_profile plot_df.groupby(segment)[features].mean() # min-max 归一化到 [0, 1] seg_norm (seg_profile - seg_profile.min()) / (seg_profile.max() - seg_profile.min()) import plotly.graph_objects as go fig go.Figure() for seg_id in seg_norm.index: values seg_norm.loc[seg_id].values.tolist() values values[:1] # 闭合多边形 fig.add_trace(go.Scatterpolar( rvalues, thetaseg_norm.columns.tolist() [seg_norm.columns[0]], filltoself, namef客群{seg_id} )) fig.update_layout( polardict(radialaxisdict(visibleTrue, range[0, 1])), title客户细分雷达图归一化后 ) fig.show()归一化这里用的是全局 min-max注意要在所有客群拼好的数据上算 min 和 max而不是每个客群各自归一化——否则每个客群的雷达图都占满整个画布形状无法横向比较。filltoself 让多边形内部填充半透明色多个客群叠加时不至于看不清边界。雷达图适合展示 3 到 5 类客群如果你的 K 值超过 6雷达图会变成蜘蛛网建议改用分组柱状图。3.3 分布对比图同一特征在四个客群里的差异雷达图告诉你每个客群的平均水平但掩盖了客群内部的分布情况。两个客群的平均余额可能一样一个是集中在一个窄区间的普通客户另一个是两极分化的混合群体它们的运营策略完全不同。所以我在画完雷达图之后一定会补一组分布对比图对每个关键特征做分客群的分布叠加。直方图是这里最实用的工具。可以用来对比不同客群在同一特征上的分布形态。画分布对比图时要注意长尾问题银行客户的余额和交易金额普遍服从对数正态分布右侧长尾拉到天际直方图画出来根本看不出差异。解决办法是对数值做 log 变换或者用 matplotlib 直接画在 log 坐标轴上。import matplotlib.pyplot as plt fig, axes plt.subplots(2, 2, figsize(12, 8)) features_to_plot [monetary, avg_balance, frequency, recency] for ax, col in zip(axes.flatten(), features_to_plot): for seg_id in sorted(plot_df[segment].unique()): subset plot_df[plot_df[segment] seg_id] ax.hist(subset[col], bins40, alpha0.5, labelf客群{seg_id}) ax.set_title(f{col} 分布对比) ax.legend() ax.set_xscale(log) # 对数坐标处理长尾 plt.tight_layout() plt.show()set_xscale(log)是最关键的参数——余额和金额这类特征不取 log直方图就是一根贴着左侧的柱子加一条拖到最右边的细线什么都看不出来。取 log 之后分布形态才会展开你才能看出高价值客群到底是单峰还是双峰。注意 recency 不需要 log 变换它是最近交易天数分布本身就在一个有限范围内。4. 从静态图到交互看板用 Dash 搭建银行客户细分可视化工具4.1 为什么选 Dash 而不是 Flask 加 ECharts静态图文件交给业务方他们看一眼就过去了不会反复探索。要真正让客群细分结果被业务用起来你得做一个交互看板让用户自己选择特征维度、切换客群编号、查看不同时间窗口的客户分布。市面上做可视化大屏的工具有很多ECharts 的自由度最高但需要写大量前端代码百度可视化大屏这类产品适合汇报场景不适合内部日常运营。我的选择是 Dash——它是 Python 生态里的交互看板框架底层由 Plotly 驱动能把上面画的散点图、雷达图、直方图直接组装进网页应用而且数据过滤回调用 Python 写团队里不需要养前端工程师。银行场景里最常见的做法是用 Dash 搭一个内部工具嵌入企业微信或统一门户地址只对数据分析团队和业务负责人开放。pip install dash pandas scikit-learn plotlyDash 应用的代码结构非常固定app Dash(name)layout 定义页面组件callback 定义交互逻辑。最小可用的看板只需要一个下拉框选择特征一个图形区域展示对应图表。4.2 用 Dash 搭建最小可用看板下面是一个能跑的看板骨架包含客群选择器和分布直方图。它的数据来源直接复用前面算好的 plot_df实际项目里可以把这一层换成数据库查询。from dash import Dash, dcc, html, Input, Output import plotly.express as px app Dash(__name__) app.layout html.Div([ html.H3(银行客户细分可视化看板), dcc.Dropdown( idfeature-selector, options[{label: c, value: c} for c in features], valuemonetary ), dcc.Graph(idsegment-histogram) ]) app.callback( Output(segment-histogram, figure), Input(feature-selector, value) ) def update_histogram(selected_feature): fig px.histogram( plot_df, xselected_feature, colorsegment, nbins40, marginalbox, log_xTrue ) fig.update_layout( titlef{selected_feature} 分布按客群拆分, height500 ) return fig if __name__ __main__: app.run(debugFalse, host127.0.0.1, port8050)这段回调函数的工作流程是用户下拉框选择特征Dash 把选中的值传给 update_histogram函数重新生成直方图并返回给前端展示。关键点是 dcc.Graph 接收的 figure 必须是一个 Plotly 图形对象不能是 matplotlib 的对象。log_xTrue 在折线图和直方图里都能直接用省去了手动取 log 的步骤。marginalbox 在直方图上方叠加一个箱线图能同时在图上看到分布形态和四分位信息业务方反馈这个设计非常直观。4.3 上线前要处理的数据脱敏与性能设置银行数据的合规要求比普通行业严格得多。客户 ID 必须哈希脱敏账户余额要按区间脱敏而不是直接显示精确值。做法是事先在 SQL 层把数值型字段做分箱——比如余额字段只在看板里保留区间标签比如“10万-50万”而不是精确到分。这样业务方也能用但不会留下可反查的精确客户信息。性能方面要注意如果跑在银行的内网服务器上数据量超过 10 万行时回调函数里每次重新绘制图形都会有一次完整的数据帧操作。两个常用优化手段一是用 dcc.Store 组件把 plot_df 存到浏览器端避免每次回调都从服务器读数据二是把 csv 数据在应用启动时加载到全局变量不要放在回调函数里重复读取。import pandas as pd # 应用启动时只加载一次 plot_df pd.read_csv(segmented_customers.csv) # 后续回调直接引用全局变量禁止在回调里重新读文件这个写法看起来简单却是一个经常被忽视的性能陷阱。开发环境下本地文件读取只慢几十毫秒部署到服务器并发访问时每次回调读文件会导致应用响应时间指数级上升。5. 客户细分可视化的避坑指南数据泄漏、图面欺骗与重训时机5.1 聚类标准化时的数据泄漏现象聚类模型上线后新客户打标签的结果跟历史样本对不上业务方反馈标签不稳定。 原因标准化时用了全量数据的均值和标准差每次重训数据一更新均值就变了所有客户的标准化结果跟着变聚类标签自然漂移。 解决把标准化器的参数、PCA 的投影矩阵、聚类中心都保存到文件中新客户进来直接用保存的参数做变换不要重新训练。import joblib joblib.dump(scaler, scaler.pkl) joblib.dump(pca, pca.pkl) joblib.dump(km, km_model.pkl)这是机器学习落地里的经典坑。训练时你用的是全量数据的均值做标准化上线时新客户必然被混入历史的分布里但历史客户的特征不能跟着每次重训就变一遍。joblib 保存的参数就是给“新客户如何映射到旧客群”用的固定基准。5.2 轮廓系数高不代表客群可运营现象聚类轮廓系数跑到了 0.45算法同学很兴奋但业务方看了客群画像后说这没法用。 原因轮廓系数衡量的是几何分布上的簇间分离度。如果数据里有一簇是“长期不动户”它们和活跃客户的分布在距离上天然分离轮廓系数自然高但这一簇客户没有任何运营价值。 解决不要单独看轮廓系数要同时看每个客群的样本占比和业务指标——比如客群 A 的 90 日交易金额中位数、近 90 日活跃率。客群占比低于 5% 的簇直接合并到距离最近的簇里因为它撑不起一次营销活动的触达规模。这个教训在银行项目里反复出现。高轮廓系数只能证明聚类算法收敛正常不能证明客群划分有业务意义。你画的散点图几何边界再清晰落在图上的是“一群人”而不是“一种可运营的策略”。5.3 雷达图坐标轴不统一导致画像误读现象雷达图里某个客群几乎所有维度都顶到外圈看起来全是高净值人群但业务核对后说这些人明明只是交易次数多。 原因做雷达图时按每个客群单独做了归一化每组数据都压缩到自己的 0 到 1 范围导致每个客群的图形都塞满圆框。客群之间的相对差异被抹掉了。 解决归一化必须基于所有客群的全局 min/max。做法是先做客群聚合得到各特征的均值矩阵再对这个矩阵做一次 min-max而不是对每个客群单独处理。这种图面欺骗很难排查因为你单独看一个客群的雷达图时图形是满的看起来很漂亮但多个客群放在一起比较时形状差异全失真了。可视化项目里最怕的是图表本身没有语法错误、渲染也正常但传达的信息是错的。画完雷达图之后多一步打印出归一化前每个客群的特征均值表核对一遍再展示。5.4 聚类中心漂移月度数据没有重训现象客户细分看板上线后前三个月挺好第六个月开始业务反馈客群标签跟实际感受对不上。 原因银行客户的交易行为有季节性。年初开门红期间交易频率整体升高年中回落年末理财到期会导致短期余额冲高。聚类中心是基于历史某段时间训练的客户行为变化后新客户被硬分到旧客群里去。 解决建立月度或季度的定期重训机制每次重训后用调整兰德指数对比新旧标签的一致性。如果新模型把 30% 以上的客户重新分配到其他客群说明客户结构发生了实质变化需要业务方重新审视客群定义。from sklearn.metrics import adjusted_rand_score # old_labels 是上个月模型打的标签new_labels 是本月重训后的标签 ari adjusted_rand_score(old_labels, new_labels) print(fARI: {ari:.3f}) # 低于 0.5 说明客群结构明显变化调整兰德指数是衡量两次聚类结果一致性的标准指标取值接近 1 说明标签基本没变接近 0 接近随机分配。这个数字可以作为要不要更新客群定义的绿灯阈值。5.5 可视化大屏反而让业务决策变慢现象费力气做了一个大屏投到办公室电视上循环播放业务方却开始频繁找数据分析师线下问数据决策链路反而更长了。 原因大屏适合汇报不适合分析。所有图表在屏幕上同时展示视觉上很热闹但没有交互路径业务方想从“资产分布”跳到“具体客户名单”大屏做不到只能再去找人。 解决把大屏当成汇报专用产物同时做一份交互版看板把筛选器、下钻、导出功能做足。我前面写的 Dash 看板就是干这个用的。大屏上的图认真设计交互看板里的图保证功能完整两者分工清楚团队效率才高。数据可视化项目里常见的一种翻车就是团队把精力都花在视觉酷炫上却没人接住业务方真正的诉求。6. 让图表驱动决策从客群画像到营销动作的闭环验证6.1 用留存率验证客群纯度画完图、上线看板不等于项目结束。你应该在客群标签上叠加后续行为指标验证每个客群是不是可运营的。最实用的是留存率给客群打标签后观察未来 30 天和 90 天的资金净流入比例、交易活跃保持率。如果某个客群的留存率和它的画像判断不一致比如画像是“高价值客户”但 90 天留存率不到 30%说明聚类特征里遗漏了关键变量比如有没有绑卡、有没有开通手机银行。6.2 用分布图验证比散点图更有说服力我自己常用的验证方式是回到分布对比图。画一张“高价值客群”在月度活跃天数上的分布图如果分布呈现明显的双峰说明这一个客群实际上包含了两种人需要回去调整 K 值或增加特征维度。散点图上簇分离度看起来很漂亮但分布图上的双峰信号是散点图给不出来的——散点图的问题在于把所有维度折叠成了两个主成分样本点之间的真实距离已经在投影时变形了。6.3 把图表变成运营动作的闭环最后一步是把每一类客群对应到明确的运营动作。我习惯在可视化看板里加一列“建议策略”比如高价值高频客群配置专属客户经理沉睡高价值客群触发唤回短信低价值低频客群放入降本名单不再人工跟进。这个映射表要挂在图表的副标题里业务方看一次图就对照一次动作。客群特征画像建议动作客群0高频高余额、产品持有多客户经理一对一维护客群1高余额低频率、年龄偏大唤醒触达推荐稳健型产品客群2中频中余额、产品数少交叉销售提升产品持有数客群3低频低余额、近90日无交易自动培育降低运营成本做银行客户细分可视化这些年我最大的教训是机器学习模型可以靠指标打分但可视化方案必须靠业务反馈打分。图表做出来不难难的是把算法结果翻译成业务一眼就能做决定的画面。跑通上面这套流程之后业务方开始主动说“这个图我看懂了”那一刻才是项目真正的验收。如果你正在做自己的客户细分项目建议从第二章的特征工程开始按顺序一路跑到看板每一步的产物都保留下来这套链路值得投入希望帮到你。本文还有配套的精品资源点击获取