小城有家老字号餐馆,老板从成摞的点菜单里发现一个秘密:很多客人点“番茄牛腩”时,往往会加一碗“米饭”。他想把这类“黄金组合”系统地找出来。最笨的办法是穷举:把 n 道菜所有组合全列一遍,再回每张菜单核对。菜单上 20 道菜就有上百万种组合,菜单一厚,账房先生直接累垮。主厨却换了个思路:与其反复翻菜单,不如先花一遍功夫数清每道菜被点的次数,再花一遍功夫把每张菜单按人气排好,统统“挂”到一棵前缀共享的树上。此后找组合,只需顺着这棵树的枝条追溯。这张“点菜单树”就是 FP-Tree,这套方法就是 FP-Growth。它把数据库压缩成一棵树,用两遍扫描取代海量候选集的枚举,是数据挖掘里挖频繁项集的一把快刀。
掌握 FP-Growth,你就能在千万级事务数据上高效完成商品搭配、用户行为等关联分析,是数据挖掘工程师与推荐系统工程师的核心技能之一。
目录
可视化演示
动态演示FP-Growth的核心机制,通过采集/分析/决策/反馈四种模式展示关键概念与交互流程。
本节配有交互动效(进入视口后自动加载,约 3KB)。
FP-Growth:FPGr FP-Growth核心思想 FP树构建过程 核心机制可视化 FP-Growth整体架构与关键组件
算法背景:从穷举到建树
老板最初请来一位非常老实的账房先生,他先把所有菜的每一种组合都写在纸条上,再拿着纸条到一摞点菜单里逐一核对。菜单上有 20 道菜,组合纸条就有上百万张;每核一组,就得把整摞菜单翻一遍。餐厅开业不过一个月,账房先生就累垮了。这种“穷举一切组合再反复回库核对”的做法,正是 Apriori 算法的真实写照。
在关联规则分析中,每一张点菜单是一条事务(Transaction),每道菜是一个项(Item),我们想找出经常一起出现、构成“频繁项集”的项。Apriori 用“候选生成—逐层验证”的思路工作:先生成所有可能组合,再逐层扫描数据库验证计数。组合数随项数指数膨胀,且每一层验证都要重新扫描整库,这就是它的致命瓶颈。
支持度(Support)是衡量“频繁”的核心指标:一个项集 X 在全部事务中出现的比例。给定最小支持度阈值 minsup,支持度不低于阈值的就是频繁项集。FP-Growth 的创立者韩家炜提出一个反直觉的思路:与其反复扫描验证候选,不如先把整库压缩成一棵树,让挖掘在树上完成。
所以 FP-Growth 的思路,相当于让主厨亲手来整理后厨:先花一遍功夫数清每道菜被点了几次,排出“人气榜”;再花一遍功夫把每张点菜单按人气榜排好,挂到同一棵树上。此后挖掘再也不碰原始菜单——这便是“以树换扫描、以空间换时间”的智慧。
- 项集 X 与支持度计数X 是若干个项的集合,σ(X) 统计整个数据库里同时包含 X 全部项的事务数量,它是公式的“分子”。
- 事务总数 |D|分母 |D| 是数据库中事务的总条数,作为归一化的基准,把计数变成 0 到 1 之间的比例。
- 支持度阈值判定把比例与最小支持度 minsup 比较,达标才称为频繁项集,这是后续建树与挖掘的总闸门。
FP-Tree 结构:会共享前缀的树
要理解这棵“点菜单树”,不妨把它想成餐厅后厨的食材架:最上方是总入口(根节点),每个节点是一道菜,节点旁的数字是这道菜在这条路径上被点过的次数。同一个菜名出现在多个分支上,说明它是多个组合里的常客。整座架子从入口开始层层向下,每一条从根到叶的路径,就是一份被压缩后的点菜单。
FP-Tree 是一棵前缀共享树:不同事务按照统一的顺序插入,相同的前缀只保留一份,用递增的计数代表“有很多客人走了同一条路”。每个节点主要包含四样东西:项名(item)、计数(count)、父节点指针、子节点表;此外每个节点还有一个链指针(node-link),指向树中下一个与自己同名的节点,把散落的同名节点串成一条链。
头表(Header Table)则是树的“索引牌”:每个频繁项一张卡片,按支持度从高到低竖直排列,每张卡片通过 node-link 指向该商品在树中的第一个节点。想挖某个项时,顺着头表就能找到它在树上的所有位置,无需全树搜索。而所有事务统一下来的人气顺序,正是前缀能够共享、树能保持紧凑的前提。
回到厨房:头表就是主厨上衣口袋里的小本子,记着每道热门菜挂在食材架的哪些隔层。有了小本子,后续挖掘“黄金组合”时,提到一道菜,主厨就能瞬间定位它参与过的所有做法,不用满厨房翻找。
- 频繁项表 LL 是从高到低排列的项序列,建树时每条事务都严格按它的顺序插入。
- 点单次数 f(i)f(i) 是项 i 在整个数据库里的出现次数,即支持度计数,决定它在 L 中的位次。
- 降序约束位次更靠前的项总是先被插入,使不同事务的相同前缀在树中合并成同一条路径。
建树过程:两遍扫描定乾坤
主厨整理菜单只花两遍功夫。第一遍,他把所有点菜单快速过目,用正字法数清每道菜被点了几次,把低于人气门槛(minsup)的冷门菜直接划掉;剩下的菜按人气从高到低排出正式名单。这一遍的目的,是拿到全局的“人气榜”。
第二遍,他重新拿起每张点菜单,只保留名单上出现的菜,并按人气榜的顺序重排。比如原菜单写“鸡蛋、面包、牛奶”,按名单顺序可能变成“面包、牛奶、鸡蛋”。然后逐张把排好的菜单“挂”到树上:从根节点出发,途中遇到同名的孩子节点就往里走、把计数加一;没有同名孩子,就新建一个节点继续往下走。
例如三条事务 {B,A,E}、{B,C}、{B,A},过滤排序后为 {B,A}、{B,C}、{B,A}。第一条建出路径 B→A,第二条在 B 下新建 C,第三条走到 B 后沿着既有边到达 A,A 的计数加一。大量事务因此共享前缀路径,树的体积被极大压缩,通常远小于原始数据库。
两遍扫描的意义在于:第一次“探路”摸清人气,第二次“搭架”一次成型。此后无论有多少张新菜单,只要数据不变,树就不会变;所有后续挖掘都在树上完成,那叠厚厚的原始菜单再也不用翻动。
- 过滤非频繁项从原始事务 T 中剔除点单次数 f(i) 低于阈值的冷门菜,因为它们不可能出现在任何频繁项集里。
- 按人气排序把剩下的项按支持度降序重排,得到与全局人气榜顺序一致的新事务。
条件模式基:追溯每一条来路
树建好了,怎么挖“黄金组合”呢?主厨的作法是倒着挖:先挑一道菜,顺着小本子(头表)找到它在树上的每一处位置,然后从那些位置一路向上,把沿途经过的菜名抄下来,形成一条条“来历路径”。比如挑中 C,就在树上找到所有标着 C 的节点,从每个节点向上抄到根为止。
这些“来历路径”就是条件模式基(Conditional Pattern Base):对目标项 i 而言,树上所有以 i 结尾的前缀路径的集合。每条路径还附带 i 在该路径末端节点的计数,表示“有多少张点菜单走了这条前缀路径,并且都点了 i”。条件模式基精确描述了“在包含 i 的事务中,其他各项分别出现了多少次”,是下一轮挖掘的原料。
例如挖 C 时,树上有两条通往 C 的路径:root→B→C 与 root→A→C,末端计数分别为 2 与 2,于是 CPB(C) = {B:2, A:2}。接下来用这些前缀路径构造一棵“条件 FP-Tree”:对前缀路径中的各项重新统计频率、筛出频繁项、按频率排序建树,得到一棵规模更小的局部树。整个过程如同“沿着枝条倒走一遍来历”。
条件模式基就像一道菜的历史档案:每一位点过它的客人都留下一条记录,记录里写着那桌同时还点了什么。档案越积越厚,规律就浮现出来——与 C 经常同框的 B 和 A,自然成为下一轮挖掘的焦点。
- 位置集合 Paths(i)Paths(i) 是头表链上所有名为 i 的节点,每个节点唯一对应树中的一处位置。
- 前缀路径 prefix(p)prefix(p) 是从根节点到 p 的父节点所经过的节点序列,代表“点了 i 的事务中,其他项的组合方式”。
- 并集聚合把所有前缀路径连同末尾计数合并起来,就是 i 的条件模式基,也是后续条件树的输入数据。
频繁项集挖掘:递归滚雪球
有了条件 FP-Tree,挖掘就变成一场“滚雪球”:从单道菜出发,每轮在雪球上粘一道新的频繁菜,滚出越来越长的组合。递归的终止条件有两个:其一是条件树为空,没有新的项可粘;其二是条件树退化为一条单路径,此时路径上所有项的组合都能直接枚举,不必再分叉递归。
形式化地说,FP-Growth 对一棵 FP-Tree 与当前前缀 α 递归执行:找出树中所有频繁单项 β,把 β 并入 α,得到频繁项集 α∪{β};再用 β 的条件模式基构造条件 FP-Tree,带着更大的前缀继续递归。每一个频繁项集都沿着唯一的一条递归路径产生,天然去重。
以 C 为例:先得到 {C};CPB(C) 中 B、A 都频繁,于是得到 {B,C} 与 {A,C};再分别以 {B,C}、{A,C} 为前缀继续挖它们的条件树。整个过程中没有任何候选集的生成与验证,而是沿着树结构“定向生长”——这正是 FP-Growth 名字里“Growth(生长)”的含义。
这就像主厨不断追问:“和这道菜一起出现的菜,又常和谁一起出现?”每一问都把范围收窄到一棵局部小树上,答案像滚雪球一样越滚越大,却从不做无用功——没有一张候选清单需要被反复核对。
- 当前前缀 αα 是已经确定频繁的项集,递归开始时为空集,随层数增长不断累积。
- 遍历频繁单项 βFreq(T) 是当前树中所有满足最小支持度的单项,逐一将它们并入前缀。
- 构造条件树 T_β以 β 的条件模式基为数据,按同样的建树规则构造一棵更小的条件 FP-Tree。
- 递归合并结果把新生成的频繁项集与后续递归返回的全部结果并入总集合,直到条件树为空或退化为单路径。
与 Apriori 对比:树与穷举之争
回到账房先生与主厨的对决。账房先生(Apriori)先找单道菜的常客,再两两组合、三三组合地生成候选清单,每生成一轮都要把整摞菜单重翻一遍核对计数。候选组合数随菜单规模指数爆炸,数据库扫描次数约等于最长频繁项集的长度,菜单一多便寸步难行。
主厨(FP-Growth)则完全不同:他只在开局整理菜单时完整翻两遍,之后所有计数都在树上原地完成。他不需要生成任何候选组合,直接避开“候选爆炸”这个 Apriori 最致命的软肋。这是 FP-Growth 的核心优势——“无候选生成”(candidate-free)的频繁模式挖掘。
从复杂度看,Apriori 的候选总量是全部非空子集数,即指数级 2^|I|-1;而 FP-Growth 把数据库压缩进树后,扫描次数固定为 2,挖掘开销与树的规模成正比,树的规模又通常远小于原始库。当项数多、事务长、支持度阈值低时,FP-Growth 的优势尤其显著。
简单说:Apriori 靠“蛮力枚举 + 反复回库核查”,FP-Growth 靠“一次归档 + 原地查证”。面对同样的菜单,前者让账房先生加班到天亮,后者让主厨气定神闲地端出结论。
- 项的总数 |I|I 是所有不同项的集合,|I| 是项的个数,它是候选组合数的底数。
- 第 k 层候选数量Apriori 第 k 层要从 |I| 个项中挑出 k 个组成候选项集,数量为组合数。
- 全部候选求和把所有层的候选数量累加,得到 2^|I| - 1,直观展示了候选集的指数级膨胀。
实践应用:从黄金组合到智能推荐
“啤酒与尿布”是关联分析最著名的传说:超市发现买尿布的年轻父亲常顺手拿啤酒,于是把两者摆在一起,销量大涨。FP-Growth 正是把这类“黄金组合”从海量收银小票中挖出来的主力算法——先把每张小票按用户会话聚成事务,再建树、挖掘,一气呵成。
挖出频繁项集后,通常需要进一步计算置信度(Confidence):买了 A 的顾客中,有多少比例也买了 B。高于阈值就形成一条强关联规则 A⇒B,可用于货架摆放、捆绑促销、交叉推荐。置信度回答的是“确定性”问题,与回答“普遍性”问题的支持度互为补充。
现代电商与内容平台面对亿级行为数据时,FP-Growth 提供了一条实用路线:按会话把行为聚成事务 → 用 FP-Growth 挖掘频繁共现 → 用置信度、提升度筛选规则 → 接入推荐系统。Python 生态中的 mlxtend 等库提供了封装好的实现,底层依旧沿用“两遍扫描建树 + 递归挖掘”的本篇思想。
从餐馆点菜单到超市收银小票,从电商购物车到 App 点击流,FP-Growth 干的是同一件事:把散落的事务凝成一棵树,再从树上摘下一串串“黄金组合”。这张“树上的菜单”,正是数据挖掘教给我们的生意密码。
- 同现计数 σ(A∪B)分子统计同时包含 A 与 B 的事务数,代表 A、B 共现的强度。
- 条件基数 σ(A)分母统计只包含 A 的事务数,把共现数归一化到“买 A 的人群”里。
- 阈值判定置信度达到最小置信度阈值后,A⇒B 才被采纳为强关联规则。
AI 应用场景
场景一:电商购物篮分析
阶段:数据分析与特征挖掘
解决的问题:传统人工经验只能发现“啤酒+尿布”式的少量显性搭配,面对百万级 SKU 的隐性组合无能为力;Apriori 生成候选组合时呈指数膨胀,内存和耗时都难以承受。FP-Growth 用两遍扫描把历史订单压缩进一棵树,分钟级挖出全部高频搭配,就像主厨把整摞点菜单浓缩成一棵“点菜单树”。
具体案例:某生鲜电商把“同一用户 24 小时内的下单”聚合成一条事务,用 FP-Growth(最小支持度 0.1%)挖掘出“牛油果+全麦面包+希腊酸奶”等组合,做成首页“凑单推荐”引导,客单价提升 9%,同时减少了运费补贴成本。
场景二:用户行为路径与内容推荐
阶段:特征工程与规则推荐
解决的问题:点击流数据高维稀疏、噪声大,直接做关联统计容易把偶发共现当成规律;FP-Growth 通过阈值过滤与树结构压缩,只保留稳定共现的频繁行为链,产出可解释的推荐规则,好比沿着“点菜单树”只摘最稳的黄金组合。
具体案例:某资讯 App 把用户一次会话内点击的文章标签聚成事务,FP-Growth 发现“科技+财经+职场”标签经常共现,据此在详情页底部插入相关标签链文章,人均停留时长提升 12%。
场景三:医疗诊断症状共现挖掘
阶段:医学数据分析与辅助决策
解决的问题:电子病历中的诊断编码成百上千,医生凭经验难以穷举症状组合;FP-Growth 能在高维离散诊断码上高效挖出高频共现症状,辅助临床预检分诊与疾病关联研究,相当于给病历也建一棵“症状树”。
具体案例:某三甲医院利用 3 年门诊病历(约 50 万条事务)运行 FP-Growth,发现“糖尿病+视网膜病变+肾功能异常”等二级组合,用于慢病管理人群的早期筛查提醒。
Python 演示
# -*- coding: utf-8 -*-"""FP-Growth 频繁模式树挖掘: 从超市小票中发现"黄金组合" """ from collections import defaultdict class FPNode: """FP-Tree 节点: item 商品名, count 计数, parent 父节点""" def __init__(self, item, count, parent): self.item = item self.count = count self.parent = parent self.link = None # 头表链: 下一个同名节点 self.children = {} # 子节点字典 def build_tree(records, min_sup): """两遍扫描建树: 第一遍统计频率, 第二遍逐条插入""" freq = defaultdict(int) for rec in records: for item in set(rec): freq[item] += 1 freq = {k: v for k, v in freq.items() if v >= min_sup} if not freq: return None, {}, [] order = sorted(freq, key=lambda k: freq[k], reverse=True) # 人气榜 header = {} root = FPNode(None, 0, None) for rec in records: items = [it for it in order if it in set(rec)] # 过滤加排序 cur = root for item in items: if item not in cur.children: cur.children[item] = FPNode(item, 0, cur) if item in header: # 挂入头表链表 tail = header[item] while tail.link: tail = tail.link tail.link = cur.children[item] else: header[item] = cur.children[item] cur = cur.children[item] cur.count += 1 return root, header, order def mine_frequent(header, min_sup, prefix): """递归挖掘: 从条件模式基出发, 逐层长出频繁项集""" result = [] for item in sorted(header, key=lambda k: header[k].count): new_prefix = prefix + [item] result.append(new_prefix) # 收集 item 的所有前缀路径(条件模式基) paths, node = [], header[item] while node: path, parent = [], node.parent while parent and parent.item is not None: path.append(parent.item) parent = parent.parent if path: paths.append((path, node.count)) node = node.link if not paths: continue # 用条件模式基统计局部频率, 构建条件 FP-Tree local_freq = defaultdict(int) for path, cnt in paths: for it in path: local_freq[it] += cnt sub_order = [it for it, c in local_freq.items() if c >= min_sup] sub_order.sort(key=lambda it: local_freq[it], reverse=True) if not sub_order: continue sub_root = FPNode(None, 0, None) sub_header = {} for path, cnt in paths: cur = sub_root for it in sub_order: if it not in path: continue if it not in cur.children: cur.children[it] = FPNode(it, 0, cur) if it in sub_header: tail = sub_header[it] while tail.link: tail = tail.link tail.link = cur.children[it] else: sub_header[it] = cur.children[it] cur = cur.children[it] cur.count += cnt result.extend(mine_frequent(sub_header, min_sup, new_prefix)) return result def fp_growth(records, min_sup): """入口: 返回所有频繁项集""" root, header, order = build_tree(records, min_sup) return mine_frequent(header, min_sup, []) if __name__ == '__main__': records = [ ['牛奶', '面包', '鸡蛋'], ['面包', '啤酒'], ['面包', '黄油'], ['牛奶', '面包', '啤酒'], ['牛奶', '黄油'], ['面包', '黄油'], ['牛奶', '黄油'], ['牛奶', '面包', '鸡蛋'], ] min_sup = 2 print('== FP-Growth 频繁项集, 最小支持度 = %d ==' % min_sup) for itemset in fp_growth(records, min_sup): sup = sum(1 for rec in records if set(itemset).issubset(set(rec))) print('{%s} 支持度 = %d' % ('+'.join(itemset), sup))
freq = {k: v for k, v in freq.items() if v >= min_sup}第一遍扫描后,把点单次数低于阈值的冷门菜全部过滤掉order = sorted(freq, key=lambda k: freq[k], reverse=True)按支持度从高到低排出“人气榜”,它是所有事务统一的插入顺序items = [it for it in order if it in set(rec)]每条事务只保留人气榜上的菜,并按同一顺序重排if item not in cur.children:有同名孩子就走老路,没有才新建节点,这正是“共享前缀”的实现tail.link = cur.children[item]把新节点挂进头表的单向链,便于挖掘时快速找到所有同名位置paths.append((path, node.count))顺着链收集每条前缀路径和末端计数,得到条件模式基sub_order.sort(key=lambda it: local_freq[it], reverse=True)条件树同样按局部频率降序排列,继续复用建树逻辑result.extend(mine_frequent(sub_header, min_sup, new_prefix))每找到一个频繁项就并入前缀,再递归挖掘条件树,像滚雪球一样生长sup = sum(1 for rec in records if set(itemset).issubset(set(rec)))展示时回原始小票核对支持度,验证挖掘结果
常见误区
误区“FP-Growth 不是号称只扫描两次嘛,那建好 FP-Tree 之后,整个挖掘过程就完全不需要碰原始数据库了,连计数都不用回数据库看。”
正解两遍扫描发生在建树阶段:第一遍统计支持度、筛出频繁项,第二遍把事务按排序插入树中。挖掘阶段确实在树上进行,不再扫描原始库;但如果要展示每个频繁项集的支持度数值(比如 Python 演示里的打印结果),通常需要回原库做一次核对计数——这是实现层的选择,不是算法本身的要求。算法主体依然是“两遍建树 + 树上挖掘”。
为何常见:把“只需要扫描两遍”这个亮点过度解读成“零次扫描”,忽略了建树本身就是要扫描的;两遍扫描已经发生了,只是相对 Apriori 的 K 次扫描大幅减少而已。
误区“万一以后阈值调低了,不频繁项就变频繁了,所以建树时把它们也保留在树里,省得以后重建。”
正解FP-Growth 的建树前提是先确定最小支持度阈值;第一遍扫描就过滤掉低于阈值的项。根据向下闭包性,一个项自身不频繁,任何包含它的项集都不可能频繁,所以保留它在树上对挖掘结果毫无帮助,只会增加树的分支和内存开销。若阈值改变,正确做法是重新建树或重新跑参数,而不是把冷门菜硬塞进食材架。
为何常见:直觉上觉得“多存点总没坏处”,但忽略了树的核心价值是紧凑;塞入不频繁项会破坏前缀共享的压缩效果,让树退化。
误区“条件模式基就是把数据库里所有包含 C 的原始小票直接抄出来,再基于这些原始事务继续挖掘。”
正解条件模式基是从 FP-Tree 上提取的“前缀路径”集合:顺着目标项 C 的所有节点,把从根到其父节点的路径连同末端计数抄下来。它已经过两道“蒸馏”——事务里不频繁的项被剔除,顺序被统一排好,重复路径被计数压缩。它和原始事务无论内容还是形态都不同,是专门为条件树准备的高密度原料。
为何常见:因为条件模式基在语义上确实描述了“包含目标项的事务中还有哪些项”,容易让人误以为它就是原始事务的子集拷贝。
误区“FP-Growth 无候选生成、扫描又少,所以不管什么数据,它一定碾压 Apriori,选它就对了。”
正解FP-Growth 的优势依赖前缀共享的程度。如果每条事务的项组合差异极大、几乎没有公共前缀,FP-Tree 会退化成近似原始数据库的大杂树,内存开销大、挖掘分支多,优势被大幅削弱。Apriori 配合哈希树、垂直数据布局、剪枝策略时也有很强的工程表现。正确结论是:在事务长、项数多、阈值低、前缀共享度高的典型场景下,FP-Growth 通常显著更优,但并非无条件碾压。
为何常见:习惯了“新算法一定淘汰旧算法”的叙事,忽略了算法性能依赖数据分布这一前提。
面试题
Q1. FP-Growth 构建 FP-Tree 需要扫描原始事务数据库几次?
- A.只需要一次性扫描
- B.恰好需要两次扫描← 正确答案
- C.需要和商品数同次数
- D.需要和事务数同次数
答案:B。FP-Growth 的“两遍扫描”分别对应统计支持度与构建树:第一遍数完每道菜被点的次数并筛出热门菜,第二遍把每张点菜单按人气榜排好挂到树上。建树之后挖掘在树上完成,不再扫库。选项 B 准确描述了这个过程。
Q2. FP-Tree 中头表(Header Table)的主要作用是什么?
- A.记录树的总节点个数
- B.存储被过滤掉的项
- C.索引各频繁项的树位置← 正确答案
- D.保存事务的原始顺序
答案:C。头表相当于主厨的小本子:每个频繁项一张卡片,按支持度降序排列,并通过 node-link 串起树中所有同名节点。想挖哪个项,顺着头表就能定位它在树上的所有位置。C 正确;A 是节点计数的职责,B 是被过滤项的去向,D 与顺序统一化无关。
Q3. 关于条件模式基(CPB),下列说法正确的是?
- A.包含目标项的全部原始事务
- B.目标项在树中的所有前缀路径← 正确答案
- C.等价于整棵 FP-Tree 本身
- D.只含目标项而无其他任何项
答案:B。条件模式基是从树中提取的以目标项结尾的前缀路径集合,每条路径还附带支持度计数。它并非原始事务(A 错),也不是整棵树(C 错),更不只含目标项(D 错),而是包含目标项之前所有前驱项的路径,是构造条件树的高密度原料。
Q4. FP-Growth 相比 Apriori 的优势包括哪些?
- A.无需生成候选项目集← 正确答案
- B.只需扫描数据库两次← 正确答案
- C.不需要设置最小支持度
- D.通过共享前缀压缩数据← 正确答案
答案:ABD。A、B、D 分别是“无候选生成、扫描两遍、前缀共享”三大优势;C 错——FP-Growth 同样需要设置最小支持度阈值,这是频繁项挖掘的基本前提,无论 Apriori 还是 FP-Growth 都绕不开。
Q5. 当 FP-Tree 退化为单条路径时,如何生成所有频繁项集?
- A.直接枚举路径上全部非空子集← 正确答案
- B.重新扫描数据库合并计数
- C.丢弃该路径并重新构建树
- D.沿路径逐节点生成二元组合
答案:A。当条件 FP-Tree 退化为单条路径时,路径上所有项的组合都必然满足最小支持度,直接枚举全部非空子集即可,无需再递归建树。这是 FP-Growth 的重要剪枝优化,能大幅减少递归深度。就像一条长队里任意几个人组合都成立,直接数组合数就行。
Q6. 在最小支持度阈值已给定的前提下,下列哪个项集一定不是频繁项集?
- A.包含不频繁单项的项集← 正确答案
- B.只包含频繁单项的项集
- C.支持度恰好等于阈值的项集
- D.由两个频繁单项组成的项集
答案:A。根据向下闭包性:若单项 i 的支持度低于阈值,任何包含 i 的项集 X 的支持度都不可能超过 i(σ(X)≤σ(i)),所以 A 一定不频繁。B、D 只是“可能”频繁,C 达到阈值就是频繁项集。
Q7. 关于 FP-Growth 的挖掘过程,下列说法正确的有?
- A.从条件模式基构造条件 FP 树← 正确答案
- B.每个频繁项集在递归中被访问一次← 正确答案
- C.挖掘过程中需要反复扫描原始库
- D.递归终止于空树或单路径树← 正确答案
答案:ABD。挖掘过程用目标项的条件模式基构造条件 FP-Tree,再递归(A);每个频繁项集由唯一的递归路径产生,不会被重复枚举(B);递归终止于条件树为空或退化为单路径(D)。C 错在挖掘阶段并不扫描原始数据库,计数全部来自树结构。
Q8. 项集 X 的支持度应该如何计算?
- A.项集出现次数除以事务总数← 正确答案
- B.项集出现次数乘以事务总数
- C.事务总数除以项集出现次数
- D.项集出现次数除以频繁项个数
答案:A。支持度=项集出现次数÷事务总数,衡量项集在全部事务中的普遍程度。A 正确;B 除出来的数会大于 1 失去意义;C 除反了;D 的分母应该用总事务数而非频繁项个数。这就像“这道菜在所有桌里占了多少比例”。
Q9. 关联规则 A⇒B 的置信度公式是?
- A.σ(A∪B)/|D|
- B.σ(A∪B)/σ(A)← 正确答案
- C.σ(A∪B)/σ(B)
- D.σ(A)/σ(A∪B)
答案:B。置信度=同时买 A 和 B 的桌数÷买 A 的桌数,衡量在买 A 的顾客中买 B 的比例。B 正确;A 是支持度;C 分母用 B 会得到反向规则;D 是 B 推 A 的置信度。类比:点了番茄牛腩的客人里有多少也点了米饭。
Q10. 影响 FP-Growth 算法性能的关键因素包括哪些?
- A.事务间在前缀上的重合程度← 正确答案
- B.最小支持度阈值的设定← 正确答案
- C.数据库中事务的存储路径
- D.FP-Tree 节点的压缩率← 正确答案
答案:ABD。前缀重合度越高,树越紧凑、挖掘越快(A);阈值越低,频繁项与条件树越多,耗时越长(B);节点压缩率影响内存与遍历开销(D)。C 的存储路径属于工程环境因素,与算法本身的性能机理无直接关系。