前几天有个做电商运营的朋友跑来问我,他每次拉商品销售数据,总会看到“婴儿纸尿裤”和“啤酒”出现在同一张小票里,问我是不是这两个品类之间有什么隐藏规律。他这个问题,恰好就是关联规则算法最典型的应用场景——从一堆交易记录里找出“买了A的人,同时还买了什么”的共现模式。这篇总结是我在实际项目里反复用这套算法之后沉淀下来的笔记,覆盖指标定义、三种经典算法、完整建模流程,以及一堆踩过才知道的坑,适合刚接触数据挖掘的读者,也适合已经跑通流程但想优化结果的人。
1. 先把问题讲明白:关联规则到底在挖什么
1.1 从一次业务对话说起
那位电商朋友关心的是“规律”,但业务上真正的问题通常是三类:要不要做捆绑推荐、货架或页面怎么摆放、优惠券该给哪些组合。关联规则算法就是把这类问题转成一组可量化的“如果……那么……”句子。
一条关联规则长这样:
{啤酒} → {纸尿裤}
它读作“买啤酒的顾客,也倾向于买纸尿裤”。但光有这句话没有意义,必须在后面附上三个数字:支持度(Support)、置信度(Confidence)、提升度(Lift)。这三个数字回答的是三个完全不同的业务问题:
- 支持度回答“这个组合常见吗”;
- 置信度回答“买了A的人里,有多少人真的买了B”;
- 提升度回答“A和B同时出现,比各自独立出现时强了多少”。
很多人只盯着规则本身,不看这三个指标,这是最容易犯的错误。一条“啤酒→纸尿裤”的规则,如果支持度只有0.001,说明几天才出现一次,再强也不能用于日常运营;如果置信度只有0.05,说明绝大多数买啤酒的人并不买纸尿裤,放一起推荐很可能被用户无视。
1.2 它和推荐系统、因果分析有什么区别
关联规则算法经常被拿来和协同过滤、因果推断混为一谈,实际上三者解决的问题完全不同。
协同过滤做的是“评分预测”或“排序”,它回答“这个用户可能喜欢哪个商品”的问题,输出的是Top N列表。关联规则是纯粹的无监督规则挖掘,它不预测未知行为,只描述已知数据里的共现规律,输出的是规则集合。
因果推断回答的是“如果把A放到用户面前,他会不会因此多买B”的问题,需要实验设计、干预分析。关联规则只能告诉你“A和B一起出现过”,不能告诉你两者是否有因果链。比如生日当天“生日蛋糕”和“蜡烛”总是同时出现,但把蜡烛摆在蛋糕旁边,并不会让用户多买一根蜡烛,真正驱动购买的是“过生日”这个事件。
理解了这三者的边界,就不会拿着关联规则的结果去做因果承诺,也能更清楚在什么阶段用这套算法最合适。
2. 核心指标:选阈值前先把定义吃透
2.1 支持度、置信度、提升度到底怎么算
用一个微型例子说明。假设一个只有五张小票的数据集:
| 小票编号 | 商品 |
|---|---|
| T1 | 牛奶, 面包, 啤酒 |
| T2 | 啤酒, 纸尿裤 |
| T3 | 牛奶, 纸尿裤 |
| T4 | 牛奶, 面包, 纸尿裤 |
| T5 | 啤酒, 面包 |
先看规则 {啤酒} → {纸尿裤}。
支持度 Support = P(啤酒 且 纸尿裤) = 同时包含这两个商品的单据数 / 总单据数 = 1 / 5 = 0.2。意思是整个数据集里,有20%的订单同时买了这两样东西。
置信度 Confidence = P(纸尿裤 | 啤酒) = 同时包含两个商品的单据数 / 包含啤酒的单据数 = 1 / 2 = 0.5。意思是所有买啤酒的订单里,一半也买了纸尿裤。
提升度 Lift = Confidence / P(纸尿裤) = 0.5 / (2/5) = 0.5 / 0.4 = 1.25。意思是“买了啤酒之后再买纸尿裤”的概率,比“不看前提随机买纸尿裤”的概率高了25%。
看到这里你应该明白一个关键逻辑:置信度受后项本身的热度影响。如果纸尿裤本身出现在80%的订单里,那么即使没有啤酒,它也有80%的概率被买走,啤酒这条前件对后件的边际贡献就很小。提升度正是用来剥离这种“本来就很热门”的干扰,它可以小于1,等于1,也可以大于1。
2.2 容易被忽视的辅助指标
除了这三个主指标,我会在实际项目中额外看几个辅助指标,用来避开一些陷阱。
杠杆率 Leverage = P(A 且 B) - P(A) × P(B),衡量的是关联强度和独立假设的绝对差值。它和提升度类似,但用差值而不是比值,对于支持度极低却提升度极高的噪声规则,杠杆率能帮忙拉回一档判断。
确信度 Conviction = (1 - P(B)) / (1 - Confidence),含义是“如果A和B完全独立,要出现多少倍的违反才可能达到当前观察值”。确信度越大,规则被误判为随机的可能性越低。
另外还有一个非常实用的习惯:同时看前件支持度和后件支持度。一条规则看上去提升度高达10,但如果前件只出现在0.1%的单子里,业务落地的价值就非常有限。相反,后件支持度很高的规则,即使提升度只有1.1,也可能因为绝对覆盖人数大而有实际收益。
2.3 阈值选择的实操建议
阈值本身没有标准答案,但有一套我反复使用的起步策略。
最小支持度 min_support 一般从0.01开始试。跑出来频繁项集如果只有十几个,说明阈值太高,规则会非常贫乏;如果频繁项集达到一两万条,说明阈值太低,后续规则会爆炸。通常我会把频繁项集数量控制在50到500条之间,再回头看业务上是否合理。
最小置信度 min_confidence 常在0.5到0.7之间试。对医疗、风控这类不允许太多误报的场景,我会把置信度提到0.8甚至0.9;但对零售推荐这类希望覆盖更多用户的场景,可以放宽到0.3,因为“买A后买B”的概率本来就不会太高。
最小提升度 min_lift 至少要大于1,实际使用通常从1.1或1.2起步。我见过有人把提升度门槛设到3以上,结果一条规则都出不了——高提升度往往集中在小众商品组合上,覆盖面很窄。阈值必须和业务目标联动,而不是机械套用某个数。
3. 三种经典算法:原理、实现与对比
3.1 Apriori:先剪枝再计数
Apriori是关联规则里最经典、也是教学上最常讲的算法。它的核心思想基于一条先验性质:如果一个项集是频繁的,那么它的所有子集一定频繁;反过来,如果一个项集不是频繁的,那么它的任何超集都不会频繁。这听起来有点像废话,但正是这条性质让算法可以大幅剪枝。
算法从所有单个商品开始,先筛出满足最小支持度的1项集,再组合出2项集候选,然后用先验性质剪掉那些含有非频繁子集的候选,接着扫描数据库计算候选的支持度,保留满足条件的项集,再继续生成3项集,一轮一轮往上迭代,直到没有新的频繁项集产生。
我用一段简化Python代码演示核心逻辑:
from itertools import combinations from collections import Counter def get_frequent_itemsets(transactions, min_support): n = len(transactions) # 生成频繁1项集 item_counts = Counter(item for t in transactions for item in t) freq = { frozenset([item]): cnt / n for item, cnt in item_counts.items() if cnt / n >= min_support } all_freq = dict(freq) k = 2 while True: # 先生成所有k项候选,再剪掉含非频繁子集的候选 candidates = { a | b for a in freq for b in freq if len(a | b) == k } candidates = { c for c in candidates if all((c - {item}) in freq for item in c) } # 扫描数据库统计候选支持度 counts = Counter( c for t in transactions for c in candidates if c.issubset(t) ) freq = { c: cnt / n for c, cnt in counts.items() if cnt / n >= min_support } if not freq: break all_freq.update(freq) k += 1 return all_freq这里的剪枝关键在 candidates 生成之后,用all((c - {item}) in freq for item in c)判断:一个k项候选,去掉任意一个商品后,剩下的k-1项集必须是刚才保留的频繁项集。只要有一个子集不频繁,这个候选就丢掉,不参与数据库扫描。
Apriori的缺点也很明显:每一轮迭代都要完整扫描一次数据库,而且候选集的数量随项集长度爆炸式增长。我在样本量超过几十万的小票数据上跑Apriori,内存和耗时都比较吃力。它适合作为理解原理的起点,或者数据规模不大时的快速验证工具。
3.2 FP-Growth:一棵树省掉反复扫描
FP-Growth(Frequent Pattern Growth)是Apriori的升级版,核心思路是只扫两遍数据库,把全部事务压缩进一棵前缀树,然后在树上递归挖掘,不再反复扫全量数据。
第一遍扫描统计每个商品的频次,筛掉不满足最小支持度的商品,剩下的按频次降序排列。第二遍扫描逐条事务,把事务里的商品按这个全局顺序排序,然后插入到以根节点为起点的前缀树里,每个节点记录商品名和计数。这棵树叫FP树。
挖掘过程不是直接在树上穷举,而是对每个频繁商品找出它的条件模式基,也就是所有包含该商品的前缀路径,递归构建条件FP树。这个过程不需要生成候选集,很多Apriori里没有意义的中间组合根本不会出现。
用现成库实现FP-Growth非常简洁,比如用mlxtend:
import pandas as pd from mlxtend.frequent_patterns import fpgrowth # df_onehot 是事务数据,每行一个订单,每列一个商品,值为 0/1 frequent_itemsets = fpgrowth( df_onehot, min_support=0.02, use_colnames=True, )我在跑几万个小票数据时,FP-Growth的速度优势非常明显,尤其在频繁项集比较长的情况下,Apriori已经跑到怀疑人生,FP-Growth还是稳稳的。项目里只要数据量稍微上来一点,我都直接用FP-Growth替代Apriori。
3.3 ECLAT:从行视角换成列视角
ECLAT(Equivalence CLAss Transformation)是第三种思路。Apriori和FP-Growth都基于水平数据集,也就是一行一个事务;ECLAT换成垂直数据集,每一列对应一个商品,列里的值是这个商品出现在哪些事务编号里,专业叫TID列表。
频繁项集的计算方式也从“扫描事务统计支持度”变成“TID列表取交集”。比如商品A出现在事务{1,2,3},商品B出现在事务{2,3,5},那么{A,B}的支持度就是交集{2,3}的大小除以总事务数,直接数集合元素个数就行,不需要再扫数据库。
这种做法的优势是,当商品维度很多、事务数相对少时,TID集合取交集非常快。缺点是TID列表本身可能很占内存,尤其商品出现在大量事务里时。
三个算法的选型,我一般按这几条经验判断:
| 算法 | 数据扫描 | 内存占用 | 适用场景 |
|---|---|---|---|
| Apriori | 多次全量扫描 | 候选集多时很大 | 小数据、教学验证 |
| FP-Growth | 两遍扫描 | 树结构较紧凑 | 海量事务、频繁项集较长 |
| ECLAT | 交集运算 | TID列表开销大 | 事务少、商品维度高的稠密数据 |
多数业务场景,FP-Growth是第一选择。事务数少到只有几千时,Apriori也可以接受。ECLAT更适合特定数据结构,比如基因表达、文本关键词矩阵这种行列关系相对紧密的数据。
4. 从原始数据到业务规则:完整实操流程
4.1 数据准备:影响结果的前置处理
关联规则对数据格式要求很严格,大多数“跑不出规则”或“规则全是垃圾”的问题,不是算法不行,而是数据没处理好。
第一,事务必须被转成one-hot编码,也就是每一行是一个订单,每一列是一个商品,交叉处填0或1。直接用原始订单明细表喂给库,算法读不懂。以mlxtend为例,输入必须是DataFrame,值只有0/1或者True/False。
第二,同一张订单里同一个商品出现多次,只能算一次。比如用户一次买了三箱牛奶,这只是一个订单里出现了“牛奶”,不是出现三次“牛奶”。如果在转one-hot前不去重,支持度计数会严重偏高。
第三,一定要剔除对分析无意义的项。我在真实零售数据里经常看到“配送费”“优惠券”“包装袋”这类订单几乎必带的项目,它们会占据高频位置,把真正的商品组合规则挤到看不见。处理方式是先把这类项从事务里去掉,或者单独分离成表。
第四,建议按业务逻辑切分数据。比如商业广场门店和社区门店客群差异很大,12月的规则和5月的规则可能完全不同。统一建模会把不同群体混合成一条“什么都不是”的规则。我通常至少按门店、季节、时段切一刀,做分层建模。
4.2 建模与调参:一次完整的Python实操
用一套虚构数据走完整流程:某连锁超市有五个分店,一个季度的交易记录,共两万张小票,316个商品SKU。目标是找出适合做组合推荐的规则。
先读数据并转one-hot:
import pandas as pd # orders 列表示订单号,items 列表示商品名 df_orders = pd.read_csv("orders.csv") # 一个订单内商品去重 df_orders = df_orders.drop_duplicates() # one-hot 编码 basket = ( df_orders.groupby(["orders", "items"])["items"] .count() .unstack() .fillna(0) .astype(bool) )316个SKU直接全部跑,会产生大量低频垃圾项。先做一个热度筛选,比如只保留出现在至少200张小票里的商品,这样既能覆盖绝大多数订单,又能砍掉一买就再也没出现的长尾:
item_counts = basket.sum().sort_values(ascending=False) hot_items = item_counts[item_counts >= 200].index basket_hot = basket[hot_items]然后跑FP-Growth,最小支持度从0.02开始试。0.02意味着一个组合至少要在400张小票里出现过才会被记录,这对一个季度两万单的量级来说是合理的起步门槛:
from mlxtend.frequent_patterns import fpgrowth, association_rules frequent_itemsets = fpgrowth( basket_hot, min_support=0.02, use_colnames=True, )生成了频繁项集之后,再生成关联规则。我选“提升度”作为主要筛选指标,最小阈值设1.2,置信度最小设0.5:
rules = association_rules( frequent_itemsets, metric="lift", min_threshold=1.2, ) rules = rules[rules["confidence"] >= 0.5] rules = rules.sort_values("lift", ascending=False)跑出来的结果里,有一条规则特别典型:
{手打鲜奶油} → {草莓蛋糕}
支持度0.023,置信度0.61,提升度4.88。含义是:这个季度有2.3%的小票同时包含这两样商品;买了手打鲜奶油的订单里,61%也买了草莓蛋糕;而草莓蛋糕本身出现在所有小票里的比例只有12%左右,买鲜奶油的人买蛋糕的概率比随机人群高了近4倍。这条规则业务上完全可解释,也适合做甜品区的组合推荐。
输出里也有大量没用的规则,比如{方便面} → {卤蛋},提升度也有2.0,但业务方看完后觉得这两个本来就该放一起,不需要算法提醒。所以在规则筛选阶段,我会把阈值跑完之后,再人工走一遍业务可解释性检查,而不是直接照单全收。
注意:min_support 不是越低越好。把0.02降到0.005之后,频繁项集数量从三百多跳到了八千多,关联规则数量更是突破四万条,里面绝大部分是长尾商品组合,别说运营看不懂,就是建模的人自己去维护规则列表都会抓狂。阈值定完一定要回头看一眼频繁项集数量,而不是只看规则数量。
4.3 规则落地:从结果到行动
模型跑出规则只算走完一半,另一半是把规则变成能执行的动作。
在零售场景里,最常用的落地方式是组合推荐。用户在购物车加了手打鲜奶油,结算页就推草莓蛋糕;蛋糕放在鲜奶油旁边或者反过来,都能直接提升连带率。更细的做法是按规则强度来设计话术,如果规则支持度很高,就做大幅曝光;如果支持度低但提升度极高,只做定向推荐,避免打扰普通用户。
营销组合同样可以参考规则。比如规则显示{咖啡豆, 手冲滤纸} → {电子秤},提升度明显偏高,那在做咖啡豆促销时,可以设计“买咖啡豆+滤纸加购电子秤享折扣”的活动。
在把这些规则交给业务方之前,我习惯整理成一张规则清单,包含规则本身、支持度、置信度、提升度、前件覆盖人数、后件覆盖人数,以及我建议的业务动作。业务方看不懂支持度没关系,但能看到“覆盖人数”和“动作建议”,这条线才算真正打通。
5. 我把掉过的坑写在这里
5.1 频繁项集爆炸:阈值太低
第一次用关联规则跑全量交易数据时,我把min_support设成了0.0005,想着反正算法快,多找点规律。结果频繁项集直接飙到十几万条,规则生成阶段内存几乎被打满,最后程序直接崩了。原因很简单:组合数量呈指数增长,项集长度每增加1,候选数量就成倍膨胀,而低阈值等同于给所有弱共现开了绿灯。
后来的对策是分步控制。第一步把min_support提到合理区间,第二步给项集长度加上限,比如只挖长度不超过4的项集,第三步对商品做热度分层,把头部热品和长尾冷品分开建。三管齐下,规则数量立刻变得可控。
5.2 数据稀疏与冷门商品
零售数据里常见的问题是长尾商品太多,比如某款进口调味酱一个季度只卖出去三次。这种商品无论和谁组合,支持度都极低,几乎不可能进入频繁项集。
直接提高支持度阈值解决不了问题,因为会把头部商品的可挖空间也压缩。我一般用两个办法:一是把同品类的长尾商品聚合统计,把“进口调味酱A”“进口调味酱B”合成“进口调味酱”这个类目再跑规则;二是干脆只对头部50个SKU做关联分析,长尾部分交给别的方法处理,比如基于内容的相似推荐。
5.3 看似强其实弱的伪相关
有些规则指标很好,但业务上完全经不起推敲。我之前在一个促销活动数据里看到{生日横幅}和{气球}提升度高达8,显然是因为两者都被同一个“生日布置”需求驱动,而不是因为买横幅会促进买气球。这种伪相关在关联规则论文里被反复讨论,实际业务里也一点都不少见。
处理方式是在规则解读前加一个“对照组”思维。把同一类用户随机分成两组,一组没有前件行为,看后件出现概率的基准水平;一组有前件行为,看后件出现概率。如果两者差异显著,规则才有部署价值。这个验证不用做得很复杂,简单的交叉表就能暴露大部分伪相关。
5.4 辛普森悖论和季节性
如果只对全量数据建模,规则可能会被群体结构误导。比如某次分析显示“咖啡”和“啤酒”整体呈负相关,把数据拆到上午时段和晚上时段,却在两个时段都发现强正相关。原因很简单:上午买咖啡的人多、晚上买啤酒的人多,两个群体混合后,从整体看反而不相关。
季节性同样是个坑。冰淇淋和遮阳伞在夏季是强关联,到冬季就消失;月饼的规则只有在中秋前一个月才有意义。把所有季节混在一起建一条全年规则,得到的结果往往对任何一个季节都不适用。
我现在处理数据时都会先问一句:“这条规则在什么时段、什么门店、什么人群里成立?”并且在输出规则时标注适用条件。很多你以为是算法问题的结果,其实是数据切片问题。
6. 场景扩展:不止购物篮
6.1 电商与零售
购物篮分析只是关联规则最基础的用法。在电商场景里,还可以做“浏览-购买”跨行为规则,比如浏览了商品详情页A的用户,最终下单时倾向于同时购买商品B,这类规则可以用来设计详情页的推荐位。页面点击序列也可以按“接下来5分钟内访问了哪些页面”转成事务序列,挖掘连续浏览路径中的组合规律,这对站内引导设计很有参考价值。
6.2 医疗与健康
医疗数据里的关联规则可以用于症状-诊断、诊断-用药、药物-不良反应等方向的探索性分析。比如某些症状组合出现时,某类检查项目的检出概率明显偏高。这类分析能为后续研究提供线索,但绝对不能用于直接诊断建议,因为医疗决策涉及干预和因果关系,关联规则只能提示值得关注的方向,而且在数据处理上要严格遵守匿名化和合规要求。
6.3 运维与日志
运维场景里,关联规则可以用来分析故障事件共现。比如系统报错A出现的短时间内,报错B也频繁出现;某些日志事件的组合出现在服务异常前的概率很高。实际操作时,事务的定义不是“一个小票”,而是“一个时间窗口内出现的所有事件”,比如5分钟内所有错误码的集合。这类时序关联规则对告警聚合、故障根因排查很有价值。
6.4 内容与文本
文本数据可以按“一篇文档包含哪些关键词”构成事务,挖掘关键词之间的共现规则。在内容标签系统里,比如某类技术文章经常同时出现“性能优化”和“缓存”,这类规则能帮内容运营发现新的选题组合;在用户画像里,也可以把用户被打上的标签当作事务,挖掘标签组合规律,辅助人群圈选。
7. 最后分享三点实操建议
这套算法我反复用过很多次,最想提醒后来者的是:关联规则不是一个“跑完出结果”的黑盒,而是一个需要反复调参、切片、人工判断的分析工具。我第一次跑全量数据时也天真地以为输出规则越多越好,后来才发现规则多到看不过来就等于没有规则,业务方最需要你做的不是在几万条规则里排排坐,而是替他们选出十到二十条真正能落地的规则。
我的建议是先小样本快速跑通流程,再上全量数据验证;先按业务切分数据,再统一建模;先人工审二审关键规则,再自动下发到业务系统。还有一点很实际:规则里的所有指标都只是帮你排序的工具,最终拍板的人永远应该是业务方。你把支持度、置信度、提升度算得再漂亮,都不能绕过业务解释这一步。想清楚这三条,再用这套算法,你会少走很多弯路。