FP-growth算法原理与Python实现:从频繁项集到关联规则挖掘
2026/9/15 5:56:55 网站建设 项目流程

简介:基于FP-growth频繁模式增长算法的Python实现与可视化工具,面向数据挖掘学习者、算法研究者和电商数据分析人员。资源完整涵盖关联规则学习、频繁项集挖掘、购物篮分析等方法,通过FP树结构可视化与运行示例,帮助理解大型数据库中的频繁模式发现流程。包体共11个文件,包含Python源码、whl依赖、CSV数据集、Markdown说明、PNG可视化图及txt文档等,整体约480KB,以代码和图文结合方式呈现,便于快速搭建FP-growth实验环境。目前已有75人学习下载。资源提供可直接运行的FP-tree实现、购物篮分析可视化演示、商品数据样本及详细说明文档,可辅助读者从原理推导到动手实践,掌握基于FP-growth的挖掘与关联规则生成方法,适合作为课程设计或项目实战参考。

1. 购物篮分析中的频繁项集挖掘:为什么是FP-growth而非Apriori

先抛结论:在百万级事务、平均每笔包含 10 个以上商品的数据集上,Apriori 的候选项集组合数量会迅速失控,而 FP-growth 通常能把分析压缩到秒级,且只依赖两遍数据库扫描。它的思路是典型的“空间换时间”:第一遍统计每个商品的出现次数,第二遍把所有事务压缩到一棵共享前缀的 FP 树里,之后频繁项集挖掘全部在这棵树上完成,不再触碰原始数据库。对做数据挖掘、机器学习特征工程和电商推荐的人来说,频繁项集是关联规则学习、捆绑推荐、异常购买行为检测的底层输入。这篇文章会把 FP-growth 的原理、Python 实现、FP 树结构可视化和面向大型数据库分析的调优串成一条可复现的路径,全程只用标准库加 Graphviz。

2. 从FP树到条件模式基:FP-growth的两遍扫描与挖掘原理

FP-growth 之所以比 Apriori 快,核心在于它不生成候选集。Apriori 每增加一个项就要扫描一遍数据去统计候选支持度,10 项集意味着大量重复 I/O。FP-growth 把“统计”提前压缩成一棵树,建完树之后,原始数据库就可以扔掉了。

2.1 第一遍扫描:统计频次并确定项头表

第一遍扫描只需要统计每个商品在多少笔事务中出现,得到一张支持度计数表,也就是项头表(Header Table)的原型。举一个最常见的购物篮场景,四笔订单如下:

TID购买商品按项头表排序后的路径
1牛奶、面包、啤酒啤酒→牛奶→面包
2牛奶、尿布、啤酒啤酒→尿布→牛奶
3面包、尿布、饼干尿布→面包
4牛奶、面包、尿布、啤酒啤酒→尿布→牛奶→面包

设最小支持计数 min_sup = 2,即某商品至少出现在 2 笔订单里才算频繁。第一遍扫描的结果:

商品支持计数是否保留排序位
啤酒3保留1
尿布3保留2
牛奶3保留3
面包3保留4
饼干1剔除-

排序规则我一般采用“支持度降序、同频按业务字典序”,目的是让高频项尽量靠近树根,让更多事务共享前缀路径。这里的四种商品同频,排序结果就按约定固定成“啤酒→尿布→牛奶→面包”。

2.2 第二遍扫描:按频次排序的路径插入与FP树构建

第二遍扫描时,每笔事务先去掉非频繁项,再按项头表顺序重排,然后从根节点开始逐项插入 FP 树。树中每个节点记录两个信息:商品项本身,以及经过该节点的累计次数。新事务与现有路径共享前缀时,只把共用节点的次数加一,只有分叉处才新建节点。

上面四条事务插入后的 FP 树结构如下:

root ├── 啤酒:3 │ ├── 牛奶:1 │ │ └── 面包:1 │ └── 尿布:2 │ └── 牛奶:2 │ └── 面包:1 └── 尿布:1 └── 面包:1

节点“啤酒:3”表示有三笔事务从啤酒开始;节点“尿布:2”表示在“啤酒”之后接“尿布”的路径出现了两次;“牛奶:2”是“啤酒→尿布→牛奶”这条路径的计数。树里保留了支持计数这个全局信息,后续挖掘不需要重新统计原始事务。

项头表在这时补上了第二列:每个项对应的节点链表。因为同一个商品会出现在多条分支上,把头指针指向第一个节点,再通过节点内的 link 指针串起所有同项节点。比如“尿布”在树里有“啤酒→尿布”和根节点直连“尿布”两个出现位置,链表就把它们连起来,方便挖掘时快速找到所有路径。

