
简介基于FP-growth频繁模式增长算法的Python实现与可视化工具面向数据挖掘、机器学习初学者及需要做购物篮分析或频繁项集挖掘的开发者。资源围绕FP树构建、频繁项集挖掘与关联规则学习展开提供完整Python实现与可视化方案可帮助理解FP-growth相较于Apriori只需两次扫描数据库的高效原理并直接用于大型数据库分析。压缩包共11个文件含Python脚本、whl依赖包、CSV数据集、PNG可视化图、Markdown说明文档、txt说明及docx附赠资料包体约480KB结构清晰便于对照学习。目前已有75人学习。通过源码演示、数据集与图文笔记读者可掌握FP树结构可视化方法理解频繁模式发现过程并能在购物篮分析场景中实际应用适合作为课程设计或项目实践的参考资料。1. 购物篮分析跑不动时先想想是不是该换 FP-growth 了电商、零售、供应链团队做关联规则时最常遇到的不是没有数据而是 Apriori 在千万级事务表上频繁项集挖掘慢到没法用。支持度阈值设到 0.02 还说得过去一旦想分析长尾商品、把阈值压到 0.005 以下Apriori 反复扫描全表、逐层生成候选集的成本会指数级膨胀。FP-growthFrequent Pattern Growth频繁模式增长把事务集压缩进一棵 FP 树只扫两遍原始数据就完成所有频繁项集的挖掘购物篮分析、交叉销售分析这类场景因此成为它的典型落地对象。本文从树结构原理讲起给出可直接运行的 Python 实现、FP 树可视化方法以及从频繁项集到关联规则的一整套参数设定思路。适合数据挖掘工程师、算法岗学习者以及想自己动手搭一套商品关联分析工具的人。2. 为什么 FP-growth 快FP 树压缩的不是数据量是候选集规模2.1 Apriori 的瓶颈与 FP-growth 的破局点Apriori 的核心逻辑是由 k 项集生成 k1 项候选集再回表数支持度这个过程有两个代价一是候选集可能远大于实际频繁项集尤其在项数多而密集的数据里二是每次筛选都要重新扫描数据库。FP-growth 换了个思路不再逐层生成候选而是先把事务里的频繁项按支持度降序排好依次插入一棵前缀共享树树里的每个节点代表一个项节点计数代表路径重复次数。挖掘时只需要从支持度最低的频繁项开始沿着树回溯不碰非频繁项。这种设计的直接效果是事务数量越大、项越长FP-growth 的相对优势越明显。它把密度信息压缩在树里后续所有递归都发生在内存中的条件 FP 树里内存消耗比 Apriori 低一个量级是常见现象。不过代价也很明确FP 树的构建依赖项的顺序同一个频繁项集. 条路径的排列顺序会影响树的紧凑程度所以排序步骤不能省。2.2 FP 树构建的最小实现下面先给一个结构清晰的 FP 树节点与建树实现不依赖任何外部库方便理解树长什么样。from collections import defaultdict class FpNode: def __init__(self, item, count1, parentNone): self.item item # 项名根节点为 None self.count count # 路径经过该节点的次数 self.parent parent # 父节点引用 self.children {} # 子节点 {项名: FpNode} class FpTree: def __init__(self, transactions, min_sup): self.min_sup min_sup self.header {} # head指针表 {项名: (支持度, 节点链)} self.root FpNode(None) # 第一遍扫描统计所有项的频次 item_sup defaultdict(int) for trans in transactions: for item in set(trans): item_sup[item] 1 # 过滤非频繁项并按支持度降序、名称升序排列 self.freq_items { item: sup for item, sup in item_sup.items() if sup min_sup } self.rank { item: idx for idx, (item, _) in enumerate( sorted(self.freq_items.items(), keylambda x: (-x[1], x[0]))) } # 第二遍扫描把事务映射成排序后的频繁项序列插入树 for trans in transactions: its sorted( (item for item in set(trans) if item in self.rank), keylambda x: self.rank[x]) self._insert(its, 1) def _insert(self, items, count): node self.root for item in items: if item in node.children: node.children[item].count count else: node.children[item] FpNode(item, count, node) node node.children[item] if item in self.header: self.header[item][1].append(node) else: self.header[item] (self.freq_items[item], [node])这段代码里_insert沿已有路径累加计数遇到路径不存在才创建新节点同时把新节点挂到 head 指针表的节点链上。head 指针表的作用是后续挖掘时快速定位所有包含某个项的单条路径。这里有两个设计决策会影响性能第一事务中重复项用set去重后再统计因为关联规则里的项集是集合语义第二排序键用了(-count, item)支持度相同的项按字典序固定顺序避免建树结果受原始事务顺序影响也让同一份数据在多次运行时得到完全一致的树这在对比实验里很重要。2.3 递归挖掘的核心条件模式基从 FP 树里挖频繁项集靠的是条件模式基conditional pattern baseCPB。对一项频繁项a找出a在树上的所有出现位置往根节点回溯得到前缀路径路径上各节点计数取该路径上a的计数合并去重后即以a为后缀的条件模式基。def mine_rec(tree, suffix, result): for item in sorted(tree.header, keylambda x: tree.rank[x], reverseTrue): new_suffix [item] suffix result.append(new_suffix) # 构造条件模式基 cond_pats [] for node in tree.header[item][1]: path [] parent node.parent while parent and parent.item is not None: path.append(parent.item) parent parent.parent if path: cond_pats.append((path, node.count)) if not cond_pats: continue # 用条件模式基建一棵条件 FP 树然后再递归 cond_trans [] for path, count in cond_pats: cond_trans.extend([path] * count) cond_tree FpTree(cond_trans, tree.min_sup) if cond_tree.header: mine_rec(cond_tree, new_suffix, result)mine_rec是从支持度最低的项开始向上挖的。低支持度项对应的路径短、记录少条件 FP 树更小递归深度自然可控。cond_trans.extend([path] * count)这一步用重复列表方式展开条件事务代码直观但吃内存生产环境应换成带权重的条件树构建不然大数据集上会在挖到长项集时内存告急。支持度阈值从root树沿用下来保证条件树里的频繁项定义与原始一致。这段递归的本质是分治每抽取一个模式后缀问题规模缩小一步最后所有频繁项集被完整枚举不需要回原始数据库验证。这也是 FP-growth 名字里 growth 的含义——频繁模式从短到长逐级生长出来。3. 基于 Python 从零写工具的完整路径3.1 生产环境下直接调 pyfpgrowth而不是自己造轮子上一章的两段代码是为了理解原理。实际做项目时建议直接装第三方库它们经过了大量真实数据验证边界情况处理得更稳。最常用的是 pyfpgrowth 和 mlxtend前者专注频繁项集挖掘后者擅长关联规则指标计算。pip install pyfpgrowth mlxtendpyfpgrowth 的调用方式极简核心就两个函数import pyfpgrowth # transactions 为 list of list每个内层 list 是一条订单的商品集合 transactions [ [牛奶, 面包, 鸡蛋], [牛奶, 尿布, 啤酒, 鸡蛋], [面包, 黄油], [牛奶, 尿布, 啤酒, 可乐], [面包, 牛奶, 尿布, 啤酒], ] # 支持度阈值设为 3即至少出现在 3 笔订单里 patterns pyfpgrowth.find_frequent_patterns(transactions, 3) print(patterns)find_frequent_patterns返回的字典以冻结的项集元组为键、支持度为值形如{(牛奶, 尿布, 啤酒): 3}。支持度阈值传的是绝对支持度出现次数不是比例。订单总量多时需要自己做一次换算比如 10 万笔订单要留 1% 比率就传100000 * 0.01 1000。这种设计容易踩坑很多人误传 0.05 导致结果为空日志里又没报错排查起来特别费时间。pyfpgrowth 的实现遵循标准的 FP-growth 算法但它的代码注释少向量的编码也没做优化速度在百万级事务上够用。在千万级或字段特别宽的库上建议用 Spark 的 FPGrowth 算子或 C 扩展库后面第 5 章会展开。3.2 真实购物篮数据的一个完整可跑案例用一个模拟的电子商务订单数据演示全流程包含数据清洗、格式转换、挖掘和结果落盘。import pandas as pd from collections import Counter # 模拟一份订单明细表 df pd.DataFrame({ order_id: [A001, A001, A002, A002, A002, A003], product: [牛奶, 面包, 尿布, 啤酒, 可乐, 牛奶], }) # 按订单聚合同一个订单的商品去重 baskets df.groupby(order_id)[product].apply(list).tolist() baskets [list(set(b)) for b in baskets] # 统计项频次辅助确定支持度阈值 item_counts Counter(item for b in baskets for item in b) # 去除订单数少于 100 的冷门商品避免大量 1-项集干扰分析 min_item_sup 100 baskets [ [item for item in b if item_counts[item] min_item_sup] for b in baskets ] baskets [b for b in baskets if len(b) 2] # 挖掘频繁项集 min_support_abs 500 patterns pyfpgrowth.find_frequent_patterns(baskets, min_support_abs) # 转成 DataFrame 便于后续筛选与导出 result pd.DataFrame([ {itemset: k, support: v} for k, v in patterns.items() ]).sort_values(support, ascendingFalse) result.to_csv(frequent_itemsets.csv, indexFalse)这段代码做了三个关键预处理步骤。一是聚合时去重同一用户一次下单买两瓶牛奶在频繁项集里只算一次这是关联分析的常规语义若想考虑数量要么引入加权支持度要么改用序列模式挖掘。二是按商品出现次数过滤把尾部长尾商品先剪掉否则find_frequent_patterns会在这些只出现一两次的项上白耗时间。三是删除包含少于 2 个商品的事务因为单商品订单对二元规则没贡献留着只会增加建树开销。支持度阈值怎么定没有统一标准。常见做法是先画支持度分布曲线看项集数量的拐点。比如想筛出出现在 1% 以上订单里的规则订单总数 5 万阈值就设 500。频繁项集数量随支持度阈值变化极陡从 300 调到 250 可能让结果数量翻倍调参时建议用二分法别一格一格试。3.3 FP 树可视化用 graphviz 把树画出来排错和汇报都好用FP 树是递归结构看代码不如看图。graphviz 能直接把树渲染成图片节点上标注项名和计数边代表前驱后继关系。from graphviz import Digraph def draw_fp_tree(tree, filenamefp_tree): dot Digraph(commentFP Tree) dot.attr(node, shapebox, fontnameMicrosoft YaHei) # 先递归收集节点和边 def visit(node, node_id): label f{node.item}\n{node.count} if node.item else Root dot.node(node_id, label) for child_item, child_node in node.children.items(): child_id f{node_id}_{child_item} dot.edge(node_id, child_id) visit(child_node, child_id) visit(tree.root, root) dot.render(filename, viewTrue, formatpng)draw_fp_tree用递归遍历把树完整写入 dot 图。viewTrue会在本地自动打开图片跑在服务器上时改成viewFalse再拿生成出的 PNG 文件去看。生成结果里能看到 2.2 节的树结构长什么样根节点下面是按支持度降序排列的多条路径共同前缀共享节点每个节点的计数是路径重合次数。画图在超大树上会密密麻麻连成一片一般只拿一个支持度阈值适中的子集来画比如随机抽 1000 条事务建树再可视化足以验证建树逻辑是否正确。4. 从频繁项集到关联规则置信度、提升度和三个必调参数4.1 先解决一个常见误用频繁项集不是关联规则频繁项集只回答哪些商品经常一起出现关联规则还要回答从 A 推出 B 有多可信。两者之间有本质差别{牛奶, 面包}频繁出现不代表买了牛奶就会买面包有可能是两者都属于大众基础商品各自支持度都很高。所以生成规则前先想清楚业务问题想要的是互补品推荐如啤酒和尿布还是替代品推荐两种同类商品间推荐反而奇怪。从频繁项集生成规则的通用做法是把每个项集拆分成 A - B 的两种组合然后按置信度过滤。手工写这个拆分逻辑很烦mlxtend 的association_rules把这一步做成了一行调用。from mlxtend.frequent_patterns import fpgrowth, association_rules # mlxtend 的数据格式是 one-hot 编码的 DataFrame不是 list of list # 转换示例把 baskets 转成矩阵 from mlxtend.preprocessing import TransactionEncoder te TransactionEncoder() te_ary te.fit(baskets).transform(baskets) df_encoded pd.DataFrame(te_ary, columnste.columns_) # mlxtend 的 fpgrowth 支持度要传比例 freq_items fpgrowth(df_encoded, min_support0.01, use_colnamesTrue) rules association_rules( freq_items, metricconfidence, min_threshold0.5, num_itemsetsNone)association_rules里最常用的三个参数是metric、min_threshold和num_itemsets。metric支持support、confidence、lift、leverage、conviction五种选哪个取决于业务目标。做推荐位排序时一般按提升度做风控规则提取时按确信度。num_itemsets如果不填函数会只基于freq_items里的支持度算指标如果频繁项集是从其他工具挖出来的想复用这套指标计算就得自己构造support列对应上。4.2 指标表每个参数在什么场景下用下表把五个评价指标、计算公式、取值含义和推荐场景汇总在一起方便直接对照选参数。指标计算公式取值含义推荐场景支持度P(A∪B)A 和 B 同时出现的概率过滤低频组合作为基础门槛置信度P(BA)P(A∪B)/P(A)买 A 的人里有 % 买 B提升度P(A∪B)/(P(A)P(B))1 正相关1 独立1 负相关推荐系统排序避免推荐替代品杠杆率P(A∪B)-P(A)P(B)支持度与独立假设的绝对差看业务增量价值不受置信度分母影响确信度(1-P(B))/(1-P(BA))规则被否定时的可靠性实际项目里最常见的问题是置信度虚高。比如牛奶支持度 60%那任何含牛奶的规则置信度都容易接近 60%但提升度只有 1.2说明商品只是热门并没有强关联。调参顺序建议先lift 1.5再看confidence 0.4顺序反了会筛掉真正有增量价值的冷门搭配。反过来lift很高但支持度极低比如只出现在 3 笔订单里的奇异组合统计上没有意义一定先设最小支持度垫底。另一个容易忽视的是后件长度。association_rules默认生成的规则都是单后件即 A - B 中 B 只有一个项。想挖多后件规则需要自己组合antecedents和consequents或者把freq_items里 3 项集展开成 1 对 2 的规则。线上系统侧更愿意接受单后件规则因为推荐位一次只推一个商品多后件规则只能放在报告里看模式。4.3 实战在一次调用里完成规则生成与排序输出rules association_rules( freq_items, metriclift, min_threshold1.5, num_itemsetsNone) rules rules[(rules[confidence] 0.4) (rules[support] 0.005)] # 按提升度降序输出前 20 条高潜规则 top_rules rules.sort_values(lift, ascendingFalse).head(20) result top_rules[[ antecedents, consequents, support, confidence, lift, leverage]].copy() result.to_csv(top_association_rules.csv, indexFalse)这里的筛选顺序有讲究。先用lift过滤成套筛一次再叠加confidence和support下限是因为 mlxtend 的association_rules只会保留满足min_threshold的行后续二次筛选越收越窄。输出leverage列能在后续给运营展示时讲清这条规则比随机推荐多带来多少成交概率这是业务方最容易理解的口径。处理完规则后建议做一个反向验证抽几条高置信度规则回到原始订单集合里人工核对。做法是过滤出同时包含前件商品和后件商品的订单看实际占比是否与置信度计算一致防止 one-hot 编码阶段因为空值或数据变形导致统计失真。5. 大数据库分析与冷门参数的进阶调法5.1 数据规模上来后事务编码方式比算法库更影响上限FP-growth 号称适合大型数据库分析这个大是有前提的。当单台机器的内存装不下完整 FP 树时算法再好也白搭。常见做法是把原始订单表做一次矢量化编码后再喂给挖掘器把字符串项映射成整数 ID用 int 替代 string 做节点比较和哈希能节省一半以上内存。事务事务本身也可以用位图bitmap表达每个 item 用一位表示是否出现这样不仅节省内存条件模式基的构造也能用位运算加速。# 将商品名映射为整数 ID树节点里存 int 而不是 str item_to_id {item: idx for idx, item in enumerate(item_counts.keys())} id_to_item {idx: item for item, idx in item_to_id.items()} transactions_int [[item_to_id[i] for i in b] for b in baskets] # pyfpgrowth 可以直接喂 int 列表结果里的项集是整数元组 patterns_int pyfpgrowth.find_frequent_patterns(transactions_int, min_support_abs) patterns { tuple(id_to_item[i] for i in k): v for k, v in patterns_int.items() }这段整数映射的思路很简单但收益极其明显。字符串的哈希运算和内存占用是整数的好几倍尤其当商品名是很长的中文或 URL 时先映射再挖能显著降低 GC 压力。映射时机也有讲究应该在清洗去重、过滤低频之后再映射否则清理阶段还要回查字典。映射后的结果集再做一次回映射输出给人看的规则这套流程与 ML 里的 label encoding 完全同构做过特征工程的同事一看就明白。5.2 超大数据集的分治思路条件库分区与多轮挖掘事务规模到达千万级以上时单机 FP-growth 会开始吃紧方向有两个一是按时间窗口或者商品类目切分数据分块挖掘后结果合并二是直接上 Spark 的 FPGrowth。这里说分块合并的逻辑因为它在单机资源有限时最简单可靠。分块挖掘的要点是支持度阈值的重算。假设全量订单 8 千万条要挖 support 0.01%即 8000 次出现。按月份切成 12 块每块约 667 万条如果每块里仍用 800 的这个绝对阈值会漏掉那些分散在多个块里、每块都不足 800 次但全量总次数超过 8000 的项集。正确做法是支持度阈值按块大小比例缩放每块用 8000 / 12 ≈ 667 作为阈值挖完所有块后把结果按项集合并计数再做一次全局阈值过滤。这个方案只有在需要精确计数时才这么调业务上能容忍近似结果的场景可以直接调低每块阈值多保留一些候选再合并。另一种是事务较多但项数不太多的场景可以垂向拆分事务把商品按高频/低频分组高频组先挖低频组补挖差集。实践中这种做法的坑在于 高频与低频之间的连接频繁项集容易漏合并时还得回表验证代码越写越复杂不如花钱堆机器上 Spark 的FPGrowth算子后者自带分区挖掘和计数合并平衡了算法复杂度和工程成本。5.3 调参经验三个最容易忽视的坑第一个坑是支持度阈值与最小项集长度的联动。很多人只调了min_support忘记过滤 1-项集结果。频繁项集里 90% 都是单个商品2 项集和 3 项集才是规则的原材料输出结果前务必要过滤len(itemset) 2。第二个坑是置信度阈值设得过高。0.7 以上的置信度往往只挖出那些包含大众热销品的规则长期看对推荐系统的点击率提升反而有限。置信度 0.7 意味着前件出现时后件 70% 概率出现这类规则对用户来说太显然增量价值低。建议在 0.3-0.5 之间先跑一轮看提升度的分布再决定要不要收紧。第三个坑是频繁项集挖掘结果里隐含的时间窗口效应。订单数据是某个时间段的快照季节性商品如冰淇淋、暖宝宝会在特定月份形成虚高关联跨季验证时会失效。进阶级做法是引入时间衰减权重近期订单的支持度计数乘以稍大的权重系数或者干脆按周切片分别挖规则对比稳定性后再决定往线上推。FP-growth 的树结构天然支持这种切片对比因为每次建树是独立的不会受历史树影响。本文还有配套的精品资源点击获取