老王在城西经营着一家快递分拣仓库。每天都有几百张快递单涌进来,每张单子上贴着一串商品标签,比如“牛奶—面包—鸡蛋”或“牛奶—面粉”。电商老板们总爱问:“老王,哪些货常常被同一张单子一起买走?”老王要是把每张单子都翻一遍,找到所有“经常结伴出现”的商品组合,那得翻到天亮。Apriori算法就像老王的笨办法——先列出所有可能组合,再一遍遍核对快递单。而FP-Growth呢?它像一位替老王重新设计了仓库布局的高人:第一遍扫单子,记下哪些商品是热门;第二遍,把每张单子的标签按热度排序后“挂”到一棵共享货架上。相同前缀的单子共用同一个架位,路径自然压缩,之后无论要挖什么组合,都不用再翻原始单子,只要顺着货架的藤蔓摸瓜就行。这就是FP-Growth——频繁模式增长算法,用一棵树替代了Apriori里令人头秃的候选集生成。
对AI从业者而言,FP-Growth是关联规则挖掘中最高效的经典算法之一,掌握它意味着你能在海量交易数据中快速找到有价值的共现模式,这是做用户画像、推荐系统和智能风控的硬技能。
目录
可视化演示
可视化展示FP-Growth算法核心机制:FP树构建与路径压缩、条件FP树递归挖掘、FP-Growth与Apriori性能对比,交互切换理解频繁模式挖掘。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FP树构建与路径压缩 事务按频次排序插入 共享前缀路径压缩 FP-Tree = insert(sort(T)) FP树通过路径压缩高效存储频繁项
核心思想:从“一遍遍翻单”到“建好货架一次成型”
老王过去应对电商老板的追问,靠的是最笨的办法:把所有可能一起买的商品组合都列出来,比如“牛奶+面包”“牛奶+鸡蛋”“面包+鸡蛋”……然后翻遍每一张快递单去数哪个组合出现得多。如果商品有几千种,组合数量会爆炸式增长,老王的仓库伙计们被累得直不起腰。Apriori算法就是这条路:先生成候选集,再反复扫描数据库验证,每多一件商品,候选组合就多一倍。
FP-Growth换了个思路。它把订单看成“标签串”,反正相同前缀的单子(比如都从“牛奶→面包”开头)可以在仓库里共用一个货架区,何必每次从头翻?于是算法先扫第一遍数据库,统计每个商品的“热度”(支持度),剔除冷门商品;扫第二遍时,把每张单子的标签按热度从高到低排序,再逐个挂到一棵树上——这棵树就是FP-Tree(频繁模式树)。树搭好了,所有频繁模式都藏在树枝上,顺着节点往下走就能找到,完全不需要再生成候选集。
说回老王:他现在的仓库不用再堆满候选组合清单,而是先造出一棵“共享货架树”,之后任何“哪些东西常一起卖”的问题,都变成在树上顺藤摸瓜。这正是FP-Growth名字里“频繁模式增长”的由来——频繁项集是从树的路径上“长”出来的。
- count(X)统计同时包含组合 X(如“牛奶+面包”)的快递单张数,这是分子。
- |D|D 代表全部订单集合,|D| 是订单总数,作为分母做归一化。
- support(X)两者相除得到组合 X 的支持度,值越接近 1 说明越常见。
支持度统计:第一遍扫单,先给商品称重
建树之前,老王要做一件事:给仓库里所有商品“称重”。他让伙计把每一张快递单的商品标签抄下来,遇到一个商品就画一笔“正”字。几天下来,每个商品的累计出现次数就是它的支持度计数。那些只出现过一次两次的冷门商品,比如某个顾客偶然买的“老式收音机”,会被直接扔出局——因为它们几乎不可能和其他商品凑成“热卖组合”。
这一步对应算法中的第一遍数据库扫描:统计全部单项的频率,保留达到最小支持度阈值(min_sup)的项,并按频率从高到低排序。这一步至关重要,因为FP树之所以能共享路径、压缩体积,前提是所有订单都把高频项放在前面。如果第一个商品每次都不同,树的分支就会像孔雀开屏一样散开;而热门商品排前面,大量订单就有了相同前缀。
在老王仓库里,这一遍扫描像给商品“过秤”:不够分量的商品直接剔除,剩下的按重量排好队。这样一来,第二遍建树时每位顾客的订单都按“重货在前”的顺序挂上货架,树的分支自然紧凑,为后续的路径压缩打下了基础。
- 支持度 requirement计算每个商品组合 X 的支持度,看它能否跨越阈值。
- min_suppmin_supp 是提前设好的最低热度门槛,比如 0.5 或出现次数≥2。
- Frequent 集合把符合条件的 X 收进“频繁项集”清单,作为后续建树的原料。
FP树构建:共享前缀,把重复路径压成一根
现在到了重头戏:老王要布置“共享货架”。规则很简单——所有订单的标签必须先按热度排序,然后从根节点开始,一个一个挂上树。如果某个标签在树上已经有同名的宝贝节点,就顺着它走,节点计数加一;如果树上还没有这个标签,就新建一个分支节点。比如三张订单分别是“牛奶-面包-鸡蛋”“牛奶-面包-黄油”“牛奶-面包”,它们的前缀“牛奶→面包”完全一样,树上只需保留一条路径,计数累加到3即可。
这就是FP树的精髓:路径压缩。FP树节点上存的不再是单件商品本身,而是商品的计数。根节点是空货架起点,每个子节点挂着一个商品标签和它的计数。同时,算法维护一张“项头表”,它就像仓库入口的索引目录,把所有频繁项按热度列出来,并链接到树上所有同名节点——之后挖掘时,想找哪个商品,直接通过索引跳到对应节点,不必全树遍历。
老王指着仓库里那排货架笑道:“以前四张单子里有三张都写着'牛奶→面包',我从头挂四个牛奶、四个面包;现在只挂一个牛奶、一个面包,计数写个3。共享货架一节省,整棵树就瘦了一大圈。”
- N 与 KN 是原始所有订单标签的总数,K 是建树后实际产生的节点个数。
N$$ 与 $$K - N - K相减得到“被共享掉”的节点数,这就是路径压缩节省的空间。
- ρ 压缩率节省的节点数除以总标签数,得到 0~1 之间的压缩率,越高说明共享越多。
条件模式基:从树叶往根回溯“局部清单”
货架建好了,接下来就是挖宝环节。FP-Growth 的挖掘是自底向上的——从支持度最低(也就是货架最底层)的节点开始。老王想查“鸡蛋”都和谁一起卖,他不需要再翻原始单据,只需要在项头表里找到“鸡蛋”的链接,顺着指针找到树上所有“鸡蛋”节点,然后从每个“鸡蛋”节点往父节点回溯,一直走到根,把沿途经过的节点路径记下来。
这条回溯出来的路径就叫做“前缀路径”(prefix path),所有前缀路径的集合就叫“条件模式基”(Conditional Pattern Base,CPB)。每条路径还要带上“鸡蛋”节点的计数,因为路径上的商品真正和“鸡蛋”共现的次数等于这个计数。老王把这些局部清单收集齐,再按支持度过滤,就得到了“鸡蛋的条件模式基”——只属于“鸡蛋”的小数据。
这就好比老王想查“鸡蛋的黄金搭档”,他不再翻整仓库的原始单子,而是顺着“鸡蛋”这条藤蔓,把每个挂着鸡蛋的货架支路单独拍个清单。接下来的工作,都在这叠局部清单里完成。
- prefix(p)p 是树上从根到 x 的一条完整路径,prefix 表示去掉末端的 x 后保留的前缀路径。
- p 通向 x限定条件是路径 p 必须最终指向节点 x,这样才能保证 x 一定出现在路径里。
- CPB(x)所有满足条件的前缀路径构成集合,这就是 x 的“小局部”数据。
条件FP树:递归降维,挖出完整的频繁模式
有了“鸡蛋”的条件模式基,老王就有了一个缩小版的数据库。他可以对这份局部清单再做一次FP树的构建:统计前缀路径中各商品的支持度,过滤掉不够格的,再挂成一棵小树——这就是“条件FP树”(Conditional FP-Tree)。在这棵小树上继续递归挖掘,每次递归都锁定一个新的后缀商品,模式就一层层“长”出来。
回到“鸡蛋”的例子:假设“鸡蛋”的支持度为2,向下递归时以“面包”为新的后缀,会发现“面包→鸡蛋”出现2次,于是频繁模式{面包,鸡蛋}诞生。再递归一层,可能得到{牛奶,面包,鸡蛋}。整个挖掘过程就像剥洋葱——每次递归剥掉一层,把后缀锚点不断加长,直到条件FP树只剩一个分支或为空为止。
老王把这套递归戏法称作“俄罗斯套娃”:每挖一层,货架就会变小一圈,但每次都会带出一个更长的组合。等所有套娃都打开,仓库里所有“热卖组合”清单就整整齐齐地摆在桌上了。
- CTree_x由 x 的条件模式基构建的“条件FP树”,这是一棵缩小版货架。
- Y 来自递归在条件FP树上继续递归挖掘,得到更小的频繁项集 Y。
- x ∪ Y 合成把每个 Y 与后缀 x 拼接,得到完整频繁模式。
性能优势:不猜组合,所以快得吓人
Apriori之所以慢,是因为它要“猜组合”:先生成所有候选集,再一遍遍扫数据库验证。候选组合的数量随着商品种类呈指数级增长——10种商品就有1024种组合,20种商品就超过100万种。而FP-Growth只扫两遍数据库:第一遍统计单项频率,第二遍建树。之后所有挖掘都在内存中的FP树上完成,不再触碰原始数据库。
从复杂度上看,FP-Growth的代价主要取决于订单的平均长度和订单数量,呈线性关系;而Apriori的候选集生成阶段是组合数爆炸。树结构还天然做了路径压缩,内存占用也远低于Apriori反复装载的候选集。尤其是面对稀疏数据(每张订单商品少但种类多)时,FP-Growth的优势更加明显。
老王总结道:“Apriori 像一个个去敲门问'你们家有没有牛奶面包',FP-Growth 则是把全楼的邮箱钥匙全配好,一次进门,之后想找谁就找谁。”
- L × NN 是订单数,L 是每笔订单平均标签数,两者相乘就是建树的总工作量。
- N × 2^kApriori 需要对 N 张订单反复扫描,去验证最多 2^k 个候选组合。
- 对比L 通常很小(个位数),k 却可能上千,所以FP-Growth量级远小于Apriori。
并行化扩展:把货架拆给多个仓库同时干
订单量太大怎么办?老王开了三家分店。他按快递单的编号把数据切成多份,每家分店领到一份订单子集,各自独立构建局部的FP树、独立挖掘频繁模式——这就是“数据划分并行”。因为FP树的构建是可以并行的,每台机器只需要处理自己的那部分订单,互不干扰。
但并行不是免费的午餐。不同的切分方式会导致各分店工作负载严重不均:如果某家分店恰好分到了大量“牛奶面包”订单,它的树必然又高又粗,构建时间远长于其他分店。所以并行化FP-Growth的关键在于负载均衡——要么按商品哈希分组让各节点数据更均匀,要么采用动态任务调度的方式让空闲节点主动领活。最后还需要一个结果合并阶段,把各分店挖出的局部频繁模式汇总,再做一次全局过滤,确保不遗漏全局频繁项。
老王看着三家分店的数据汇总说:“分店越多是越快,但最后合并数据、统一口径这一步省不掉。想要真正跑得快,就得让三家分店干得一样多,别让一家累死三家闲死。”
- (1-p)1-p 表示程序中不可并行的串行部分,比如最终合并结果,它始终占用1份时间。
- p/k可并行部分 p 被 k 个处理器分担,理想情况下耗时缩小为原来的 1/k。
- Speedup总耗时是串行与并行之和,取倒数得到加速比,k 越大加速比越趋于 1/(1-p)。
AI 应用场景
电商平台的捆绑推荐
阶段:特征工程与模式挖掘
解决的问题:传统人工分析销售数据找“常被一起购买的商品组合”效率极低,且难以及时发现新组合。FP-Growth通过扫描历史订单快速构建FP树,自动挖掘全部高支持度组合,为商品捆绑推荐提供数据支撑。
具体案例:某生鲜电商每天产生50万笔订单,数据团队用FP-Growth对近30天订单做关联规则分析,设置最小支持度0.5%。算法在几分钟内找出“牛油果+全麦面包”“三文鱼+柠檬汁”等高频组合,运营部门据此设置搭配套餐,捆绑销量提升了23%。
银行反欺诈的关联特征识别
阶段:风险特征发现
解决的问题:欺诈交易往往有隐蔽的“组合套路”,比如短时间内“小额试探+大额转账+快速提现”。传统规则引擎靠专家手工编写组合规则,覆盖面小。FP-Growth能从海量历史交易中挖掘高频“特征组合”,辅助风控团队快速发现可疑模式,并作为特征喂给后续分类模型。
具体案例:某支付平台用FP-Growth分析已确认的诈骗交易流水,发现“夜间+新设备+改密+大额”的组合频繁出现在欺诈样本中。该组合被加入风控规则后,欺诈拦截率提升了12%,同时误报率没有明显上升。
医疗症状共现分析
阶段:医学知识发现
解决的问题:慢病管理需要了解哪些症状和指标往往同时出现,传统统计逐一核对组合非常耗时。FP-Growth把每位患者的症状清单视为一条“事务”来构建FP树,快速发现高频症状组合,辅助医生优化问诊流程和健康管理方案。
具体案例:某健康管理平台对2万名糖尿病患者的体征记录进行FP-Growth挖掘,发现“高血糖+高血脂+睡眠差”的支持度显著偏高。平台据此设计了糖尿病联合干预套餐,将相关患者的复诊依从率提升了18%。
Python 演示
class FPNode: def __init__(self, item, parent): self.item = item self.count = 1 self.parent = parent self.children = {} self.link = None class FPGrowth: def __init__(self, transactions, min_sup): self.tx_list = transactions self.min_sup = min_sup self.root = FPNode(None, None) self.header_map = {} self.order_map = {} self.link_map = {} def build(self): freq = {} for tx in self.tx_list: for it in tx: freq[it] = freq.get(it, 0) + 1 self.header_map = {k: v for k, v in freq.items() if v >= self.min_sup} self.order_map = {k: i for i, k in enumerate( sorted(self.header_map, key=lambda x: -self.header_map[x]))} self.link_map = {k: [] for k in self.header_map} for tx in self.tx_list: items = [it for it in tx if it in self.order_map] items.sort(key=lambda x: self.order_map[x]) cur = self.root for it in items: if it not in cur.children: node = FPNode(it, cur) cur.children[it] = node self.link_map[it].append(node) else: cur.children[it].count += 1 cur = cur.children[it] def prefix_paths(self, item): paths = [] for node in self.link_map[item]: path = [] p = node.parent while p and p.item is not None: path.append(p.item) p = p.parent paths.append((path, node.count)) return paths def mine(self): patterns = [] for item in sorted(self.header_map, key=lambda x: -self.header_map[x]): for path, count in self.prefix_paths(item): patterns.append((tuple(path) + (item,), count)) return patterns if __name__ == "__main__": orders = [ ["牛奶", "面包", "鸡蛋", "果酱"], ["牛奶", "面包", "鸡蛋"], ["牛奶", "面包"], ["牛奶", "果酱", "鸡蛋"], ["面包", "果酱"] ] fp = FPGrowth(orders, min_sup=2) fp.build() for pat, cnt in fp.mine(): print(pat, cnt)
self.header_map = {k: v for k, v in freq.items() if v >= self.min_sup}第一遍扫描后按最小支持度过滤冷门商品,相当于把不够分量的商品剔除出货架。self.order_map = {k: i for i, k in enumerate(sorted(self.header_map, key=lambda x: -self.header_map[x]))}按热度降序给频繁项编号,保证后续上树时高频项排在路径前面。items.sort(key=lambda x: self.order_map[x])每个事务先按热度排序再插入,这样不同订单才能共享前缀路径。if it not in cur.children: ... else: cur.children[it].count += 1树上已有同名节点就计数加一,否则新建节点,这是路径压缩的核心逻辑。self.link_map[it].append(node)维护项头表与树节点的链接,让后续挖掘能快速定位所有包含某商品的路径。while p and p.item is not None: path.append(p.item)从节点向上回溯父链,提取前缀路径形成条件模式基。
常见误区
误区“FP-Growth直接建树就能找到频繁项集,不需要像Apriori那样先算支持度,省了一道工序。”
正解FP-Growth依然要计算支持度,而且第一遍扫描数据库就是专门做单项支持度统计的,只有达到最小支持度阈值的商品才允许进入FP树。它省掉的不是“支持度统计”,而是“候选集的逐层生成与验证”。你可以把支持度统计理解为给商品称重,这步省了,货架就乱了。
为何常见:因为FP-Growth的宣传点集中在“不需要生成候选集”,初学者容易把候选集和支持度统计混为一谈,误以为连计数都省了。
误区“FP树就是把每张快递单原封不动挂上去,像文件夹一样存在树上。”
正解FP树存入的每个事务都经过了两次预处理:一是删掉支持度低于阈值的冷门项,二是按支持度从高到低重新排序。此外,多个事务若前缀相同,在树上只保留一条路径并用计数值表达重复次数,这才是“路径压缩”的真正含义。
为何常见:人脑中“树=数据结构”的直觉很自然,容易忽略算法在入树前对数据做过的排序与过滤,从而低估了树的压缩力度。
误区“每递归一层都要把对应的数据子集完整复制一份,内存会越来越大。”
正解条件模式基是由项头表链接回溯出来的“前缀路径集合”,本质上是FP树上指针的遍历结果,不是物理复制整张表。构建条件FP树时也只取过滤后的节点重新组织链接,数据规模随递归快速收缩,因此整体内存占用是受控的。
为何常见:递归算法容易让人联想到“一层层复制数据”,尤其当教材用表格展示CPB时,读者误以为这些表真的被完整存储。
误区“FP-Growth又快又省内存,以后所有关联规则任务都无脑用FP-Growth就好。”
正解FP-Growth在大多数场景下确实优于Apriori,但并非万能。当最小支持度阈值很高时,候选集本来就不多,Apriori的简单实现可能更易维护;当数据量小且需要频繁变更阈值时,Apriori重新扫库的代价反而更低。此外,Apriori天然生成候选项集的结构在某些增量学习场景仍有价值。
为何常见:看到性能对比图上一骑绝尘的FP-Growth,学习者容易忽略“性能优势的前提是低支持度+大数据量”这一适用边界。
面试题
Q1. FP-Growth 名称中的“频繁模式增长”最准确的含义是什么?
- A.频繁项集直接从候选集增长
- B.频繁项集从候选集逐步筛选
- C.频繁项集从树路径递归生长← 正确答案
- D.频繁项集从支持度增长推导
答案:C。FP-Growth 不生成候选集,而是通过FP树上的条件模式基递归挖掘,让频繁项集像树枝一样逐层“长”出来,故称“增长”(Growth)。A、B描述的是Apriori的思路,D是干扰项。
Q2. 在构建FP树之前,FP-Growth 对每个事务做了哪两步预处理?
- A.去除重复项并按字典序排序
- B.删除冷门项并按支持度降序排序← 正确答案
- C.保留全部项并按出现顺序排列
- D.合并同类项并打乱顺序
答案:B。第一步删除支持度低于最小阈值的项,第二步把保留项按支持度从高到低排序。这么做是为了让大量事务共享前缀路径,最大化路径压缩效果。A的字典序和D的打乱顺序都会破坏前缀共享的机会。
Q3. FP树上某个节点的 count 字段表示什么?
- A.该商品在原始数据库中出现的总次数
- B.从根到该节点的路径在所有事务中出现的次数← 正确答案
- C.该节点子节点的数量
- D.该商品在事务中出现的平均位置
答案:B。count 记录的是“以该节点为结尾的前缀路径”在已插入事务中的累计频次,也就是这条路径被多少张订单共享。它既不是单纯商品频次,也非子节点数量。
Q4. “条件模式基”的本质是什么?
- A.原始事务中包含目标项的所有完整事务
- B.目标项所有前缀路径及对应计数的集合← 正确答案
- C.目标项在每笔订单中的排列位置
- D.目标项的支持度大于阈值的证明
答案:B。条件模式基是“所有包含目标项 x 的前缀路径集合”,每条路径附上 x 节点的计数。它通过项头表链接回溯得到,而非重新扫描全部原始事务。A错在它只保留了前缀而不是完整事务,且还带局部计数。
Q5. 相比 Apriori,FP-Growth 的优势体现在哪些方面?
- A.不需要扫描数据库统计支持度
- B.避免了候选组合的指数级生成← 正确答案
- C.只需要扫描数据库两次即可建树← 正确答案
- D.支持内存中直接完成全部挖掘← 正确答案
答案:BCD。FP-Growth 依然要扫库统计支持度(A错)。真正优势在于不生成候选集(B对)、只扫描两遍(C对)、挖掘在内存FP树上完成(D对),这些特性共同带来了数量级的性能提升。
Q6. 最小支持度阈值调高之后,FP树会发生什么变化?
- A.树会变得更高更宽
- B.树会变小,被过滤的项更多← 正确答案
- C.树保持不变,仅影响挖掘结果
- D.树会变成多棵小树
答案:B。阈值调高意味着更多商品因达不到热度门槛被剔除,进入FP树的项更少,事务被过滤得更多,树整体变小。把阈值调低则树变大,可挖掘模式更多但噪音也更多。
Q7. 项头表(Header Table)在FP-Growth中的作用是什么?
- A.存储每个商品的支持度并排序
- B.建立商品与树节点的快速索引← 正确答案
- C.记录树的深度和路径长度
- D.保存所有候选频繁项集
答案:B。项头表不仅记录频繁项及支持度,更关键的是每个项都带有一串链接,指向FP树上所有同名节点。挖掘条件模式基时,通过这个索引能瞬间定位“该商品出现在哪些路径”,省去全树遍历。
Q8. FP-Growth 挖掘时为什么选择“自底向上”的顺序?
- A.因为底部的节点支持度最低,先处理低频项可以快速收敛← 正确答案
- B.因为底部节点离根最近,遍历代价最小
- C.因为支持度最高的项一定在底部
- D.因为FP树的根节点在底部
答案:A。FP树按支持度降序排列,最底部常是支持度较低的项。从低频项开始挖掘,其条件模式基规模小,能快速生成以它为后缀的模式,再逐层上推组合成更大模式。B、C、D在体系结构上不成立,根节点在顶部。
Q9. FP-Growth 并行化实现面临的挑战有哪些?
- A.数据切分不均导致负载倾斜← 正确答案
- B.各分区的局部频繁项合并后可能不满足全局阈值← 正确答案
- C.共享前缀的历史路径无法简单分割← 正确答案
- D.FP树本身不能并行构建
答案:ABC。A对,哈希切分可能让某节点分到大热业务的密集订单;B对,局部频繁但全局不频繁,需要合并后二次过滤;C对,一棵共享前缀树按订单划分后会变成多棵树,原本共享的路径被拆分,可能损失部分压缩效果;D错,FP树可以在各分区并行构建,这是并行化的基础。
Q10. 下列关于 FP-Growth 的说法正确的有哪些?
- A.挖掘过程需要反复扫描原始数据库
- B.条件FP树是递归挖掘的核心载体← 正确答案
- C.支持度低于阈值的商品不会出现在FP树中← 正确答案
- D.FP-Growth比Apriori一定快十倍以上
答案:BC。A错,建树完成后挖掘只在内存树上进行,原始库不再扫描;B对,条件FP树是逐层递归降维的关键;C对,入树前冷门项已被过滤;D错,“一定快十倍”是绝对化表述,性能取决于数据特征和阈值,不可一概而论。