有没有遇到过这种情况:业务方丢给你一份几十万行的订单流水,开口就问“哪些商品经常被一起买?能不能挖出点搭配销售的规律?”你如果用Excel透视表硬扛,可能折腾一整天只得到一堆没什么解释力的数字。这时候,关联规则就该上场了。
关联规则算是机器学习里相当经典的一类无监督学习方法,核心任务是从交易数据、行为日志这类“事件集合”中找出高频共现的模式。而提到关联规则,绕不开的两个算法就是Apriori算法和FP-树频集算法(FP-Growth)。这篇文章不打算做教科书式的理论复述,我就从实际项目里怎么用、这两个算法各自好在哪里、坑在哪里的角度,把关联规则这件事讲透。
1. 关联规则到底在解决什么问题:从购物篮到用户行为
1.1 一个被反复说起的案例:购物篮分析的本质
绝大多数讲关联规则的文章都会从“啤酒和尿布”的故事开始。这个故事的真实性其实一直被争论,但它能流传这么多年,确实精准说明了关联规则的应用场景:在一个购物篮中,出现了商品A,那么商品B同时出现的概率是不是显著变高了?如果是,那就可以据此做出商品陈列、捆绑销售、促销策略等业务决策。
我在实际项目中接触到的场景比“啤酒尿布”要宽泛得多。比如一个在线教育平台,用户在一个月内点击了“Python入门”“数据分析实战”“SQL基础”三门课程,这三门课之间构成什么关系?再比如一个内容社区,用户读了一篇“装修避坑指南”之后,会不会更大概率去读“全屋定制选购攻略”?这些都可以建模为关联规则问题。
所谓购物篮分析,本质上就是把“一个用户的一次行为集合”看成一个“篮子”,把篮子里的每一项看成“商品”。这个篮子可以是订单,也可以是会话,甚至是一个时间段内的行为序列。把数据抽象成这样的结构之后,我们就不再被具体业务形态干扰,可以用同一套算法去处理非常多的场景。
1.2 三个核心指标:支持度、置信度、提升度
学习关联规则,第一关就是理解三个指标:支持度(Support)、置信度(Confidence)、提升度(Lift)。很多人会背公式,但真的拿到业务数据时还是搞不清楚该怎么用。我建议从业务含义去理解它们。
支持度:衡量一个项集(比如“牛奶和面包同时出现”)在所有订单中的出现频率。计算公式是包含该项集的订单数除以总订单数。支持度解决的是“这个规律到底覆盖了多少人”的问题。如果一条规则只有5个订单支持,哪怕置信度100%,也不值得投入资源。
置信度:表示在已经包含前件(比如牛奶)的订单中,同时包含后件(面包)的比例。计算公式是“牛奶和面包同时出现的支持度”除以“牛奶出现的支持度”。置信度解决的是“有A时出现B的概率有多大”的问题,它是衡量规则强度的指标。
提升度:这是很多人最容易忽略但最重要的指标。提升度的计算公式是规则置信度除以后件的支持度,它衡量的是“A出现后B的概率”和“B本来就出现的概率”之间的比值。如果提升度大于1,说明A对B有正向促进作用;小于1则说明更倾向于抑制;等于1则代表A和B是独立的,所谓的规则只是巧合。
举个具体的例子来说明。假设一个超市有10000条订单,其中600条包含牛奶,400条包含面包,同时包含两者的有200条。那么“牛奶→面包”的支持度是2%,置信度是200/600约等于33.3%,而提升度是33.3%除以4%等于8.33。这个提升度意味着买了牛奶的顾客买面包的概率是普通顾客的8倍多,这显然是条强规则。
1.3 为什么“相关性”不等于“因果性”
这个提醒必须放在最前面。关联规则挖掘出来的只是“共现关系”,它描述的是“这两个东西倾向于一起出现”,但它完全不能证明“一个导致另一个”。在业务实践中,我见过很多团队的误用——把关联规则的结果直接解释成因果,然后做出了错误的策略。
举例来说,数据分析发现“购买婴儿纸尿裤的用户会在同一周内购买啤酒”,真正的解释很可能是“购买纸尿裤的大概率是有夜间照顾婴儿需求的中青年男性,他们在采购日常用品的同时顺便给自己囤了啤酒”,而不是“纸尿裤导致啤酒购买”。这就是基于人群特征的共同驱动,而不是商品间的因果链条。
所以在向业务方输出结果时,一定要把规则描述成“在某种人群或场景下存在强共现关系”,而不是“A导致B”。关联规则是用来发现组合机会的,不是用来做因果推断的。
2. Apriori算法:先找频繁项集,再生成规则的经典路线
2.1 算法的两步走逻辑
Apriori算法的思想其实非常直白,整个过程可以拆成两大步:第一步是找出所有频繁项集,第二步是从频繁项集中生成关联规则。
“频繁”的定义就是支持度大于等于我们设定的最小支持度阈值。比如设定最小支持度0.02,那么在所有订单中出现次数至少达到总数2%的商品集合,就是频繁项集。
Apriori算法的核心操作是一个逐层递推的过程:先生成所有单商品项集(1项集),筛选出满足最小支持度的;然后基于频繁1项集两两组合生成2项集,再筛选,生成3项集,再筛选……直到无法生成新的频繁项集为止。
这里的“生成候选集”是一个组合爆炸的过程,如果商品种类有1000种,那2项集就有约50万种,3项集更是上亿量级。如果没有优化机制,普通机器根本跑不动。Apriori的聪明之处就在于它有一条非常重要的性质——Apriori性质。
2.2 剪枝策略:Apriori性质与其推导
Apriori性质用一句话概括就是:如果一个项集是频繁的,那么它的所有子集都是频繁的;反过来,如果一个项集是非频繁的,那么它的所有超集都是非频繁的。
这个性质看起来像废话,但它提供了一把极强的剪枝剪刀。假设“牛奶”本身不是频繁项(出现次数太少),那么在生成2项集时,所有包含“牛奶”的组合(比如“牛奶+面包”“牛奶+苹果”)都可以直接扔掉,根本不需要去统计它们的支持度。这就砍掉了大量的无效计算。
在候选集生成的过程中,还有一个可以优化的点:生成k+1项集的候选项时,只从“前k-1项相同”的频繁k项集中做合并,这样能保证不会生成重复的组合,同时确保新候选集的所有k-1项子集都已经是频繁的。
正是这个“先剪枝再计数”的思路,让Apriori成为那个年代最有效的关联规则挖掘算法。我在实现的时候会特别强调这个性质,因为很多初学者会忽略它,直接做全量组合,结果就是内存爆炸、跑半天出不来,根本没有理解算法的精髓。
2.3 一个可以跑起来的最小实现(mlxtend)
在Python里,我并不推荐从零手写Apriori的底层代码,除非你是为了学习或者研究目的。工业场景下直接使用mlxtend库就够了,它封装了一套非常干净的关联规则挖掘接口。
首先准备数据。关联规则的标准输入是一个“事务”列表,每行代表一个订单,每行的内容是商品集合。
import pandas as pd from mlxtend.preprocessing import TransactionEncoder from mlxtend.frequent_patterns import apriori from mlxtend.frequent_patterns import association_rules # 模拟一组订单数据 dataset = [ ['牛奶', '面包', '黄油'], ['牛奶', '面包'], ['啤酒', '尿布'], ['牛奶', '尿布', '啤酒'], ['面包', '黄油'], ['牛奶', '面包', '尿布'], ['啤酒', '尿布'], ] # 将事务数据转换为one-hot编码的DataFrame te = TransactionEncoder() te_ary = te.fit(dataset).transform(dataset) df = pd.DataFrame(te_ary, columns=te.columns_) # 跑Apriori,最小支持度设为0.2 frequent_itemsets = apriori(df, min_support=0.2, use_colnames=True) print(frequent_itemsets)接下来从频繁项集生成规则,并按照提升度排序:
rules = association_rules(frequent_itemsets, metric="lift", min_threshold=1.0) rules.sort_values("lift", ascending=False, inplace=True) print(rules[["antecedents", "consequents", "support", "confidence", "lift"]])这段代码就能完成一次完整的Apriori挖掘。输出结果里有几个要注意的地方:use_colnames=True是为了让项集显示为商品名称而不是数字编号;association_rules里的metric="lift"是筛选规则的最常用方式,实际项目中我一般至少把提升度阈值设在1.5以上,否则噪声太多。
2.4 Apriori的瓶颈:为什么数据稍大就变慢
Apriori算法够经典,但它的弱点也非常明显。它需要多次扫描全量数据集,每生成一层的候选项集就要扫一次数据库。假设最长的频繁项集长度为L,那就要扫描L次。在数据量几十万上百万条交易时,I/O开销就很可观了。
另一个问题是候选项集的数量依然可能很大。剪枝能去掉一部分,但组合的基数摆在那里,特别是商品种类多、数据稀疏的时候,候选集的生成和计数仍然非常耗时。
我在一个几十万订单的数据集上实测过,商品种类约5000种时,Apriori跑完的耗时要几十秒到几分钟,这还算可以接受。但如果商品种类上到几万、订单量上到几百万,Apriori就有点扛不住了。这时候就该把目光转向FP-树频集算法。
3. FP-树频集算法:从“多次扫描”到“一次构建”的优化思路
3.1 FP树的构建过程拆解
FP-树频集算法,通常直接叫FP-Growth,它的核心思路和Apriori完全不同。Apriori是“生成候选集+扫描验证”,FP-Growth则是“压缩数据到内存中的树结构+递归挖掘”。FP-Growth只需要扫描数据库两次,这是它性能碾压Apriori的关键。
第一次扫描:统计每个商品的支持度,过滤掉不满足最小支持度的商品,然后按照支持度从高到低排序。第二次扫描:逐行处理每条交易,把交易中出现且频繁的商品,按照全局频率降序插入到一棵FP树中,同时在节点上累加计数。
把事务压缩进树后,每个项集的所有必要信息都保存在了树和头指针表中,不需要再反复回到原始数据里去数次数。
用一个小例子说明。假设有三条交易:(鱼, 牛奶, 面包)、(面包, 牛奶)、(鱼, 面包),最小支持度为1。第一次统计得到鱼出现2次、面包3次、牛奶2次,都是频繁的,排序顺序为面包、鱼、牛奶(按频次降序)。第二条交易插入了面包和牛奶,第三条插入面包和鱼。最终树的结构是:根节点下有面包(计数3),面包节点下有两个分支:牛奶(计数2)和鱼(计数1),牛奶下还有鱼(计数1)。
路径面包→鱼和面包→牛奶→鱼共享了节点“面包”,这就压缩了数据量。整个树的结构记录了所有频繁模式的关键信息。
3.2 从FP树挖掘频繁项集:条件模式基与递归生长
构建完FP树,接下来就是递归挖掘的过程。FP-Growth的挖掘策略不像Apriori那样显式枚举候选集,而是通过构造条件模式基(Conditional Pattern Base)来逐步生长频繁项集。
以经常被一起买的“牛奶+鱼”为例。要找到所有包含“牛奶”的频繁项集,需要先找到FP树中所有包含“牛奶”的路径,即牛奶的前缀路径。每条路径上都带着计数信息——路径中每个节点恰好出现了多少次。把相同前缀路径的计数累加,就得到了“牛奶的条件模式基”。
基于条件模式基,可以构建一棵“牛奶的条件FP树”。在条件FP树上继续递归挖掘,把新挖掘到的节点和原模式合并,就得到了更长的频繁项集。整个过程没有任何重复扫描原数据,全部在树结构上完成。
理解FP-Growth的难点在于“递归生长”的抽象感。我个人的学习经验是,先手工跑一遍小规模数据的挖掘过程,把每一层的条件模式基画出来,然后再回来看代码会顺畅很多。这类算法的递归逻辑用空想来理解很容易绕晕。
3.3 用同一份数据实测FP-Growth
mlxtend同样封装了FP-Growth的实现,接口和Apriori几乎一致,替换起来非常方便。
from mlxtend.frequent_patterns import fpgrowth # 使用相同的数据格式 frequent_itemsets_fp = fpgrowth(df, min_support=0.2, use_colnames=True) print(frequent_itemsets_fp) # 生成规则 rules_fp = association_rules(frequent_itemsets_fp, metric="lift", min_threshold=1.0) rules_fp.sort_values("lift", ascending=False, inplace=True) print(rules_fp[["antecedents", "consequents", "support", "confidence", "lift"]])在数据规模较小时,两者的结果几乎一致。但到几十万条交易、商品上万的数据集上,FP-Growth通常比Apriori快一到两个数量级。我跑过的最典型的一个对比是:Apriori需要40秒才能完成的挖掘任务,FP-Growth大概3秒就出来了,差别非常直观。
值得注意的是,有少数的数据分布场景下,两者差距没有这么明显,比如频繁项集非常短(所有项集长度都在2以内)的时候,Apriori的多次扫描开销并不大,差距就不显著。但从实际经验看,大多数业务数据都是长尾分布,FP-Growth的优势是压倒性的。
3.4 结构设计背后的工程考量
FP-Growth真正聪明的地方是它构建了一棵高度压缩的前缀树。这棵树能把共享前缀的交易合并存储,从而大幅减少内存占用。比如有一万条交易都包含面包,但后面的搭配各不相同,在FP树里面包节点只出现一次,后面的分支再分别延伸。
这种做法非常契合真实业务数据的特点:头部商品高度集中,长尾商品稀疏分散。FP树利用的就是这个分布特征。
另外,FP-Growth在挖掘过程中也展现出良好的模块化特性。每一个条件FP树的构建和挖掘都是独立的,天然适合并行化处理。在大数据场景下,可以先对不同商品的条件模式基做分区,再用分布式计算来加速。这一点在单机版的Apriori中是很难做到的。
4. 两个算法怎么选:业务场景、数据规模和工程成本的权衡
4.1 两个算法的对比表格
很多人在学习阶段纠结“到底用哪个”,实际上工程上的答案很明确:默认用FP-Growth。但搞清楚两者的区别依然非常重要,因为在某些特定条件下,Apriori反而更有优势。
| 维度 | Apriori | FP-Growth |
|---|---|---|
| 核心思想 | 候选集生成 + 逐层筛选 | 前缀树压缩 + 递归挖掘 |
| 扫描次数 | 多次(频繁项集层数) | 两次 |
| 内存占用 | 候选集常驻内存,开销较大 | FP树结构较为紧凑,但条件库也占内存 |
| 适用数据量 | 中小数据集(几千到几十万) | 中到大规模数据集(百万级) |
| 实现复杂度 | 逻辑简单,易理解 | 树构建和递归较复杂 |
| 算法效果 | 结果一致 | 结果一致 |
有一点要特别说明:两个算法挖掘的频繁项集在相同支持度阈值下是完全一致的,差异只体现在效率和资源开销上。所以选型时不需要担心结果质量不同,只需要考虑计算资源。
4.2 不同数据规模下的实测感受
我个人的选型经验大致是这样的。数据量在1万条订单以内,商品种类不超过几百个,Apriori和FP-Growth的耗时几乎都是毫秒到秒级。这时候想用哪个就用哪个,我通常会选择Apriori做快速验证,因为代码逻辑更直白,出了问题好查。
数据量到了10万到100万条交易、商品种类几千的时候,我会毫不犹豫地选择FP-Growth。内存方面也要心里有数,如果商品种类特别多、交易特别长,FP树的条件模式基可能会占用大量内存,必要的时候得调大JVM或Python进程的可用内存。
数据量上到千万级甚至更大,单机已经很难支撑,此时要考虑分布式方案。业界成熟的工具(比如Spark的FP-Growth实现)也是基于同样的树结构思路去分布式化的。这种情况下Apriori基本没有登场机会。
4.3 规则筛选与业务落地的常见坑
算法跑完只是第一步,真正的难点在于从一大堆规则中淘出有价值的那几条。我在实践中总结下来,规则筛选至少要过三道关。
第一道关是统计显著性。提升度大于1只是说正面关联比随机共现高,但样本量过小时这个“高”可能完全来自波动。所以要额外关注支持度绝对值——一条规则至少要有几十个以上的订单做支撑,否则不可信。比如支持度0.001看起来很合理,但如果总共10000个订单,那只有10个订单在支撑,噪声风险极高。
第二道关是业务合理性。很多挖掘出的强规则其实是冗余的。比如“牛奶→乳制品”这种规则完全没有指导意义,因为后件是前件的超集。更麻烦的是“交叉持单”规则,比如“A平台优惠券→B平台使用”,这可能是同一用户在两个平台之间的自然流转,而并非主导关系。规则落地前需要业务方一起判断信息的可执行性。
第三道关是动作闭环。一条规则要能变成业务策略,必须能转化成具体的触发动作。比如“买了婴儿车的用户有较高概率购买婴儿座椅”,那么动作就是在购物车结算页对购买婴儿车的用户弹婴儿座椅的推荐。如果规则本身没法映射到一个清晰的触点上,挖掘得再准也是废的。
4.4 一个完整的简单分析流程示例
最后用一个完整的流程把这些串起来。假设手头有一个电商订单表,字段是order_id和product_name,任务是想找出“搭配购买”的机会。
第一步是数据预处理,把明细表聚合成事务列表,每一行为一个订单包含的商品集合。这里有两点要注意:一是确认是否需要做类别合并,比如“牛奶”和“脱脂牛奶”是放在一起分析还是分开分析;二是决定要不要对高销量商品做降采样,否则它们会“霸屏”。
第二步是调参跑模型。先用一个相对宽松的最小支持度(比如0.005)跑一轮,看看项集分布,再根据结果调。我一般会从支持度0.01、置信度0.5、提升度1.5起步,然后根据规则数量调整。规则过多就收紧支持度,过少就放宽。
第三步是写一个简单的挑选函数,按业务逻辑过滤掉同质规则。比如计算前后件是否有包含关系,或者是否属于同一个品类。
第四步是把通过筛选的规则输出成一张报告表格,配两三个典型的商品组合案例,一起提交给业务方,让业务方判断是否有进一步做捆绑促销或商品推荐的价值。
这套流程我反复用过很多次,从技术实现上并不复杂,难点全在数据理解和业务判断上。建议初次接触关联规则的朋友,先拿一份真实的订单流水完整跑一遍,不要用太干净的演示数据。数据一脏,你就能深刻理解为什么要花那么多时间做预处理了。
5. 写在最后的几条实际经验
回头再看关联规则这件事,它之所以到现在还没被深度学习那套东西冲击掉,原因其实很简单:它是一种自带解释性的方法。输出的是“前提→结论”形式的规则,业务方一看就懂,不像神经网络给出一堆特征权重让人摸不着头脑。
我个人在实战中还有几条小经验值得分享。第一,最小支持度的选择不要拍脑袋,可以先统计一下单商品的支持度分布,取中位数附近的值作为起调点。第二,FP-Growth跑出来的结果如果条数太多,优先去检查数据质量而不是继续压缩阈值,很多“强规则”其实是重复录入、别名乱造或者刷单造成的假象。第三,年度大促期间的数据和日常运营数据混在一起时,最好按活动周期分开挖掘,因为大促期间的用户行为模式和非大促期间差异极大,混在一起容易得到不符合日常规律的规则。
如果非要在Apriori和FP-树频集算法里选一个入门,我会建议新手先把Apriori弄懂,因为它帮你建立“项集—支持度—置信度—提升度”这个基本框架;实际干活的时候再用FP-Growth去跑大数据。这个组合是最稳妥的路径,既能理解原理,又能解决问题。