关联规则:啤酒与尿布的数学
前两课都在给人"贴标签、分堆",这一课换个目标:找出经常一起出现的东西。学完它,你能手算支持度、置信度、提升度三个指标,还能亲手写出一个二十几行的迷你 Apriori——不需要装任何新库。
先记住一句话:支持度管多常见,置信度管多可靠,提升度管是不是巧合。
一、啤酒与尿布
零售业最著名的都市传说:超市发现买尿布的人经常顺手买啤酒(大概是当爹的半夜哄娃犒劳自己),于是把两样货摆到一起,双双销量上涨。故事细节真假存疑,但它示范的思路千真万确,并有一个正式的名字——购物篮分析:
text
拿起一把小票 → 找出"总是出双入对"的商品组合 → 摆货架 / 搭套餐 / 做推荐这个思路远不止卖货:第一课日志里的扫描器也有它的"购物篮"——/.env、/admin、/wp-login 总是在同一个 IP 的请求里一起出现。"经常一起出现"这个模式,货架上能用,风控里也能用。
二、三个指标:全部手算一遍
光说"一起出现"太含糊,三个指标把它量化。先给一份 5 张购物小票的数据:
text
小票1:{尿布, 啤酒, 薯片}
小票2:{尿布, 啤酒}
小票3:{尿布, 牛奶}
小票4:{牛奶, 面包}
小票5:{尿布, 啤酒, 牛奶}我们关心规则"买尿布 → 买啤酒"(读作:买了尿布的人,有多大概率也买了啤酒):
| 指标 | 定义 | 手算 | 含义 |
|---|---|---|---|
| 支持度 Support | 同时含 A、B 的小票占比 | 3/5 = 0.6 | 这对组合多常见 |
| 置信度 Confidence | 买 A 的人里也买了 B 的比例 | (3/5) ÷ (4/5) = 0.75 | 这条规则多可靠 |
| 提升度 Lift | 置信度 ÷ B 的支持度 | 0.75 ÷ (3/5) = 1.25 | 买 A 有没有让买 B 更可能 |
置信度 0.75 看着挺高,但请警惕一个反问:啤酒本来就畅销(支持度 0.6),就算尿布和啤酒毫无关系,随便抓个买尿布的人也有六成概率买了啤酒。提升度就是用来戳破这种"假关系"的:
- Lift > 1:正相关,买 A 确实让 B 更可能出现——值得摆一起
- Lift = 1:相互独立,纯属巧合,别浪费货架
- Lift < 1:负相关,买了 A 反而更不买 B——这俩别捆在一起卖
🧠 记忆锚点:支持度管多常见,置信度管多可靠,提升度管是不是巧合。 一条好规则三者缺一不可:常见、可靠、且不是巧合。
三、Apriori:怎么不把组合数爆掉
商品一多,组合就是天文数字:100 种商品的两两组合就有 4950 种,三件套更上万。Apriori 算法用一条性质剪掉绝大多数组合:
🧠 记忆锚点:频繁项集的子集必定频繁。 反过来用才厉害——一个组合(比如 {啤酒, 薯片})自己都不常出现,那么任何包含它的更大组合({啤酒, 薯片, X})也绝不可能频繁,全体不用看了。
于是流程变成一层一层盖楼:先数 1 件组合,砍掉不频繁的;只拿幸存者两两拼成 2 件组合,再砍;再拼 3 件的……每一层都在上一层幸存者的基础上盖,楼越高人越少。
四、动手:手写迷你 Apriori
不装任何库,二十几行写一个能跑的(数据就是第二节的 5 张小票)。新建 apriori.py 整段复制运行:
python
from itertools import combinations
baskets = [
{"尿布", "啤酒", "薯片"},
{"尿布", "啤酒"},
{"尿布", "牛奶"},
{"牛奶", "面包"},
{"尿布", "啤酒", "牛奶"},
]
N = len(baskets)
min_support = 0.4 # 支持度门槛:至少 40% 的小票里出现才算频繁
def support(itemset): # 一个项集的支持度
hits = sum(1 for b in baskets if itemset <= b) # <= 是集合的"子集"判断
return hits / N
def frequent_itemsets(k_max=3):
"""逐层盖楼:第 1 层是单个商品,往上只用幸存者拼更大的组合"""
level = {frozenset([i]) for b in baskets for i in b}
level = {s for s in level if support(s) >= min_support} # 砍掉不频繁的
all_layers = [level]
for k in range(2, k_max + 1):
# 只用上一层幸存者两两拼接(拼接处共享 k-2 个元素,避免拼出废话)
cands = {a | b for a in level for b in level if len(a & b) == k - 2}
level = {c for c in cands if support(c) >= min_support} # 再砍
if not level:
break
all_layers.append(level)
return all_layers
print("— 频繁项集 —")
for layer in frequent_itemsets():
for s in layer:
print(f" {set(s)} 支持度={support(s):.2f}")
print("— 关联规则 —")
rules = []
for layer in frequent_itemsets():
for s in layer:
if len(s) < 2:
continue
for i in s: # 每种拆法都当一条规则
left, right = s - {i}, frozenset([i]) # 前件 → 后件
conf = support(s) / support(left)
lift = conf / support(right)
rules.append((set(left), set(right), support(s), conf, lift))
for left, right, sup, conf, lift in sorted(rules, key=lambda r: -r[4]):
print(f" {left} → {right} 支持度={sup:.2f} 置信度={conf:.2f} 提升度={lift:.2f}")对照输出,值得停留的三行:
- {啤酒} → {尿布},置信度 1.00:买啤酒的小票里 100% 都有尿布——因为数据太少,样本里碰巧如此
- {尿布} → {啤酒},提升度 1.25:正相关,真规则,可以摆一起
- {尿布} → {牛奶},提升度 0.83:小于 1!买尿布的人反而更不买牛奶——置信度 0.50 看着还行,提升度一票否决。只看置信度会被这种规则骗得很惨
这正是第五节的第 1 个坑的现场版。
五、用规则时的三个坑
- 相关不等于因果。"买尿布的买啤酒"不等于"尿布导致了啤酒",更可能是同一个隐藏原因(新手爸爸这一人群)同时驱动了两者。摆货架无所谓,做决策要想清楚因果
- 小样本虚高。5 张小票里置信度 1.00 的规则毫无意义;规则必须建立在足够多的记录上,看置信度时永远带着样本量一起看
- 业务解读永远是最后一步。算法只输出"一起出现",摆不摆到一起、捆不捆着卖,是货架成本、毛利、库存在说话——挖掘给弹药,业务扣扳机
六、动手实验
💡 动手实验 1:把
min_support从 0.4 依次改成 0.6、0.2,数一数频繁项集变多还是变少、各出现什么。门槛太严颗粒无收,太松垃圾成山——支持度门槛就是挖掘的"灵敏度旋钮"。
💡 动手实验 2:加一行
if conf < 0.5: continue只保留置信度 0.5 以上的规则;再把输出按置信度排一次序(把排序 key 换成r[3])。体会一下:按置信度排和按提升度排,榜首可能完全不同——你更信哪个排序?为什么?
💡 动手实验 3:编 8~10 张你自己的小票(比如奶茶店:{珍珠奶茶, 蛋糕}, {柠檬茶, 鸡排}……),跑同一个脚本,找出提升度最高的一条规则,并用一句话说明你打算怎么用这个发现。
小结
- 购物篮分析 = 找"经常一起出现"的组合;货架、套餐、推荐、日志风控里都在用
- 三个指标:支持度管多常见,置信度管多可靠,提升度管是不是巧合;Lift ≤ 1 的规则不值得投入
- Apriori 靠"频繁项集的子集必定频繁"逐层剪枝,组合数从天文数字变成可算
- 相关不等于因果;小样本的置信度虚高;挖掘给弹药,业务扣扳机
五颗钉子都敲完了:流程、清洗、分类聚类、关联规则。最后一课把它们全部串起来,做一次完整的实战——奶茶店会员流失分析。