
有没有遇到过这种情况业务方丢给你一份几十万行的订单流水开口就问“哪些商品经常被一起买能不能挖出点搭配销售的规律”你如果用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项集时所有包含“牛奶”的组合比如“牛奶面包”“牛奶苹果”都可以直接扔掉根本不需要去统计它们的支持度。这就砍掉了大量的无效计算。在候选集生成的过程中还有一个可以优化的点生成k1项集的候选项时只从“前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, columnste.columns_) # 跑Apriori最小支持度设为0.2 frequent_itemsets apriori(df, min_support0.2, use_colnamesTrue) print(frequent_itemsets)接下来从频繁项集生成规则并按照提升度排序rules association_rules(frequent_itemsets, metriclift, min_threshold1.0) rules.sort_values(lift, ascendingFalse, inplaceTrue) print(rules[[antecedents, consequents, support, confidence, lift]])这段代码就能完成一次完整的Apriori挖掘。输出结果里有几个要注意的地方use_colnamesTrue是为了让项集显示为商品名称而不是数字编号association_rules里的metriclift是筛选规则的最常用方式实际项目中我一般至少把提升度阈值设在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-Growthmlxtend同样封装了FP-Growth的实现接口和Apriori几乎一致替换起来非常方便。from mlxtend.frequent_patterns import fpgrowth # 使用相同的数据格式 frequent_itemsets_fp fpgrowth(df, min_support0.2, use_colnamesTrue) print(frequent_itemsets_fp) # 生成规则 rules_fp association_rules(frequent_itemsets_fp, metriclift, min_threshold1.0) rules_fp.sort_values(lift, ascendingFalse, inplaceTrue) 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反而更有优势。维度AprioriFP-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去跑大数据。这个组合是最稳妥的路径既能理解原理又能解决问题。