上个月我帮一家本地电商搭数据挖掘流程,老板前后抛了四类问题:预测一下明天的订单量大概是多少——这是回归;判断这个用户下一步会不会流失——这是分类;把几万活跃用户分成几个有特征的群体——这是聚类;购物车里的哪些商品经常一起出现——这是关联分析。同一个数据集,四类问题对应四种完全不同的模型,而 Python 生态几乎一口吃下了所有。
这也是我写这篇教程的初衷:很多人一上来就抱着 sklearn 文档啃,学了一堆算法名字,真拿到业务数据却不知道从哪一步开始。所以我打算按“回归 / 分类 / 聚类 / 关联分析”四条主线,把从环境准备到特征工程、从模型训练到结果评估的完整链路走一遍。文章里的代码我都实际跑过,用的都是常见库,你可以直接把里面的套路搬到自己项目里改一改就上。
1. 回归、分类、聚类、关联分析:先理清四个任务要回答什么问题
1.1 监督与无监督:数据挖掘任务的两大阵营
我在带新人时发现,很多人不是不会调包,而是根本分不清什么时候该用回归、什么时候该用聚类。其实判断标准很简单:看你的数据里有没有“标准答案”。
回归和分类属于监督学习,特征矩阵 X 旁边还站着一个标签列 y。就像老师批改作业,你手里有正确答案(标签),让模型照着学。区别只在于 y 的类型:y 是连续数值,比如房价、销量、温度,那就是回归;y 是离散类别,比如“流失 / 不流失”“垃圾邮件 / 正常邮件”,那就是分类。
聚类和关联分析属于无监督学习,数据里只有 X,没有 y,需要模型自己从数据中找规律。聚类是把相似样本自动归到一组,类似于你拿到一堆照片,没人告诉你谁是谁,但你可以按人脸相似度自然分成几堆;关联分析则是从大量事务记录里找“一起出现”的组合,典型的场景就是购物篮分析。
注意:无监督不是没有答案,而是答案需要事后由业务方来定义和验证。
1.2 四类任务对应的业务场景
面对一个实际问题,先要对号入座,选错方向后面全白做。我习惯用一张表来给需求归类:
| 任务类型 | 典型业务问题 | 示例算法 | 输出结果 |
|---|---|---|---|
| 回归 | 下个月销售额是多少? | 线性回归、随机森林回归、XGBoost 回归 | 连续数值 |
| 分类 | 客户会不会流失? | 逻辑回归、随机森林、XGBoost 分类 | 类别标签 + 概率 |
| 聚类 | 用户可以分为几类? | KMeans、层次聚类、DBSCAN | 每行样本的簇编号 |
| 关联分析 | 哪些商品会被一起买? | Apriori、FP-Growth | 规则 + 支持度/置信度/提升度 |
这张表看起来简单,但实际项目中任务混在一起的情况很常见。比如“预测用户未来一个月购买金额”是回归,可如果业务方只需要知道“高价值用户有哪些”,把金额按阈值切成“高 / 中 / 低”三档,就变成了分类。同一个数据源,问题定义方式不一样,模型选型和评估方式全都不一样。
1.3 一个典型的任务选错反例
有个朋友做过一个“用户流失预警”项目,刚开始他用 KMeans 把用户分了几类,然后看分类结果里有没有“流失”那一类。这其实是走错了方向:流失预警是监督学习,应该有历史标签,比如“过去 3 个月未下单的用户标记为流失”,再用这个标签训练分类模型。KMeans 聚类结果里确实可能聚出一个“低频用户簇”,但那个簇不等于流失用户,两者可能重叠但永远不能互相替代。
所以接到任何问题,先自问一句:我手里有没有一个现成的标签 y?有就做监督学习,没有才考虑聚类和关联。这个判断花不了 10 秒,却能避开后面大量无用功。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 跑通数据挖掘之前的准备:环境、数据清洗与特征工程
2.1 Python 环境与必备库
Python 数据挖掘的主流库,其实用一只手就能数过来:pandas 处理表格数据,numpy 做数值计算,scikit-learn 提供大部分经典算法,matplotlib + seaborn 做可视化,xgboost 用来上强度,mlxtend 专门做关联规则挖掘。
安装命令我直接给出来:
bash复制pip install pandas numpy scikit-learn matplotlib seaborn xgboost mlxtend
不建议一上来就装 Anaconda 全家桶。它确实方便,但体积大、环境冲突也麻烦。我是用 venv 或 conda 单独建一个项目环境,把依赖用 requirements.txt 固定住,这样换机器、换项目都不会互相干扰。装完之后顺手验证一下版本:
python复制import pandas as pd
import sklearn
import xgboost
print(pd.__version__)
print(sklearn.__version__)
print(xgboost.__version__)
版本差异是真实存在的坑。xgboost 的老接口 xgb.DMatrix 和新接口 XGBRegressor 混用时会出很多怪问题,我建议统一走 sklearn 风格的接口,参数名也跟 sklearn 对齐,学习成本最低。
2.2 数据加载与缺失值处理
拿到一份数据,第一步永远不是建模,而是 df.info() 和 df.describe()。这两行能告诉你数据量、列类型、缺失情况、数值分布范围。我常用的处理流程是这样:
python复制import pandas as pd
df = pd.read_csv('your_data.csv')
# 快速体检
print(df.shape)
print(df.info())
print(df.describe())
print(df.isnull().sum())
缺失值处理不是无脑填均值。如果某个字段缺失了 80% 以上,我倾向于直接删掉这一列,因为补出来的值大概率是噪音;缺失比例低于 5% 的,可以直接删除对应行;中等缺失比例、且字段本身有价值的,用中位数或众数填充。为什么用中位数而不是均值?因为均值对异常值敏感,比如“用户收入”字段,几个年入千万的样本会把均值拉高,中位数更稳健。
python复制# 数值列用中位数填充
df['income'] = df['income'].fillna(df['income'].median())
# 类别列用众数填充
df['channel'] = df['channel'].fillna(df['channel'].mode()[0])
get_dummies 做类别编码时有个小坑,我踩过好几次。pandas 默认会把一个 K 类别的列展开成 K 列 0/1 特征,但这会引入多重共线性,线性回归里会出问题。标准做法是 drop_first=True,只保留 K-1 列。
2.3 标准化与数据划分:顺序错了等于白干
标准化不是所有模型都需要。线性回归、逻辑回归、KMeans 这类基于距离或梯度的模型,对特征尺度敏感,必须做标准化;而决策树、随机森林、XGBoost 这类树模型,对特征尺度不敏感,不做标准化也可以。
真正容易翻车的是标准化和划分数据集的前后顺序。数据泄露这个词在后面专门细讲,这里先给结论:必须先切训练集 / 测试集,再在训练集上用 fit 计算均值和方差,然后用同一组参数去 transform 测试集。
python复制from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler
X = df.drop('target', axis=1)
y = df['target']
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
scaler = StandardScaler()
X_train_scaled = scaler.fit_transform(X_train)
X_test_scaled = scaler.transform(X_test)
stratify=y 是分类任务里的重要参数,它会让训练集和测试集的类别比例保持一致。如果不用,某一次划分可能把某个稀有类别全分到测试集,模型训练时根本没见到这个类别,测试时自然预测不准。
3. 回归实战:从线性回归到随机森林与 XGBoost
3.1 回归问题怎么定义:先举一个房价预测的例子
回归就是预测连续数值。我就用房价预测来说明整个流程。假设数据里有三个特征:房屋面积、房龄、楼层,目标是房价。为了演示,我构造一个小数据集。
python复制import pandas as pd
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LinearRegression
from sklearn.ensemble import RandomForestRegressor
from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score
df = pd.DataFrame({
'area': [50, 60, 70, 80, 90, 100, 110, 120, 65, 75, 85, 95, 105, 115, 130],
'age': [10, 8, 6, 5, 3, 2, 1, 1, 7, 5, 4, 3, 2, 1, 1],
'floor':[3, 5, 8, 12, 15, 18, 20, 22, 6, 9, 11, 14, 17, 19, 25],
'price':[95, 110, 130, 150, 175, 200, 230, 250, 120, 140, 160, 185, 210, 235, 270]
})
X = df[['area', 'age', 'floor']]
y = df['price']
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42)
真实项目当然不会只有 15 条样本,但作为演示,这个数据足够展示流程。实际使用时,数据量越大,树模型的优势越明显。
3.2 基线模型:线性回归与评估指标
第一个模型永远应该是最简单的。线性回归是理解回归问题的最好起点,它假设特征和目标之间近似线性关系。
python复制model = LinearRegression()
model.fit(X_train, y_train)
y_pred = model.predict(X_test)
print('MAE:', mean_absolute_error(y_test, y_pred))
print('RMSE:', mean_squared_error(y_test, y_pred, squared=False))
print('R2:', r2_score(y_test, y_pred))
评估指标刚开始不用贪多,记住四个就好:
| 指标 | 含义 | 特点 |
|---|---|---|
| MAE | 平均绝对误差 | 直观,单位跟目标一致 |
| MSE | 均方误差 | 大误差被放大 |
| RMSE | 均方误差开根号 | 单位还原,比 MSE 可读 |
| R² | 模型解释了多少方差 | 越接近 1 越好,但也可能过拟合 |
R² 是回归任务里我最先看的指标。它等于 0.9 代表模型解释了 90% 的数据波动,剩下 10% 是模型没抓住的部分。但如果 R² 从训练集的 0.99 掉到测试集的 0.6,说明模型在背答案,而不是学规律。
3.3 随机森林回归:当数据不再线性
线性回归的前提是线性关系。现实中房价和面积的关系未必是严格直线,面积超过某个阈值后价格可能涨得没那么快,这时候随机森林就派上用场了。随机森林是很多棵决策树的集成,每棵树在不同的样本子集和特征子集上训练,最后取平均。这个“平均”的动作就是它能压制过拟合的关键。
python复制rf = RandomForestRegressor(
n_estimators=200,
max_depth=6,
min_samples_leaf=2,
random_state=42
)
rf.fit(X_train, y_train)
y_pred_rf = rf.predict(X_test)
print('RF MAE:', mean_absolute_error(y_test, y_pred_rf))
print('RF R2:', r2_score(y_test, y_pred_rf))
# 特征重要性
importance = pd.Series(rf.feature_importances_, index=X.columns).sort_values(ascending=False)
print(importance)
参数初期不要乱调,先记三个:n_estimators 树的数量,太少容易不稳,太多训练变慢,200 左右起步;max_depth 限制树深,太深必过拟合;min_samples_leaf 叶子节点最少样本数,设成 2 或 5 能防止模型为了一条样本拟合出极端规则。
3.4 XGBoost 回归:结构化数据上的常胜将军
梯度提升树和随机森林的核心区别是:随机森林并行训练了很多独立的树,大家投票;XGBoost 是串行地一棵一棵训练,每棵新树都去拟合前面所有树的残差。这样逐轮纠错,拟合能力通常更强。
python复制from xgboost import XGBRegressor
xgb = XGBRegressor(
n_estimators=300,
max_depth=4,
learning_rate=0.05,
subsample=0.8,
colsample_bytree=0.8,
random_state=42
)
xgb.fit(X_train, y_train)
y_pred_xgb = xgb.predict(X_test)
print('XGB MAE:', mean_absolute_error(y_test, y_pred_xgb))
print('XGB R2:', r2_score(y_test, y_pred_xgb))
learning_rate(也叫 eta)是梯度提升模型的灵魂参数。学习率越低,每棵树贡献得越少,整体越稳健,但也需要更多树。我经常用 learning_rate=0.05 配 n_estimators=300,这个组合在很多表格数据上表现都比较稳。实际业务里,如果随机森林和 XGBoost 测试集效果差不多,我优先选随机森林,因为它参数更少、解释性更好。XGBoost 再强,也架不住你用错了特征。
3.5 回归模型的过拟合控制:交叉验证和网格搜索
单次划分训练集 / 测试集存在运气成分。我建议用交叉验证来粗评模型,再用网格搜索调参。
python复制from sklearn.model_selection import cross_val_score, GridSearchCV
import numpy as np
scores = cross_val_score(rf, X, y, cv=5, scoring='r2')
print('CV R2 mean:', np.mean(scores))
GridSearchCV 就是帮你在参数组合里暴力搜索出得分最高的一组:
python复制param_grid = {
'max_depth': [4, 6, 8],
'min_samples_leaf': [2, 4, 6]
}
grid = GridSearchCV(
RandomForestRegressor(n_estimators=200, random_state=42),
param_grid,
cv=5,
scoring='r2'
)
grid.fit(X_train, y_train)
print(grid.best_params_)
网格搜索很吃算力,参数组合数是指数级增长的,先用小范围粗搜,锁定最优区间再细搜。不要上来就放十个参数进去,那是给服务器上刑。
4. 分类实战:逻辑回归、随机森林与分类评估
4.1 分类和回归其实是同一套骨架
很多人觉得分类和回归是两个世界,其实它们的骨架完全一样:都是喂特征 X,拟合标签 y,区别只在最后一步。回归输出的是连续值;分类输出的是离散类别,且通常在输出前多一个概率化的映射。逻辑回归就是最典型的例子——它本质上还是线性回归,只是外面套了一层 sigmoid 函数,把任意实数压缩到 0 到 1 之间,变成“属于正类的概率”。
我用鸢尾花数据集演示一遍,因为它是 sklearn 内置的,不用额外下载。
python复制from sklearn.datasets import load_iris
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report, confusion_matrix
data = load_iris()
X, y = data.data, data.target
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.2, random_state=42, stratify=y
)
lr = LogisticRegression(max_iter=500)
lr.fit(X_train, y_train)
y_pred_lr = lr.predict(X_test)
print(classification_report(y_test, y_pred_lr))
print(confusion_matrix(y_test, y_pred_lr))
max_iter=500 是我加进去的,因为 sklearn 逻辑回归默认迭代次数设置的偏保守,数据一多经常警告“不收敛”。调大这个参数基本不会带来负面作用。
4.2 特征重要性:看懂随机森林在根据什么做决策
随机森林分类和随机森林回归在使用上几乎一模一样,只有损失函数不同。我建议把分类模型里的 predict_proba 用起来,它输出的是“这个样本属于每个类别的概率”,在业务场景里这比单纯的硬分类有信息得多。比如判断用户流失时,你有两个用户都被模型判为“流失”,但一个概率是 0.51,一个是 0.97,显然应该优先处理后者。
python复制rf_clf = RandomForestClassifier(
n_estimators=200,
max_depth=5,
min_samples_leaf=2,
random_state=42
)
rf_clf.fit(X_train, y_train)
y_prob = rf_clf.predict_proba(X_test)
print(rf_clf.feature_importances_)
print(classification_report(y_test, rf_clf.predict(X_test)))
特征重要性是树模型给业务方最好的解释材料。它直接告诉你“哪个字段在影响预测结果”,这在做信用卡风控、用户流失预警这些需要解释理由的场景里,比模型准确率重要得多。比如鸢尾花的特征重要性排出来,通常是花瓣长宽排最前,花萼长宽排最后,这跟植物学常识相符,模型不是在乱猜。
4.3 分类评估指标:准确率会骗人
分类任务的评估指标比回归复杂得多,因为存在“代价不对等”的问题。先看四个基础概念:
| 符号 | 含义 | 通俗解释 |
|---|---|---|
| TP | 把正类预测为正类 | 病人生病,模型说生病 |
| TN | 把负类预测为负类 | 健康人,模型说健康 |
| FP | 把负类预测为正类 | 健康人,模型说生病 |
| FN | 把正类预测为负类 | 病人生病,模型说健康 |
精确率 Precision = TP / (TP + FP),分母是模型所有预测为正类的样本,它回答的是“模型说是的,有多少真的是”;召回率 Recall = TP / (TP + FN),分母是真实所有正类样本,它回答的是“真正是的,模型找到了多少”。F1 是两者的调和平均。
我讲一个很现实的例子。假设 1000 个样本里只有 10 个正类,一个模型无脑全部预测为负类,准确率有 99%。从准确率看,这模型简直完美,但它一个正类都没找到,业务价值为零。所以在样本不平衡时,要盯住 F1 和召回率,而不是准确率。
医学诊断场景会更极端:漏诊一个病人(FN)的代价远大于误诊一个健康人(FP),所以宁可模型把一部分健康人误判成患者,也要把召回率拉高。这就是为什么评估指标必须跟着业务目标走。
4.4 类别不平衡处理:class_weight 和过采样
类别不平衡是分类实战里绕不开的坑。最简单的处理方式是给模型传递 class_weight='balanced',让模型在损失计算时自动给少数类更高的权重:
python复制lr_balanced = LogisticRegression(max_iter=500, class_weight='balanced')
如果还不够,再考虑过采样,比如用 imbalanced-learn 库的 SMOTE,对少数类做合成样本。但注意,SMOTE 必须在训练集上使用,绝对不能在整个数据集上做完再切训练测试集,否则会泄漏测试集信息,测试成绩虚高。
评估指标也要跟着调整。不平衡场景下,不要只看准确率,要同时看精确率、召回率、F1,以及 ROC AUC。ROC AUC 衡量的是模型对正负类排序能力,跟阈值无关,是看“好模型”还是“烂模型”的通用指标。
python复制from sklearn.metrics import roc_auc_score
# 二分类场景,需要 predict_proba 的正类概率
auc = roc_auc_score(y_true, y_pred_prob[:, 1])
print('AUC:', auc)
AUC 的直觉理解是:随机抽一个正类样本和一个负类样本,模型给正类打更高分的概率。0.5 等于瞎猜,0.9 以上就算相当能打。
5. 聚类实战:KMeans、层次聚类与 DBSCAN
5.1 聚类不是在分类,而是在“造标签”
聚类是没有标准答案的分组。它的典型场景是用户分群:你有几十个用户行为特征,但不知道用户分几类,想让模型自动找出结构。我用一个生成的人工数据集来演示三种主流聚类算法,这样能看到它们在不同数据形态下的表现。
python复制from sklearn.datasets import make_blobs
from sklearn.cluster import KMeans, AgglomerativeClustering, DBSCAN
from sklearn.metrics import silhouette_score
from sklearn.preprocessing import StandardScaler
X_blob, _ = make_blobs(
n_samples=300,
centers=4,
cluster_std=0.8,
random_state=42
)
scaler = StandardScaler()
X_scaled = scaler.fit_transform(X_blob)
做了标准化再聚类,这一步非常重要。KMeans 用欧氏距离衡量相似度,如果“年龄”这个字段的取值范围是 0 到 100,“收入”的范围是 0 到 100000,收入会完全主导距离计算,年龄的影响几乎被淹没。标准化把每个特征拉到同一个尺度,各特征才有公平发言权。
5.2 KMeans 与 K 值选择:肘部法和轮廓系数
KMeans 的原理一句话能说清楚:先随机选 K 个中心点,然后把每个样本分到离它最近的中心点,再更新中心点为簇内均值,如此反复直到中心点不再变化。它的核心参数是 K,也就是“分几组”。但 K 通常是未知的,我用两个指标来辅助判断。
第一个是肘部法,看簇内误差平方和(inertia)随 K 变化的曲线。
python复制import matplotlib.pyplot as plt
inertias = []
for k in range(1, 11):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
km.fit(X_scaled)
inertias.append(km.inertia_)
plt.plot(range(1, 11), inertias, marker='o')
plt.xlabel('K')
plt.ylabel('Inertia')
plt.show()
inertia 会随着 K 增大而单调下降,因为簇越多,样本离簇中心越近。我们要找的是曲线拐点,也就是“看起来像手肘”的位置,K 超过这个点之后,再增加簇数对误差的改善越来越小。
第二个是轮廓系数,它同时衡量簇内紧密度和簇间分离度,范围从 -1 到 1,越大越好。
python复制from sklearn.metrics import silhouette_score
for k in range(2, 11):
km = KMeans(n_clusters=k, random_state=42, n_init=10)
labels = km.fit_predict(X_scaled)
score = silhouette_score(X_scaled, labels)
print(f'K={k}, silhouette={score:.3f}')
两个指标要配合着看。肘部法告诉你在哪里边际收益递减,轮廓系数告诉你分组的紧致程度高不高。如果某个 K 值下轮廓系数特别低,说明数据本身没有清晰分离成那么多簇。
python复制km = KMeans(n_clusters=4, random_state=42, n_init=10)
km_labels = km.fit_predict(X_scaled)
print(sorted(set(km_labels)))
n_init 是 KMeans 里容易被忽略的参数。默认值是 10,意思是算法会从 10 个不同初始中心点出发跑 10 次,选结果最好的那一次。因为 KMeans 的初始中心点是随机的,可能收敛到局部最优。我在正式实验里一般保留默认或者设成 20,但要注意训练时间的增加。
5.3 层次聚类:不需要提前定 K 的另一个选择
层次聚类跟 KMeans 的思路完全不同:它一开始把每个样本当成一个簇,然后不断合并距离最近的两个簇,直到所有样本都合并成一个簇。这个过程会生成一个树状结构,叫谱系图或树状图。K 的选择是在这棵树上“切一刀”,切得越深,得到的簇越多。
python复制from scipy.cluster.hierarchy import dendrogram, linkage
from scipy.cluster.hierarchy import fcluster
Z = linkage(X_scaled, method='ward')
# 画谱系图
dendrogram(Z)
plt.show()
# 根据谱系图,距离阈值约 8 时切出 4 个簇
labels_hier = fcluster(Z, t=8, criterion='distance')
热词里的“两步聚类”也值得一说。两步聚类是 SPSS 里很常用的聚类方法,第一步用类似 BIRCH 的算法把样本预聚类成很多微簇,第二步再对微簇做层次聚类。它的好处是能处理远大于普通层次聚类数据量的场景。其实质就是一种“先用粗聚类把数据量降下来,再做精细层次聚类”的工程优化。
我在实际项目里用层次聚类不多,因为它的计算成本是 O(n²) 量级,样本上万就很吃力。但它有个独特优势:能输出谱系图,业务方可以直观看到“在多大相似度下,用户被合并到一起”,解释性比 KMeans 强。
5.4 DBSCAN:处理任意形状簇和离群点
KMeans 有个默认假设:簇是球形的。如果数据是两个嵌套的圆环,KMeans 会硬生生把圆环从中间劈开,结果完全错误。这种情况我会换 DBSCAN,它的核心思想是“密度连通”:一个样本周围如果足够密集,就不断向外扩展,把连通的密集区域划成一个簇,稀疏区域的样本则被标记为噪声。
python复制db = DBSCAN(eps=0.4, min_samples=10)
db_labels = db.fit_predict(X_scaled)
# -1 是噪声点
print(set(db_labels))
DBSCAN 的两个参数很关键:eps 是邻域半径,min_samples 是成为核心点所需的最少邻居数。eps 设太小会把一个簇拆成很多碎块,设太大又会把多个簇合并成一个;min_samples 则控制对噪声的容忍度,越大噪声越多。它不需要指定 K,这是巨大的优势,但调参敏感性更高,需要结合可视化逐组试。
三种聚类算法的选择,我总结成一个表格:
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| KMeans | 快、简单、适合大规模 | 必须指定 K、假设球形簇、对噪声敏感 | 用户分群、图压缩 |
| 层次聚类 | 输出树状结构,无需提前定 K | 数据量大时慢 | 小样本、需要解释 |
| DBSCAN | 自动发现任意形状簇、能识别噪声 | 高维距离退化、eps 难调 | 地理分布、异常检测 |
5.5 聚类落地时的业务解读:指标不是终点
聚类任务没有标签,所以要验证“分得好不好”,除了轮廓系数这类内部指标,更重要是看业务上能不能解释。我做过一次用户分群,KMeans 分出的某个簇,内部平均客单价是整体均值的 3 倍,行为特征上都指向“高活跃高消费”,这个簇我才会在后续运营里单独对待。如果某个簇的特征就是“什么都比其他人低一点”,那大概率是没分好,或者数据里没有足够的信息支撑更细的分群。
记住一句话:聚类结果需要业务方点头,而不是算法自己点头。
6. 关联分析实战:用 Apriori 算法挖掘购物篮规则
6.1 支持度、置信度与提升度:三个指标先弄明白
关联分析最经典的场景是超市购物篮。假设我们有几笔交易记录,每笔记录是顾客买的一堆商品,Apriori 算法要回答的问题是:哪些商品组合会频繁一起出现?
三个核心指标必须吃透,不然刷出几百条规则你根本不知道怎么筛。
| 指标 | 公式 | 业务含义 |
|---|---|---|
| 支持度 Support | 包含该商品集的订单数 / 总订单数 | 这个组合有多普遍 |
| 置信度 Confidence | 包含 A 和 B 的订单数 / 包含 A 的订单数 | 买 A 的人有多大比例也买 B |
| 提升度 Lift | Confidence(A→B) / Support(B) | 买 A 对买 B 是促进、抑制还是无关 |
我举个具体例子。超市有 1000 笔订单,其中 100 笔同时买了牛奶和面包,单独买牛奶的订单有 200 笔。那么“牛奶→面包”的支持度是 100/1000 = 0.1,置信度是 100/200 = 0.5。如果全店面包的购买率是 0.3,提升度就是 0.5 / 0.3 ≈ 1.67。大于 1 说明牛奶和面包正相关,买牛奶的人比平均人群更可能买面包;小于 1 说明两者互相抑制;等于 1 说明彼此独立。
6.2 上手 mlxtend:构造事务数据与 Apriori
mlxtend 的 Apriori 接口设计得很友好。先准备一份事务型数据,每一行是一笔订单的商品列表:
python复制from mlxtend.frequent_patterns import apriori, association_rules
from mlxtend.preprocessing import TransactionEncoder
import pandas as pd
dataset = [
['牛奶', '面包', '黄油'],
['牛奶', '面包'],
['啤酒', '尿布'],
['牛奶', '尿布', '面包', '啤酒'],
['面包', '黄油'],
['牛奶', '啤酒', '尿布'],
['面包', '啤酒'],
['牛奶', '面包', '啤酒'],
]
Apriori 算法不接受原始的列表形态,我需要先用 TransactionEncoder 把它转成“每个商品一列、每笔订单一行”的 0/1 矩阵:
python复制te = TransactionEncoder()
te_ary = te.fit(dataset).transform(dataset)
df_trans = pd.DataFrame(te_ary, columns=te.columns_)
print(df_trans)
转换后的表格长这样:行是订单,列是商品,有购买为 True,没购买为 False。
python复制frequent_itemsets = apriori(df_trans, min_support=0.3, use_colnames=True)
print(frequent_itemsets)
min_support=0.3 表示只保留至少在 30% 订单里出现的商品组合。支持度阈值是关联分析里最重要的旋钮:设太低,会生成大量低频组合,规则又臭又长;设太高,可能一个频繁项集都找不出来。我一般先从 0.1 或 0.2 起步,看频繁项集数量再调整,目标是让结果控制在几十条以内。
生成规则时用 metric='lift' 来排序筛选:
python复制rules = association_rules(frequent_itemsets, metric='lift', min_threshold=1.2)
rules = rules.sort_values('lift', ascending=False)
print(rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']])
6.3 规则解读:怎么从表格里挑出有用信息
假设跑出来的规则里有一条:{牛奶, 面包} → {啤酒},支持度 0.3,置信度 0.6,提升度 1.8。业务上的解读是:同时买牛奶和面包的人里,有 60% 还会买啤酒;这个概率是全站啤酒购买率的 1.8 倍。
我筛选规则有个习惯:先看提升度,只保留明显大于 1 的;再看置信度,过滤掉那些买了 A 但只有不到一半概率买 B 的弱规则;最后看支持度,确保规则覆盖的订单量足够,避免为了一两笔订单去设计捆绑陈列。
Apriori 的规则是有方向性的。{啤酒}→{尿布} 和 {尿布}→{啤酒} 的置信度可能完全不同,要看业务上谁适合当前导品、谁适合被推荐。
6.4 关联分析常见误区:频繁共现不等于因果关系
关联分析在电商类目配置、商品陈列、交叉销售里很常用,但它最容易犯的错是:把“一起出现”当成“因为 A 所以 B”。牛奶和面包经常一起出现,可能是因为它们本来就被放在超市的同一个区域,顾客顺路一起拿,不是因为“买了牛奶就更想买面包”。算法不会分辨因果和相关性,需要你结合业务判断。
还有一个操作层面的坑是数据稀疏。真实电商订单里,绝大多数商品的出现频率非常低,min_support 设得小一点就容易出现几百条只覆盖 5 笔订单的规则。这类规则在统计上没有意义,在业务上更是误导。我处理时通常会加一道后置过滤,把支持度和置信度都设下限,或者要求规则的 antecedents 里的商品是重点品类,而不是全部品类。
7. 数据挖掘最容易翻车的五个细节:数据泄露、随机种子与评估偏差
7.1 数据泄露:最隐蔽也最致命的错误
数据泄露是指模型在训练阶段接触到了测试集的信息,导致测试评估虚高,上线后实际效果崩盘。它不一定是恶意行为,更多是操作顺序不对导致的。
举几个我见到的真实案例:
- 先用全量数据的均值填充缺失值,再切训练/测试集。这样测试集的缺失值其实已经用包含它自身信息统计出来的值填充了,等于模型提前看到了测试集。
- 先对全量数据做标准化或归一化,再切分数据集。跟上面同理,测试集的信息混进了训练阶段。
- 先用全量数据跑一遍特征选择,选出 top 特征,再在训练集上训练模型。特征选择的过程也被动利用了测试集信息。
- 用
df.dropna()前没有检查行索引是否是对应测试集样本。
原则很简单:任何基于数据统计的预处理步骤,都必须在切分之后、只基于训练集进行。如果你觉得手动写太容易出错,就用 sklearn 的 Pipeline,把预处理和模型打包成一个完整流程,让它在交叉验证的每一折里自动只学习对应训练折的信息。
python复制from sklearn.pipeline import Pipeline
pipe = Pipeline([
('scaler', StandardScaler()),
('clf', RandomForestClassifier(random_state=42))
])
# 交叉验证会自动在每个折内先 fit scaler,再 fit 模型
scores = cross_val_score(pipe, X, y, cv=5)
7.2 随机种子与可复现性
我每次做实验都会在代码里写上 random_state=42。这个数字没什么神秘,就是一个固定的随机种子,让随机过程变得可复现。如果不设,同一个模型跑两次,得到的结果可能不一样,你很难判断改进到底是模型改动带来的,还是单纯运气好。
数据集划分、KMeans 初始化、随机森林的样本抽样、XGBoost 的列抽样,这些环节都有随机性。项目里我会在全局设置一次 numpy 和 sklearn 的随机种子:
python复制import numpy as np
np.random.seed(42)
但要提醒一句:随机种子只能让单机实验可复现。当任务分布到多进程或多台机器时,每个进程的随机状态很难完全同步,这是另一个层面的问题,目前没有银弹。
7.3 评估指标与业务目标不一致
选错评估指标的坑,前面分类部分已经提过,这里再强调一遍。回归任务不能只看 R²。R² 对异常值敏感吗?其实不直接体现,但 RMSE 因为对误差平方加权,会放大异常样本的影响。如果你的业务更关心“普通用户预测得准不准”,而不是“极端值别差太远”,MAE 可能比 RMSE 更贴合。
分类任务里,如果业务方关心的是“能不能在有限预算内圈出尽量多的潜在流失用户”,那么召回率优先级最高,因为漏掉的用户才是损失。如果关心的是“电话外呼的名单千万别浪费,打过去的人流失概率要高”,那精确率优先级最高。所以建模前一定先问清楚:模型结果给谁用?他做决策时最怕哪种错误?
7.4 训练和预测之间的特征一致性
模型上线后效果突然变差,最常见的不是算法崩了,而是特征没对齐。训练时的特征列顺序是 ['area', 'age', 'floor'],线上传进来的 DataFrame 列顺序是 ['floor', 'area', 'age'],模型内部其实已经乱了。更隐蔽的是,类别编码时训练集里有 5 个城市,线上只出现 4 个,one-hot 拼出来的特征矩阵列数跟训练时不一致。
我的防御手段很简单:
- 训练完成后把
model.feature_names_in_或 fit 时的列名存成 pickle,预测前做一次列名对齐。 - 用 Pipeline 把预处理和模型打包,线上直接调用同一个 Pipeline,而不是分别调用标准化器和模型。
- 类别字段用 sklearn 的
OneHotEncoder(handle_unknown='ignore'),这样即使线上出现训练集没见过的新类别,也能正常生成特征,而不是直接崩掉。
7.5 什么时候该退回简单模型:先跑通再谈进阶
我最后想分享一条经验:做数据挖掘项目,永远先跑通基线,再谈进阶。线性回归和逻辑回归简单,但简单有简单的优势——训练快、参数少、可解释、不容易过拟合。先拿它们跑一遍全流程,把数据清洗、特征工程、评估体系建立好,再做随机森林和 XGBoost 的对比。如果树模型相对线性模型的提升不到 5%,而业务方又需要解释模型,那么我会更倾向用简单模型。
我自己做项目的固定流程是:固定随机种子 → 切分数据 → 跑一个简单基线模型 → 建立评估指标 → 逐步引入树模型 → 用交叉验证和网格搜索调参 → 最后再看特征重要性,回头补特征工程。顺序一旦固定,很多低级错误就不会再犯。你还想在这个流程里加上聚类、关联分析,思路也是一样的:先想清楚任务类型,再选算法,最后用业务语言解释结果。