2.3 递归挖掘条件FP树,得到所有频繁项集

建树完成后进入递归挖掘阶段。对项头表中的每个项,先向上回溯其所有路径,收集条件模式基(Conditional Pattern Base),再用这些路径单独构造一棵条件 FP 树,在条件树上继续递归,直到无法再生成更高阶的项集。这个过程的伪代码逻辑是:

def mine(header, min_sup, prefix, result): for item in 按支持度升序遍历 header: new_prefix = prefix + [item] result.add((new_prefix, header[item].support)) cond_patterns = 收集 item 所有节点的向上路径及计数 cond_tree = 用 cond_patterns 构造条件FP树 if cond_tree 非空: mine(cond_tree, min_sup, new_prefix, result)

还是看前面的例子。挖掘“牛奶”时,它的三个出处分别是“啤酒→牛奶:1”“啤酒→尿布→牛奶:2”,所以条件模式基是{啤酒}:1{啤酒,尿布}:2。用这两条路径构造条件 FP 树后,能继续筛出“啤酒:3”和“尿布:2”两个频繁条件项,于是得到频繁项集{啤酒,牛奶}:3{尿布,牛奶}:2,以及继续递归得到的{尿布,啤酒,牛奶}:2。整个过程只处理当前项的局部路径,不扫描整库,这是 FP-growth 命名里“Pattern Growth”的由来。

3. 用Python从零实现FP-growth:可直接运行的源码

原理落到代码上,其实不到一百行。这里给出一份我自己项目里常用的最小实现,适合边读边改。

3.1 数据结构定义与构建树函数

class FPNode: """FP树节点。children 用 dict 存孩子,link 指向下一个同项节点。""" def __init__(self, item=None, count=0, parent=None): self.item = item self.count = count self.parent = parent self.children = {} self.link = None def build_fp_tree(transactions, min_sup): """transactions: 事务列表,每个元素是商品集合;min_sup: 最小支持计数""" # 第一遍扫描:统计频次 item_counts = {} for trans in transactions: for item in set(trans): item_counts[item] = item_counts.get(item, 0) + 1 # 过滤非频繁项,按频次降序、字典序升序排列 header = {item: [count, None] for item, count in item_counts.items() if count >= min_sup} if not header: return None, None ordered = sorted(header.keys(), key=lambda x: (-header[x][0], x)) root = FPNode() for trans in transactions: items = [item for item in ordered if item in trans] if items: _insert(items, 1, root, header) return root, header def _insert(items, count, node, header): """把一条路径插入FP树,并维护项头表链。count 用于条件模式基的加权插入。""" first = items[0] if first in node.children: child = node.children[first] child.count += count else: child = FPNode(first, count, node) node.children[first] = child if header[first][1] is None: header[first][1] = child else: cur = header[first][1] while cur.link: cur = cur.link cur.link = child if len(items) > 1: _insert(items[1:], count, child, header)

这段代码里,header[item]存的是二元组[支持计数, 节点链首]。第一遍扫描完成后直接用它当项头表,省一次字典拷贝。ordered的排序决定 FP 树形态,同一个数据集排序不同,树形不同,但挖掘结果一致,只是树紧凑程度有差异。

_insert里有个细节:while cur.link逐节点找链表尾部再挂新节点。链表较长时这一步会慢,工程上可以改成项头表直接维护头尾两个指针,插入从 O(n) 退化为 O(1)。探索原型阶段不必做,大数据量时再优化。

3.2 递归挖掘频繁项集的实现

def find_frequent_patterns(header, min_sup, prefix, result): """递归挖掘频繁项集,结果写入 result 列表。""" for item in sorted(header.keys(), key=lambda x: header[x][0]): new_prefix = prefix + [item] result.append((new_prefix, header[item][0])) # 收集条件模式基 cond_base = {} node = header[item][1] while node: path = [] parent = node.parent while parent is not None and parent.item is not None: path.append(parent.item) parent = parent.parent if path: key = tuple(path) cond_base[key] = cond_base.get(key, 0) + node.count node = node.link if not cond_base: continue # 展开条件模式基,构建条件FP树 weighted_trans = [] for path, cnt in cond_base.items(): weighted_trans.extend([list(path)] * cnt) cond_root, cond_header = build_fp_tree(weighted_trans, min_sup) if cond_header is not None: find_frequent_patterns(cond_header, min_sup, new_prefix, result) def fp_growth(transactions, min_sup_ratio=0.1, min_sup=None): """对外入口。min_sup_ratio 是相对支持度,min_sup 是绝对计数,二选一。""" transactions = list(transactions) n = len(transactions) if min_sup is None: min_sup = max(1, int(n * min_sup_ratio)) root, header = build_fp_tree(transactions, min_sup) result = [] if header is not None: find_frequent_patterns(header, min_sup, [], result) return result

