聚类是无监督学习的核心,但大多数教程只讲 K-Means 跑通就完事。这篇教程 从数学直觉 → K-Means 选 K → DBSCAN/HDBSCAN 噪声检测 → UMAP 高维降维 → 营销 ROI 落地,每一层都是真实数据 + 真实输出 + 真实代码。所有图都是我本机 matplotlib 渲染的 (Hiragino Sans GB 中文字体)。
本文所有结果均来自本机真实运行 · Python 3.11 · sklearn 1.9.0 · hdbscan 0.8.x · umap-learn 0.5.12
| 维度 | 值 |
|---|---|
| 样本数 | 7,043 客户 (清洗后 7,032) |
| 原始列数 | 21 (1 ID + 3 数值 + 16 类别 + 1 标签) |
| 目标 | Churn (Yes/No, 流失率 26.5%) |
| 数值特征 | tenure / MonthlyCharges / TotalCharges |
| 类别特征 | gender / Contract / PaymentMethod / InternetService / 等等 15 个 |
| 业务场景 | 电信客户分群 + 流失预警 + 营销 ROI |
numpy 2.4.6 / pandas 3.0.5 / scikit-learn 1.9.0
hdbscan 0.8.x / umap-learn 0.5.12 / matplotlib 3.11.1
中文字体: Hiragino Sans GB (macOS 系统字体)
import pandas as pd
df = pd.read_csv("data/telco_churn.csv")
print(f"原始数据: {df.shape}")
# (7043, 21)
numeric_cols = ["tenure", "MonthlyCharges", "TotalCharges"]
df[numeric_cols] = df[numeric_cols].apply(pd.to_numeric, errors="coerce")
df = df.dropna(subset=numeric_cols)
# (7032, 21) - 去掉 11 条 TotalCharges 缺失
核心问题 · 聚类到底在算什么? 选什么距离度量?
聚类的目标函数:最小化簇内距离 + 最大化簇间距离。但 "距离" 怎么定义决定了 80% 的结果。
三大距离度量在 Telco 数据上的对比实验 (标准化后):
| 度量 | Normal 点距离 | Outlier 点距离 | 比值 (outlier/normal) | 对异常敏感度 |
|---|---|---|---|---|
| L2 Euclidean | 0.1732 | 19.3054 | 111.5x | 极敏感 |
| L1 Manhattan | 0.3000 | 33.4361 | 111.5x | 同样比例,但绝对距离更小 |
| Cosine | — | 1.9948 | — | 完全不看距离,只看方向 |
在 Telco tenure vs MonthlyCharges 上跑 K-Means K=4:
| 度量 | Silhouette |
|---|---|
| Euclidean (L2) | 0.4721 |
| Manhattan (L1) | 0.4721 |
两种距离在本数据集上结果几乎一致 (Silhouette 都是 0.4721)。这告诉我们: 对于低维、分布均匀的数据,选 L2 或 L1 差别不大;真正有差异的场景是高维稀疏数据 (文本/embedding) 或异常值密集时。
核心问题 · 怎么选 K? 选完后怎么翻译成业务?
K-Means 必须预设 K,但业务上往往不知道应该分几群。三种指标各有侧重:
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, davies_bouldin_score
from sklearn.preprocessing import StandardScaler
X = StandardScaler().fit_transform(df[["tenure", "MonthlyCharges", "TotalCharges"]])
for k in range(2, 11):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X)
sil = silhouette_score(X, labels)
| 簇 | Tenure (月) | Monthly ($) | Total ($) | Senior | Churn 率 | 客户数 | 占比 |
|---|---|---|---|---|---|---|---|
| 0 (高价值) | 56.87 | 89.76 | 5,085 | 21.5% | 16.9% | 2,360 | 33.5% |
| 1 (新/低端) | 20.07 | 52.19 | 868 | 13.6% | 31.5% | 4,672 | 66.5% |
核心问题 · 不是所有客户都应该被分到一群 — 噪声怎么办?
K-Means 的硬伤:每个客户都被强制分到 1 簇。但真实业务里:
DBSCAN 有两个关键参数: eps (邻域半径) 和 min_samples (核心点最少邻居)。我在 Telco 3 维特征上做 9 组实验:
| eps | min_samples | 簇数 | 噪声比 | Silhouette | |
|---|---|---|---|---|---|
| 0.3 | 5 | 1 | 0.0% | — | |
| 0.3 | 10 | 1 | 0.0% | — | |
| 0.3 | 20 | 1 | 0.0% | — | |
| 0.5 | 5 | 1 | 0.0% | — | |
| 0.5 | 10 | 1 | 0.0% | — | |
| 0.5 | 20 | 1 | 1 | 0.0% | — |
| 0.8 | 5 | 1 | 0.0% | — | |
| 0.8 | 10 | 1 | 0.0% | — | |
| 0.8 | 20 | 1 | 0.0% | — |
HDBSCAN (★3.1k) 是 DBSCAN 的进化版 — 无需预设 eps,自动适应变密度:
import hdbscan
hdb = hdbscan.HDBSCAN(min_cluster_size=100, min_samples=10)
labels = hdb.fit_predict(X)
| 维度 | K-Means | HDBSCAN |
|---|---|---|
| Silhouette | 0.4721 | -0.0694 |
| 簇数 | 4 (强制) | 10 (自适应) |
| 噪声识别 | ❌ 无 | ✅ 48.9% |
| 软聚类概率 | ❌ 无 | ✅ 0.00-1.00 |
| 超参数 | K (1 个) | min_cluster_size + min_samples (2 个) |
| 适合数据 | 低维均匀分布 | 高维变密度 / 噪声密集 |
核心问题 · 21 维数据怎么聚? 怎么可视化?
Telco Churn 原始有 21 列特征 (one-hot 后 30 维)。直接在高维空间聚类有两个问题:
降维是关键步骤。但PCA (线性) vs UMAP (非线性) 效果天差地别。
from sklearn.preprocessing import OneHotEncoder, StandardScaler
from sklearn.decomposition import PCA
import umap
cat_cols = df.select_dtypes(include=["object", "string"]).columns.tolist()
cat_cols = [c for c in cat_cols if c != "Churn"]
num_cols = df.select_dtypes(include=["int64", "float64"]).columns.tolist()
X_cat = OneHotEncoder(sparse_output=False, drop="first").fit_transform(df[cat_cols])
X_num = StandardScaler().fit_transform(df[num_cols])
X_full = np.hstack([X_num, X_cat]) # (7032, 30)
PCA 2D 解释方差: 53.4% (PC1=35.9% + PC2=17.5%) — 接近一半信息丢失
UMAP 2D: 非线性降维,保留局部结构(同簇点拉近,异簇点推远)
| 方法 | 簇数 | 噪声比 | 业务可解释性 |
|---|---|---|---|
| 原始 30-dim HDBSCAN | 3 | 0.0% | ❌ 太粗,看不出结构 |
| UMAP 2D + HDBSCAN | 8 | 1.0% | ✅ 精细分群 + 极少噪声 |
真实发现:UMAP 降维后聚类质量远胜直接在 30 维聚类:
核心问题 · 聚完怎么用? 怎么算出真金白银的 ROI?
在 tenure/MonthlyCharges/TotalCharges 上跑 K-Means K=4 (按 L2 Elbow 选择),给每个簇业务命名:
| 分群 | Tenure | Monthly | Total | Churn | 客户数 | 占比 |
|---|---|---|---|---|---|---|
| 🌱 新低消费 | 10.3 月 | $31.78 | $304 | 25% | 1,696 | 24.1% |
| 📊 中价值稳健 | 15.5 月 | $80.79 | $1,253 | 48% | 2,273 | 32.3% |
| 📊 中价值稳健 (老) | 53.6 月 | $34.92 | $1,837 | 5% | 1,159 | 16.5% |
| 💎 高价值核心 | 59.5 月 | $93.31 | $5,549 | 15% | 1,904 | 27.1% |
基于业务经验给每个分群匹配营销动作,计算 ROI:
| 分群 | 营销动作 | 营销成本 | 增量/挽回收入 | ROI |
|---|---|---|---|---|
| 🌱 新低消费 | 引导升级套餐 + 免费试用 | ¥50,880 | ¥122,112 | 1.4x |
| 📊 中价值稳健 | 自动化邮件 + 满意度调查 | ¥11,365 | ¥32,731 | 1.9x |
| 📊 中价值稳健 (老) | 自动化邮件 + 满意度调查 | ¥5,795 | ¥16,690 | 1.9x |
| 💎 高价值核心 | VIP 专属客服 + 提前上新 | ¥38,080 | ¥91,392 | 1.4x |
| 合计 | — | ¥106,120 | ¥262,925 | 1.48x |
| 脚本 | 章节 | 运行时间 | 关键产出 |
|---|---|---|---|
| L1_math.py | 距离度量对比 | 5 秒 | L1_distance_metrics.png |
| L2_kmeans.py | K-Means 选 K | 15 秒 | L2_choose_k.png + L2_kmeans_result.png + profile.csv |
| L3_hdbscan.py | 密度聚类 | 10 秒 | L3_dbscan_hdbscan.png |
| L5_umap.py | UMAP 降维 | 60 秒 | L5_pca_vs_umap.png + L5_umap_clusters.png |
| L7_business.py | ROI 落地 | 5 秒 | L7_business_roi.png + segment profile |
cd ~/Documents/Code/articles-site/_tmp_ml
# 数据准备 (一次性)
curl -sL -o data/telco_churn.csv \
"https://raw.githubusercontent.com/IBM/telco-customer-churn-on-icp4d/master/data/Telco-Customer-Churn.csv"
# 装包 (一次性)
python3 -m venv .venv
.venv/bin/pip3 install numpy pandas scikit-learn matplotlib seaborn hdbscan "umap-learn>=0.5.7" "numba>=0.60"
# 跑全部 (95 秒)
.venv/bin/python3 L1_math.py
.venv/bin/python3 L2_kmeans.py
.venv/bin/python3 L3_hdbscan.py
.venv/bin/python3 L5_umap.py
.venv/bin/python3 L7_business.py
| 项目 | Star | 用途 |
|---|---|---|
| scikit-learn-contrib/hdbscan | ⭐3.1k | 变密度聚类首选 |
| MaartenGr/BERTopic | ⭐7.8k | NLP 主题聚类一站式 |
| lmcinnes/umap | ⭐7.7k | UMAP 官方实现 |
| facebookresearch/faiss | ⭐40k | 工业级向量聚类 |
| rapidsai/cuml | ⭐5.3k | GPU 加速 / 亿级数据 |
| spotify/annoy | ⭐14k | Spotify 音乐推荐 |
把"分得好不好"拆成 3 层判断 · 数学层 → 业务层 → 上线层
聚类评估不像 LR/XGBoost 那样只看 AUC。聚类是"无监督",没有标签可以对。所以必须从 3 个层次分别评估:
数学层: 分得齐不齐? (算法内部视角)
业务层: 分得有意义吗? (业务可解读)
上线层: 分得稳定吗? (线上可监控)
公式:
s(i) = (b(i) - a(i)) / max(a(i), b(i))
a(i) = 客户 i 到自己簇内其他点的平均距离 (越小越紧)
b(i) = 客户 i 到最近的其他簇点的平均距离 (越大越分得开)
解读阈值:
| 范围 | 解读 | 业务含义 |
|---|---|---|
| 0.71 - 1.0 | 簇结构强 | 教科书例子才有,真实数据罕见 |
| 0.51 - 0.70 | 合理 | 商业项目 ≥0.5 就够用 |
| 0.26 - 0.50 | 弱结构 | 真实业务常见,要结合其他指标 |
| < 0.25 | 无结构 | 聚类意义不大,换算法或加特征 |
我们的真实数字 (Telco K-Means K=2): 0.4795 — 弱结构,但在 Churn 场景够用。
公式: 每个簇的"散度 / 簇间距离"的平均值。
解读: DBI < 0.5 优秀, 0.5-1.0 好, > 1.5 差。和 Silhouette 互补,通常一起用。
没有绝对阈值,只看"拐点"。从陡降到平台的位置就是最佳 K。
好的聚类: 每簇 ≥ 5% 客户 (每个分群都有营销意义)
差聚类: 一个簇 90% 客户,其他加起来 10% (没分出来)
我们的真实数据 (Telco K=4):
| 簇 | 客户数 | 占比 | 判断 |
|---|---|---|---|
| 0 高价值核心 | 1,904 | 27.1% | ✅ |
| 1 新低消费 | 1,696 | 24.1% | ✅ |
| 2 中价值 (月付高费) | 2,273 | 32.3% | ✅ |
| 3 中价值 (老低费) | 1,159 | 16.5% | ✅ |
好的聚类: 不同簇在某关键特征上差异 ≥ 2x
我们的真实数据:
Tenure 差异: 簇 0 = 59.5 月, 簇 1 = 10.3 月 → 5.8x ✅
MonthlyCharges 差异: 簇 0 = $93.31, 簇 1 = $31.78 → 2.9x ✅
Churn 率差异: 簇 2 = 48%, 簇 3 = 5% → 9.6x ✅ 强信号
好的聚类: 不同簇的 Churn 率差异 ≥ 1.5x,说明聚类抓到了"流失倾向"信号。
差聚类: 所有簇 Churn 率都差不多 (聚类只反映"自然分群",但不反映业务目标)
for i in range(10):
km = KMeans(n_clusters=4, random_state=i)
labels_i = km.fit_predict(X_scaled)
# 算每个客户 10 次分到同一簇的比例
stable_ratio = (counts == 10).mean()
# > 90% 稳定, 簇结构可信
# < 70% 不稳定, 不能上线
| PSI 值 | 状态 | 动作 |
|---|---|---|
| < 0.1 | 稳定 | 继续监控 |
| 0.1 - 0.25 | 轻微漂移 | 观察 + 增加监控频率 |
| > 0.25 | 严重漂移 | 必须重训模型 |
| 层次 | 指标 | 判断标准 | 失败时怎么办 |
|---|---|---|---|
| 数学层 | Silhouette | > 0.5 | 换算法 (K-Means → HDBSCAN) |
| DBI | < 1.0 | 加特征 / 重新标准化 | |
| Inertia | 看拐点 | 减小 K (合并孤儿簇) | |
| 业务层 | 簇大小 | 每簇 ≥ 5% | 减小 K |
| 特征差异 | 簇间 ≥ 2x | 换 HDBSCAN / UMAP+HDBSCAN | |
| Churn 差异 | ≥ 1.5x | 聚类只反映自然分群,接 LR/XGBoost | |
| 上线层 | Bootstrapping 稳定性 | > 90% | 调 random_state 或换算法 |
| PSI 月度 | < 0.1 | 漂移 > 0.25 重训 |
| 检查项 | 我们的数据 | 判断 |
|---|---|---|
| Silhouette (K=2) | 0.4795 | ⚠️ 弱结构 (但 Churn 场景够用) |
| DBI (K=2) | 0.854 | ✅ 中等 |
| 簇大小 | 24-32% | ✅ 每簇 ≥ 5% |
| 特征差异 (Tenure) | 5.8x | ✅ 差异清晰 |
| Churn 率差异 | 5% vs 48% (9.6x) | ✅ 强信号 |
| Bootstrapping | 未跑 | ❓ 留作下一步 |
最后一句: 聚类是商业分析的"开胃菜",不是"主菜"。聚类告诉你"客户长什么样",LR/XGBoost 告诉你"客户会不会流失",两者结合才是完整的商业分析 pipeline。先聚类看群体,再预测看个体,最后 ROI 看效果。
完 · 2026-08-24 · Chase's Personal Page · 100% 本机真实运行