FP-Growth关联规则挖掘实战:从原理到调优避坑
简介:这份资源是面向数据挖掘初学者与MATLAB使用者的FP-Growth关联规则挖掘实现包,用于在交易数据中高效发现频繁项集并提取关联规则,可应用于市场篮子分析、网络行为分析等场景。压缩包共8个文件,以7个.m脚本文件和1个.mat数据集为主,脚本分别承担主流程调度、FP树构建、幂集与子集生成、规则展示及树结构可视化等职责,mydata数据集用于演示算法运行,整体约5KB,轻量易读。已有191人学习下载。读者可借此理解FP-Growth通过FP树存储频繁项集、减少数据扫描次数的核心思路,掌握从预处理、构造FP树、生成条件模式基到递归挖掘与规则提取的完整流程,并可通过修改数据集与支持度阈值迁移到自己的应用场景,为后续复杂数据挖掘任务打下基础。
1. 从一份 FP-Growth 关联规则挖掘压缩包说起:它到底能解决什么问题
你手上有一份名为FP-Growth Assiciation Rule Mining.rar的压缩包,名字里两个关键词已经把技术路线钉死了:FP-Growth 和 Association Rule Mining。前者是频繁模式树的构建与挖掘算法,后者是关联规则挖掘这个任务本身。很多人第一次接触它,是在购物篮分析、推荐系统召回、日志共现分析或者故障根因定位这类场景里——手里有一堆事务型数据,想知道“买了 A 的人还会买什么”“出现告警 X 之后通常跟着告警 Y”。Apriori 能算,但候选集爆炸和多次扫库会让它在中等规模数据上就喘不过气,FP-Growth 用一棵压缩前缀树把事务压进去,只扫两遍数据库,再递归挖条件模式基,这就是它值得单独打包一份代码来跑的原因。这份压缩包适合两类人:一类是要快速跑通关联规则、拿到可解释规则集的算法工程师;另一类是想搞懂 FP-Tree 到底怎么建、怎么挖,而不满足于调库的开发者。下面我按“先立住原理、再动手复现、最后讲坑”的顺序把它拆开。
2. FP-Growth 的算法骨架:为什么它比 Apriori 少扫库
2.1 从频繁项集到关联规则的两段式任务
关联规则挖掘的标准定义是:给定事务集合 $D$,每条事务是若干项的集合,我们要找出所有满足最小支持度min_sup的频繁项集,再从频繁项集里生成满足最小置信度min_conf的规则 $X \rightarrow Y$。支持度是 $X \cup Y$ 出现的比例,置信度是 $P(Y|X)$。这里有两个必须记住的边界:支持度是“同时出现”的频次占比,置信度是条件概率,两者不能混。很多人第一次翻车就是把置信度当成支持度去筛,结果规则数量爆炸还全是噪声。
Apriori 的做法是逐层生成候选集,每层都要重新扫一遍数据库来验证支持度,数据库一大,I/O 就是瓶颈。FP-Growth 换了个思路:先把整个数据库压进一棵 FP-Tree,树上的每个节点记录项和计数,共享前缀的事务会合并到同一条路径上,这样数据库的共现结构被一次性编码进内存。之后挖掘频繁项集不再扫库,而是在树上递归地构造条件模式基和条件 FP-Tree。这就是它“只扫两遍库”的由来:第一遍统计每个项的支持度并过滤掉低于min_sup的项,第二遍按支持度降序把每条事务插入树。
2.2 FP-Tree 的构建:排序、过滤、插入三步
构建 FP-Tree 的核心动作只有三步,但每一步都有参数和顺序讲究。第一步扫库统计单项频次,丢弃低于min_sup的项,这叫“过滤低频项”。第二步对每条事务里的项按全局支持度降序排列,这一步是 FP-Tree 能共享前缀的前提,顺序不一致树就退化成普通前缀树,压缩效果大打折扣。第三步逐条插入,遇到相同前缀就复用节点并累加计数,遇到新项就开分支,同时维护一个头表(header table),把每个项的所有节点用链表串起来,方便后续从底向上挖条件模式基。
下面这段 Python 用纯字典和列表实现 FP-Tree 的构建,不依赖任何第三方库,方便你直接对照压缩包里的实现看差异:
# fp_tree_build.py # 纯 Python 实现 FP-Tree 构建,便于理解压缩包内算法的数据结构 class FPNode: def __init__(self, name, count, parent): self.name = name # 项名 self.count = count # 计数 self.parent = parent # 父节点 self.children = {} # 子节点字典 self.node_link = None # 头表链表指针 def increment(self, cnt): self.count += cnt def build_fp_tree(transactions, min_sup): # 第一遍:统计单项支持度 header = {} for trans in transactions: for item in trans: header[item] = header.get(item, 0) + 1 # 过滤低频项,保留满足 min_sup 的项 header = {k: v for k, v in header.items() if v >= min_sup} if not header: return None, None # 头表:项 -> [计数, 链表头节点] header_table = {k: [v, None] for k, v in header.items()} root = FPNode('Null Set', 1, None) # 第二遍:按支持度降序插入每条事务 for trans in transactions: # 只保留频繁项,并按全局支持度降序排序 filtered = [item for item in trans if item in header] filtered.sort(key=lambda x: header[x], reverse=True) cur = root for item in filtered: if item in cur.children: cur.children[item].increment(1) else: new_node = FPNode(item, 1, cur) cur.children[item] = new_node # 更新头表链表 if header_table[item][1] is None: header_table[item][1] = new_node else: node = header_table[item][1] while node.node_link: node = node.node_link node.node_link = new_node cur = cur.children[item] return root, header_table逻辑说明:header先做全局频次统计,min_sup是绝对计数还是比例取决于你传入的数值,压缩包里常见做法是传比例再乘以事务总数换算成计数。filtered.sort的排序键是全局支持度,不是事务内顺序,这一点如果写反,树会变得又宽又深。头表链表用尾插法维护,保证从任意节点都能顺着node_link找到同项的所有出现位置,这是后续挖掘条件模式基的入口。参数上,min_sup设得越高,树越小、规则越少;设得越低,树越稠密、递归越深,内存和时间都会上去。
2.3 条件模式基与递归挖掘:从叶子往上找前缀路径
树建好之后,挖掘从支持度最低的项开始,因为它在头表链表里对应的路径最短。对每个项,沿着node_link找到所有节点,从每个节点往父节点回溯,得到一条前缀路径,路径上每个节点的计数取该节点的计数,这些路径合起来就是“条件模式基”。把条件模式基当成新的事务集合,重新统计频次、过滤、建一棵条件 FP-Tree,然后递归下去。递归的终止条件是条件 FP-Tree 只剩单条路径,这时直接枚举这条路径上所有子集与当前后缀的组合即可。
# fp_growth_mine.py # 递归挖掘频繁项集 def ascend_tree(node): # 从节点回溯到根,返回前缀路径 path = [] while node.parent is not None: path.append(node.name) node = node.parent return path[::-1] def find_prefix_paths(tree_node): # 收集某个项的所有条件模式基 paths = {} while tree_node is not None: path = ascend_tree(tree_node) if path: paths[tuple(path)] = paths.get(tuple(path), 0) + tree_node.count tree_node = tree_node.node_link return paths def mine_tree(header_table, min_sup, prefix, freq_items): # 按支持度升序处理头表项 sorted_items = sorted(header_table.items(), key=lambda x: x[1][0]) for item, (count, node) in sorted_items: new_prefix = prefix + [item] freq_items.append((new_prefix, count)) # 构造条件模式基 cond_paths = find_prefix_paths(node) # 用条件模式基重建事务列表 cond_trans = [] for path, cnt in cond_paths.items(): cond_trans.extend([list(path)] * cnt) # 递归建条件 FP-Tree cond_root, cond_header = build_fp_tree(cond_trans, min_sup) if cond_header: mine_tree(cond_header, min_sup, new_prefix, freq_items)逻辑说明:ascend_tree返回的是从根到当前节点的项序列,find_prefix_paths把同项所有节点的前缀路径按计数累加,得到条件模式基。mine_tree里对头表按计数升序排序,是为了先挖支持度低的项,减少后续递归的搜索空间。cond_trans.extend([list(path)] * cnt)是把路径按计数展开成事务,这一步在数据量大时是内存热点,压缩包里如果做了优化,通常会用生成器或直接传计数而不是展开。参数上,递归深度受最长事务长度限制,min_sup在递归中保持不变,但条件 FP-Tree 的规模会逐层缩小。
3. 把压缩包跑起来:环境、入口与最小复现路径
3.1 解压后的目录结构与入口判断
拿到FP-Growth Assiciation Rule Mining.rar,第一步不是急着跑,而是先看清里面是什么。常见做法是先解压到一个干净目录,用tree或find列出文件,判断它是纯脚本、带 notebook,还是带数据集和配置。典型的关联规则挖掘包会有这么几类文件:算法实现(fp_growth.py或fp_tree.py)、数据加载与预处理脚本、一个main或run入口、示例数据集(.txt/.csv/.dat),以及依赖说明。如果压缩包里只有算法文件没有入口,你就得自己写一个最小驱动脚本,把数据读进来、调算法、输出规则。
# 解压并查看结构 mkdir -p fp_growth_work && cd fp_growth_work # 假设压缩包在当前目录的上一级 unrar x ../FP-Growth\ Assiciation\ Rule\ Mining.rar # 或者用 7z / unzip 视实际格式而定 find . -maxdepth 3 -type f | sort逻辑说明:unrar x保留目录结构解压,find列出三层以内的文件,先确认有没有requirements.txt、README或入口脚本。如果压缩包里是.py文件,直接看文件头的 import 判断依赖;如果是 notebook,注意单元格执行顺序,notebook 最容易出现“跳着跑就报未定义”的问题。参数上,解压路径不要带空格和中文,很多老脚本对路径编码处理不干净,带空格会直接报文件找不到。
3.2 数据格式与最小可跑数据集
关联规则挖掘的输入是事务集合,常见格式有两种:一种是每行一条事务、项之间用空格或逗号分隔,比如bread milk beer;另一种是稀疏矩阵或 one-hot 编码的 CSV。压缩包里的示例数据大概率是第一种,因为 FP-Growth 天然吃这种格式。如果你要换成自己的数据,先做一件事:确认每行的项没有重复,重复项会导致计数虚高。下面是一个最小数据集和读取脚本:
# load_data.py # 读取空格分隔的事务数据,返回列表的列表 def load_transactions(path): transactions = [] with open(path, 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue # 按空格切分,过滤空项 items = [x for x in line.split() if x] # 去重,避免同一事务内重复项干扰计数 items = list(dict.fromkeys(items)) transactions.append(items) return transactions if __name__ == '__main__': trans = load_transactions('sample.dat') print('事务数:', len(trans)) print('首条事务:', trans[0] if trans else '空')逻辑说明:dict.fromkeys在保持顺序的前提下去重,比set更安全,因为后续排序依赖项的顺序稳定性。encoding='utf-8'要显式写,Windows 默认编码读中文项名会乱码。参数上,如果你的数据项本身包含空格,就不能用空格切分,得换成制表符或逗号,并在读取时对应调整split的分隔符。
3.3 跑通一次完整挖掘:支持度、置信度与规则输出
最小复现路径是:加载数据 → 建 FP-Tree → 挖频繁项集 → 生成关联规则 → 输出。生成规则时,对每个频繁项集,枚举它的非空真子集作为前件,剩余部分作为后件,计算置信度,过滤掉低于min_conf的规则。下面这段把前面的构建和挖掘串起来,并加上规则生成:
# run_mining.py from fp_tree_build import build_fp_tree from fp_growth_mine import mine_tree from load_data import load_transactions from itertools import combinations def generate_rules(freq_items, min_conf): rules = [] for itemset, support in freq_items: if len(itemset) < 2: continue for i in range(1, len(itemset)): for antecedent in combinations(itemset, i): consequent = tuple(x for x in itemset if x not in antecedent) # 计算前件支持度 ant_support = None for s, sup in freq_items: if set(s) == set(antecedent): ant_support = sup break if ant_support and support / ant_support >= min_conf: rules.append((antecedent, consequent, support, support / ant_support)) return rules if __name__ == '__main__': transactions = load_transactions('sample.dat') min_sup_count = 2 # 绝对支持度计数 min_conf = 0.6 # 最小置信度 root, header = build_fp_tree(transactions, min_sup_count) freq_items = [] if header: mine_tree(header, min_sup_count, [], freq_items) rules = generate_rules(freq_items, min_conf) for ant, con, sup, conf in sorted(rules, key=lambda x: -x[3])[:20]: print(f"{ant} -> {con} 支持度={sup} 置信度={conf:.2f}")逻辑说明:min_sup_count这里用绝对计数,和build_fp_tree里的过滤逻辑对应;如果你习惯用比例,就在加载后乘以事务总数换算。generate_rules里对每个频繁项集枚举前件,combinations保证不重复,ant_support从前件对应的频繁项集里取,取不到就跳过。参数上,min_conf设 0.6 到 0.8 之间通常能得到可读性较好的规则,设太低会输出大量弱关联,设太高可能一条规则都没有。输出按置信度降序取前 20 条,方便快速看效果。
4. 参数调优与性能边界:支持度、置信度和内存怎么权衡
4.1 min_sup 的设定:从数据分布反推而不是拍脑袋
min_sup是 FP-Growth 里最敏感的参数,它直接决定树的大小和递归深度。设得太高,频繁项集少、规则少,可能漏掉有价值的弱关联;设得太低,树会变得极稠密,内存和时间都飙升,甚至递归爆栈。我的习惯是先做一次单项频次统计,看支持度的分布,再选一个能过滤掉长尾但保留头部共现的值。比如事务总数 10 万,你希望一个项至少出现 500 次才参与挖掘,那min_sup就是 0.005 或绝对计数 500。下面这段帮你快速看分布:
# sup_distribution.py from collections import Counter from load_data import load_transactions trans = load_transactions('sample.dat') counter = Counter() for t in trans: counter.update(t) total = len(trans) print('事务总数:', total) for item, cnt in counter.most_common(20): print(f'{item}: 计数={cnt} 支持度={cnt/total:.4f}')逻辑说明:Counter.update累加所有事务的项频次,most_common(20)看头部项的分布。如果头部项支持度都在 0.1 以上,而长尾项低于 0.001,min_sup取 0.01 左右通常能兼顾效果和性能。参数上,这个脚本只做诊断,不参与挖掘,但它是你定min_sup的依据,比反复试错快得多。
4.2 置信度与提升度:别只看置信度选规则
置信度高不代表规则有价值,因为后件本身可能就很频繁。比如“买牛奶 → 买面包”置信度 0.9,但如果面包本身在 90% 的事务里都出现,这条规则等于没说。这时候要看提升度(lift),定义是置信度 / 后件支持度,大于 1 才说明前件对后件有正向提升。压缩包里如果只输出支持度和置信度,建议自己补一列提升度。下面是在规则生成里加提升度的改法:
# 在 generate_rules 中补充后件支持度查询 def consequent_support(consequent, freq_items): for s, sup in freq_items: if set(s) == set(consequent): return sup return None # 计算 lift = conf / (consequent_support / total_transactions)逻辑说明:后件支持度同样从频繁项集里查,查不到说明后件不是频繁项,规则本身也不该保留。lift > 1表示正相关,lift = 1表示独立,lift < 1表示负相关。参数上,我一般会同时卡min_conf >= 0.6和lift >= 1.2,这样输出的规则既有强度又有增量价值。
4.3 大数据下的内存控制:条件模式基展开是热点
前面提到cond_trans.extend([list(path)] * cnt)在数据量大时会吃内存,因为一条路径按计数展开可能变成几千条事务。优化方向有两个:一是用生成器惰性产出事务,二是在建条件 FP-Tree 时直接传“路径 + 计数”而不是展开。压缩包里如果没做这层优化,你在百万级事务上跑就会看到内存曲线陡增。另一个边界是递归深度,Python 默认递归限制 1000,长事务数据可能触发RecursionError,可以通过sys.setrecursionlimit临时放宽,但更稳的做法是把递归改成显式栈。参数上,如果单条事务平均长度超过 50,就要警惕递归深度和条件模式基的规模。
5. 避坑与排查:FP-Growth 落地时最容易翻车的 5 个点
5.1 现象:规则数量为 0,但数据明明有共现
原因通常是min_sup设成了比例却按绝对计数传,或者反过来。比如事务总数 1000,你传min_sup=0.01给按计数过滤的函数,等于要求项出现至少 0.01 次,所有项都通过,树巨大;反过来传min_sup=100给按比例过滤的函数,等于要求支持度 100,一条规则都出不来。解决:在入口处统一单位,打印一次min_sup的实际值和事务总数,确认量纲一致。
5.2 现象:同一事务内重复项导致支持度虚高
原因是你没去重,一条事务里milk milk bread被当成两个 milk,计数比实际多。解决:加载数据时用dict.fromkeys或set去重,但注意set会打乱顺序,如果后续依赖顺序,用保序去重。这个坑在日志共现分析里特别常见,因为日志里同一条消息可能重复出现。
5.3 现象:树建好了但挖掘结果和 Apriori 对不上
原因多半是排序键写错。FP-Tree 要求按全局支持度降序插入,如果你按项名字母序或事务内顺序排,树的结构会变,虽然理论上频繁项集不变,但条件模式基的构造会受影响,某些实现下会漏项。解决:确认filtered.sort(key=lambda x: header[x], reverse=True)这一行没被改,且header是过滤后的频次表。
5.4 现象:递归挖掘报 RecursionError 或跑很久不结束
原因是min_sup太低导致条件 FP-Tree 一直有分支,递归深度超过 Python 限制,或者条件模式基展开后事务数爆炸。解决:先提高min_sup看是否缓解,如果业务上不能提高,就把递归改成显式栈,或者对条件模式基做采样。另一个检查点是头表链表是否有环,链表维护写错会导致find_prefix_paths死循环。
5.5 现象:规则里出现单一项的前件或后件
原因是生成规则时没有过滤len(itemset) < 2,或者枚举子集时允许了空集。解决:在generate_rules开头加if len(itemset) < 2: continue,并确保combinations的i从 1 到len(itemset)-1,这样前件和后件都非空。这个坑不致命但会让输出很难看,评审时容易被质疑。
6. 进阶技巧:用规则做召回排序与增量更新
跑通基础挖掘之后,真正体现价值的是把规则用起来。我一般会把规则按lift和confidence加权打分,用于推荐召回:对用户已购项,匹配所有前件包含这些项的规则,把后件作为候选,分数用confidence * lift排序。这样比单纯按置信度排更能压掉热门项的干扰。另一个进阶点是增量更新:数据每天新增,全量重建 FP-Tree 成本高,可以维护一个增量事务缓冲,定期合并后重建,或者对新增数据单独挖规则再和旧规则做并集去重。下面是一个规则打分的片段:
# rule_scoring.py def score_rules(rules, user_items): # rules: [(antecedent, consequent, support, confidence, lift)] candidates = {} user_set = set(user_items) for ant, con, sup, conf, lift in rules: if set(ant).issubset(user_set): for item in con: score = conf * lift if item not in candidates or score > candidates[item]: candidates[item] = score return sorted(candidates.items(), key=lambda x: -x[1])逻辑说明:set(ant).issubset(user_set)判断用户已购项是否覆盖规则前件,覆盖则后件进候选,分数取conf * lift,同一后件保留最高分。参数上,lift建议在规则生成阶段就算好并存入规则元组,避免打分时重复查表。这个打分函数可以直接接在召回层后面,作为一路特征。
最后说个我自己的习惯:每次跑完 FP-Growth,我都会把规则按前件长度分组看一遍,长度 1 的前件规则用于冷启动召回,长度 2 以上的用于精细化推荐,如果某一组数量异常少或异常多,先回去查min_sup和去重逻辑,而不是急着调模型。这套流程我踩过不少坑,希望帮到你。
本文还有配套的精品资源,点击获取