find_frequent_patterns里,sorted(... key=lambda x: header[x][0])让低频项先挖掘。挖掘顺序不影响结果的完整性,但低频项的条件模式基更短,递归展开更轻,实战中整体节奏更平稳。cond_basetuple(path)做 key,把相同前缀路径合并计数,再按出现次数展开,逻辑清晰但内存有浪费,第 5 章会讲替换方案。

权重cnt直接乘以路径重复次数,是因为路径计数等于该购物路径在原始事务中出现过的次数,这是 FP-growth 能省掉重新扫描事务库的关键前提。

3.3 支持度参数与数据集格式约定

跑通这段代码只需要 Python 3.7 以上版本,不用装第三方库。下面用一个最小示例验证:

if __name__ == '__main__': transactions = [ ['牛奶', '面包', '啤酒'], ['牛奶', '尿布', '啤酒'], ['面包', '尿布', '饼干'], ['牛奶', '面包', '尿布', '啤酒'], ] patterns = fp_growth(transactions, min_sup_ratio=0.5) for items, sup in sorted(patterns, key=lambda x: (-len(x[0]), -x[1])): print(f"{', '.join(items)} -> {sup}")

输出会包含面包, 牛奶, 啤酒 -> 2尿布, 牛奶, 啤酒 -> 2牛奶, 啤酒 -> 3等结果。参数约定如下:

参数含义我的默认值备注
min_sup_ratio相对支持度阈值0.110% 事务包含该项才保留
min_sup绝对支持计数None传了就用绝对计数,忽略 ratio
事务格式list 或生成器-元素必须是可去重的可迭代对象
重复商品同一事务内自动去重-建议输入前自行清洗脏数据

4. FP树结构可视化与关联规则自动生成

算法可以跑通还不够,FP 树一旦分叉多,肉眼基本看不出结构。FP 树结构可视化不仅是给学生看原理用的,也是做数据挖掘项目时向业务方解释“规则从哪来”最直接的手段。

4.1 导出Graphviz dot并渲染FP树PNG

我习惯用 Graphviz 的 dot 格式输出,因为它既能生成静态图片,也能转成 SVG 放进网页报告。下面这段代码把整棵树递归导出成 dot 文件:

def export_fp_tree_dot(root, filepath='fp_tree.dot'): lines = ['digraph FP_Tree {', ' node [shape=circle, fontname="Helvetica"];', ' edge [arrowhead=none];'] counter = [0] node_ids = {} def get_id(node): if node not in node_ids: node_ids[node] = f'n{counter[0]}' counter[0] += 1 return node_ids[node] def walk(node): nid = get_id(node) label = f'{node.item}:{node.count}' if node.item else 'root' lines.append(f' {nid} [label="{label}"];') for child in node.children.values(): cid = get_id(child) lines.append(f' {nid} -> {cid};') walk(child) walk(root) lines.append('}') with open(filepath, 'w', encoding='utf-8') as f: f.write('\n'.join(lines)) print(f'dot 文件: {filepath}')

调用方式:

python fp_growth.py dot -Tpng fp_tree.dot -o fp_tree.png

get_id用 Python 对象在node_ids里的自增编号保证节点名唯一,避免两个值相同但位置不同的商品节点被 Graphviz 合并。没有 Graphviz 时,先apt install graphviz或 macOS 上用brew install graphviz。输出 PNG 里每个节点标着“商品名:支持次数”,能直接看出高支持度路径在树的上半部分,低频项则在深层稀疏分支。

4.2 关联规则学习:置信度与提升度的计算与过滤

频繁项集本身只是“共同出现”的统计,转成可解释规则还要算置信度和提升度。置信度是“买了 A 的前提下买 B 的概率”,提升度衡量这条规则比随机购买强多少:

from itertools import combinations def generate_association_rules(freq_patterns, total_trans, min_conf=0.6): """从频繁项集生成关联规则,返回 antecedent, consequent, conf, lift""" sup_dict = {frozenset(items): sup for items, sup in freq_patterns} rules = [] for items, sup in freq_patterns: itemset = frozenset(items) if len(itemset) < 2: continue for i in range(1, len(itemset)): for antecedent_tuple in combinations(items, i): antecedent = frozenset(antecedent_tuple) consequent = itemset - antecedent if not consequent: continue support_ante = sup_dict[antecedent] confidence = sup / support_ante if confidence >= min_conf: support_con = sup_dict[consequent] lift = confidence / (support_con / total_trans) rules.append((antecedent, consequent, confidence, lift)) return rules

参数说明:total_trans是总事务数,计算提升度时要用到;min_conf是置信度下界,业务上我一般先设 0.6 看结果分布,太稠密再提到 0.8。以第 2 章的购物篮数据为例,牛奶 -> 啤酒的置信度是 3/3 = 1.0,提升度 1.33,说明牛奶购买者购买啤酒的倾向明显高于全站平均。输出规则后按(lift, confidence)双排序,前几条往往就是可落地的捆绑推荐候选。

5. 面向大型数据库分析的FP-growth调优

前面几节跑通的是内存能装下的数据集。真正面对千万级订单、GB 级 CSV 文件时,代码要过几道坎:事务读取、条件模式基展开、递归深度、支持度阈值选择。

5.1 事务流式读取与两遍扫描的工程处理

FP-growth 要扫描两遍数据库,但生成器只能消费一次。对超大型数据,我采用“第一遍落盘”方案:第一遍流式读取原始文件,过滤非频繁项并排序后,把有序事务写进临时文件;第二遍再顺序读临时文件建树。

def read_baskets(csv_path): """按客户ID聚合购物篮,逐篮产出商品集合。""" current_customer = None basket = set() with open(csv_path, newline='', encoding='utf-8') as f: for row in csv.DictReader(f): customer = row['customer_id'] if customer != current_customer: if basket: yield basket basket = set() current_customer = customer basket.add(row['product_id']) if basket: yield basket

关键在这里:yield basket让整份数据不驻留内存,每个客户一个集合。第一遍统计商品频次时直接对这个生成器做 Counter 累加,得到频繁项列表后写临时文件。

瓶颈常见现象对策
事务读取内存涨到 OOM生成器 + 分组聚合
离群长事务单条路径极深,建树慢限制单事务项数上限
条件模式基展开递归时内存暴增用加权插入替代 list 展开

5.2 最小支持度的选择、递归深度与运行时长特征

最小支持度的设置直接决定结果规模。经验上,零售购物篮数据先用相对支持度 0.01 试跑,看频繁项数量级;项超过 10 万条就上调到 0.02,少于几千条就往下探。绝对计数方式适合长尾品类,比如“至少被 500 个用户购买过”,避免热门商品把低频但高价值的规则淹没。

递归挖掘深度接近商品种类数,Python 默认递归上限只有 1000,深层数据会抛RecursionError。我在项目启动时会加一行:

import sys sys.setrecursionlimit(10000)

运行时长特征方面,FP-growth 的耗时主要集中在条件树构建,理论上与频繁模式数量近似成正比。如果某次分析比预期慢很多,优先检查是不是支持度阈值过低导致条件模式基爆炸,而不是去优化树节点数据结构。

5.3 条件模式基的加权插入优化与结果交叉验证

第 3 章的weighted_trans.extend([list(path)] * cnt)在挖掘长路径时会把内存打满。生产环境里,_insert本来就支持count参数,所以条件模式基直接按(path, cnt)加权建树即可:

def build_cond_tree_from_base(cond_base, header, min_sup): """cond_base: {tuple(路径): 计数}""" cond_root = FPNode() cond_header = {} for path, cnt in cond_base.items(): valid = [item for item in header if item in path] if valid: _insert(valid, cnt, cond_root, cond_header) # 过滤掉不满足 min_sup 的项 for item in [k for k, v in cond_header.items() if v[0] < min_sup]: del cond_header[item] return cond_root, cond_header

这样前端不用展开成重复事务列表,内存占用和条件树构建时间都能降一个数量级。注意_insert会把同一个路径的计数直接累加到共享节点上,所以条件树节点计数天然就是该前缀路径在条件模式基里的支持数。

最后提一个验收习惯:实现完成后,拿mlxtend.frequent_patterns.fpgrowth做交叉验证,它的输入是 one-hot 的 pandas DataFrame,min_support=0.5对应绝对计数 2。两边输出的频繁项集做对称差,如果为空说明实现没问题。迁移到大规模数据前先跑这条校验,能一次性兜住条件模式基计数、排序协议、链式头表三类最容易出错的边界情况,我在旧项目里就是靠这条验收命令放心的。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询