Apriori与FP-growth实战:跑通关联规则挖掘与购物篮分析

发布时间:2026/10/10 9:40:01
Apriori与FP-growth实战:跑通关联规则挖掘与购物篮分析 简介关联规则挖掘经典算法实现资源包面向数据挖掘初学者、算法研究者及需要做购物篮分析的数据分析人员。资源聚焦 Apriori 与 FP-growth 两种经典频繁项集挖掘算法包含完整的 Python 实现与配套测试数据可帮助读者理解支持度、置信度等核心指标掌握从原始事务数据到频繁项集、再到关联规则的完整流程。压缩包内含3个文件分别为两个Python源码文件和一个文本数据文件总大小仅80KB轻量易用便于快速运行与对比实验。其中一个 Python 脚本通过候选集逐层扫描实现频繁项集发现另一个借助 FP 树与条件模式基避免反复全库扫描二者形成明显效率对照。读者可自行调整支持度阈值统计运行时间与内存占用也可结合并行化或分布式思想进一步优化。已有 824 人学习这份资源适合作课程作业、毕业设计或算法实战的参考备查。1. 关联规则挖掘的快速起点这份 Apriori FP-growth 资源包里有什么能直接跑关联规则挖掘最经典的入口就是购物篮分析顾客买了面包之后有多大比例还会顺手拿一罐黄油。这份资源包里的apriori.py和fpgrowth.py是数据挖掘领域两种最主流的频繁项集算法实现配上一份data.txt测试数据解压后可以直接在命令行跑出频繁项集和关联规则。如果你是刚接触数据挖掘、想用代码把一个算法完整跑通的学生或者工作中需要做商品捆绑、行为关联分析但不想从零造轮子的一线工程师这套代码比教材里的伪代码具体得多。它把支持度、置信度、频繁项集、规则生成这些概念落到真实可执行的 Python 脚本里不多不少刚好够你在一小时内完成第一次完整实验。2. 理解支持度与置信度两个公式决定 Apriori 能不能跑出有效规则2.1 支持度和置信度两个公式提前决定规则有没有业务价值关联规则挖掘的输出是一堆形如A → B的规则但并不是所有「同时出现」都值得关注。支持度Support回答的是「A 和 B 一起出现的概率有多大」置信度Confidence回答的是「买了 A 的人里有多少比例也买了 B」。前者衡量覆盖面后者衡量强关联程度。支持度的计算公式是支持度(A→B) count(A∪B) / N其中N是总交易数。置信度的计算公式是置信度(A→B) count(A∪B) / count(A)。举个例子如果 1000 笔订单里面包和黄油同时出现在 100 笔中同时面包单独出现在 400 笔中那么规则面包 → 黄油的支持度是 0.1置信度是 0.25。这意味着每 10 笔订单里只有 1 笔同时包含这两样而且买面包的人里只有四分之一买了黄油这条规则的业务价值就很有限。在apriori.py里这两个公式直接对应两个命令行参数最小支持度阈值和最小置信度阈值。阈值通常由业务方给定但实际调参时你会发现一个规律支持度阈值设得越低频繁项集越多规则数量呈指数级膨胀阈值设得越高留下的规则越少但越「显然」。这个度怎么拿捏我在第 5 章会专门讲。2.2 Apriori 的逐层搜索与剪枝为什么「子集频繁」能砍掉大量候选Apriori 算法最核心的思想是一条先验性质Apriori property如果一个项集是频繁的那么它的所有子集也一定是频繁的。反过来用就是剪枝逻辑如果一个项集的某个子集不频繁那这个项集本身也不可能频繁直接丢弃不用再去数据库里数一遍支持度。算法的迭代过程分四步走。第一步扫描整个数据集统计每个单品的出现次数筛掉低于最小支持度的项得到频繁 1-项集L1。第二步由L1两两组合生成候选 2-项集C2再次扫描数据集计数筛掉不达标的得到L2。第三步由L2连接生成候选 3-项集C3但在计数之前先做一次剪枝检查每个候选的所有子集是否都在L2里不在就删掉。第四步重复「生成候选 → 剪枝 → 扫描计数」的循环直到无法生成新的频繁项集为止。这个剪枝步骤是 Apriori 性能的关键。没有它候选集数量会按照组合数爆炸式增长有了它大量明显不频繁的组合在计数之前就被淘汰了。比如有 1000 个单品候选 2-项集理论上接近 50 万个但如果单品的频繁项只有 50 个候选 2-项集就只剩下 1225 个扫描量直接降了两个数量级。2.3 FP-growth 的针对性优化扫描次数从 k 次压到 2 次Apriori 的瓶颈在于每次生成新候选集都要重新扫描全量数据。如果最长频繁项集的长度为k就要完整扫描k次数据库。数据量在百万级交易时这个开销非常可观。FP-growth 是 2000 年提出的改进方案核心思路是把数据压缩进一棵前缀树FP 树将「多次全量扫描」降为「只扫描两次」。第一次扫描统计每个项的支持度并排序第二次扫描构建 FP 树。之后挖掘频繁项集不再依赖原始数据集而是在 FP 树上递归地找条件模式基、构建条件 FP 树直到树为空。Apriori 和 FP-growth 的关系可以理解为一个是反复翻原书找答案一个是先做好索引再查。对比维度AprioriFP-growth扫描次数每层迭代扫一次共 k 次只扫两次原始数据候选集需要生成候选并剪枝不生成候选直接模式增长数据结构无特殊结构FP 树 头表适合场景数据量小、阈值高数据量大、阈值低实现难度简单易理解递归逻辑略复杂2.4 这份资源里两个脚本的分工与输入输出约定apriori.py和fpgrowth.py面对的是同一份data.txt但角色定位不同。apriori.py完整实现了「频繁项集 关联规则生成」适合学习原理和验证结果fpgrowth.py偏性能向通常只输出频繁项集规则生成策略依赖具体实现是否补写了置信度计算。如果你打开两个脚本对比会看到apriori.py里大概率有generate_rules之类的函数而fpgrowth.py的挖掘函数主要集中在mine_fp_tree这种递归结构里。两个脚本共享同一种数据格式约定data.txt里每一行是一笔完整的事务行内的项之间用特定分隔符切开。常见做法是用逗号也有用空格或制表符的具体看脚本头部读取数据的代码。我在下一章会带你一步步确认这个格式并跑通第一个完整实验。3. 跑通 apriori.pydata.txt 格式、阈值参数与输出结果对照3.1 先看 data.txt 的格式事务数据最常见的组织方式拿到资源包第一件事别急着跑代码先打开data.txt看一眼。这个文件就是你的测试数据也是最容易踩坑的地方。事务数据常见的组织方式有三种每行一笔事务、每行一笔事务但项之间用逗号分隔、每行是一个 JSON 数组。这份资源里的data.txt走的是最经典的第一种变形——每行一笔事务项与项之间用逗号分隔。举个例子文件内容可能是这样的milk,bread,butter bread,eggs milk,bread,eggs,butter bread,beer,diapers每一行代表一个顾客的一次购买记录行内的每个词代表一件商品。注意同一个项在一行内不应该重复出现如果出现了重复比如milk,milk,bread支持度计数就会虚高我在第 5 章会展开讲这个问题。确认完格式之后用wc -l data.txt看一眼总行数这个数就是支持度公式里的N后面核对输出结果时用得上。3.2 运行脚本参数怎么传、代码内部做了什么确认数据格式没问题后直接在终端执行。常见做法是脚本接受三个位置参数数据文件路径、最小支持度、最小置信度。命令大概是python apriori.py data.txt 0.2 0.6其中0.2是最小支持度阈值0.6是最小置信度阈值。执行后脚本会扫描data.txt找出所有支持度不低于 0.2 的频繁项集再从中生成置信度不低于 0.6 的关联规则。如果你少传一个参数脚本通常会报IndexError因为参数是按顺序读取的。打开apriori.py典型的脚本骨架长这样import sys from itertools import combinations def load_data(filename, delimiter,): transactions [] with open(filename, r, encodingutf-8) as f: for line in f: line line.strip() if line: items line.split(delimiter) transactions.append(set(items)) # 用集合去重防止重复项干扰计数 return transactions def calculate_support(transactions, itemset): count 0 for trans in transactions: if itemset.issubset(trans): count 1 return count / len(transactions) def apriori(transactions, min_sup): support_data {} all_items set() for trans in transactions: all_items | trans current [frozenset([item]) for item in sorted(all_items)] while current: next_candidates [] for itemset in current: sup calculate_support(transactions, itemset) support_data[itemset] sup if sup min_sup: next_candidates.append(itemset) current generate_next_candidates(next_candidates) return support_dataload_data函数负责把文本行切分成集合set(items)这一步很关键它天然去掉了行内重复项避免支持度被重复计数推高。calculate_support用issubset判断某个项集是否包含在事务中复杂度是事务数乘以项集大小。apriori主循环从单一项开始逐层扩张把不满足阈值要求的项集直接丢进support_data记录但不再参与下一轮候选生成。值得一提的细节是frozenset的用法。Python 里普通set不能作为字典的键因为它是可变类型frozenset不可变所以可以用作support_data的键来保存支持度。如果你自己改写脚本时用了set做键运行到一半会直接抛TypeError: unhashable type: set。3.3 读懂输出支持度、置信度与规则的对应关系跑完apriori.py后输出一般分两部分频繁项集列表和规则列表。频繁项集部分长这样频繁项集: (milk, bread) - support: 0.400 (milk, butter) - support: 0.300每行显示一个频繁项集和它对应的支持度。你可以用第一节里提到的count(A∪B) / N手动验证一条比如milk和bread同时出现的行数除以总行数应该和输出一致。这个手动验证步骤我每次都做花两分钟能确认代码没有改错阈值逻辑。规则部分长这样规则: milk - bread, conf: 0.667, sup: 0.400 bread - milk, conf: 0.667, sup: 0.400注意milk → bread和bread → milk是两条不同的规则因为置信度的分母不同。前者看的是买牛奶的人里多少比例买了面包后者看的是买面包的人里多少比例买了牛奶。实际业务中这条方向差异决定了促销策略完全不同——把面包放在牛奶旁边和把牛奶放在面包旁边带来的增量交易结构不一样。3.4 和《数据挖掘导论》课后题对照教材答案用这份代码验证很多读者拿这套资源是去验证《数据挖掘导论》等教材的课后题答案。教材会给一个极小的数据集让你手算频繁项集或者某条规则的支持度、置信度。我建议的方法是把教材上的数据集手工整理成data.txt的格式然后调低最小支持度到 0.01调低最小置信度到 0.01跑一遍输出与自己的手算结果对照。这样做的价值在于手算容易漏掉某条规则的方向或者把支持度计数数错。代码输出虽然不会告诉你「哪一步错了」但它给了你一个确定的基准答案。如果手算和代码不一致问题几乎一定出在「包含关系」的判断上——比如项集(milk, bread)是否算milk出现标准的issubset语义是必须全部包含和教材定义完全一致。4. FP-growth 的实现细节FP 树构建、条件模式基与递归挖掘4.1 构建 FP 树之前为什么先按支持度排序FP-growth 和 Apriori 的第一个分岔点出现在第二次扫描数据集的时候。Apriori 直接开始候选计数而 FP-growth 在构建 FP 树之前会先把每个事务里的项按全局支持度从高到低排序。这个排序不是可选项它直接决定 FP 树的压缩率。原理很简单FP 树的前缀路径共享机制只有让出现频率高的项尽量排在前面不同事务的长公共前缀才能被合并到同一条树路径上。如果顺序是乱的比如高频的milk排在事务末尾那么两份原本高度重叠的事务会因为没有共享前缀而各自成链树的高度和节点数都会膨胀频繁项集的挖掘效率随之下降。常见做法是在第一次扫描时用Counter统计所有项的出现次数过滤掉低于最小支持度的项然后按支持度降序得到一个全局排序表。第二次扫描构建树时每个事务里的项先按这个排序表重组再插入 FP 树。from collections import Counter, defaultdict def build_fp_tree(transactions, min_sup, order): fp_tree {} # 节点结构: (item, count, children) header {} # 头表: item - (count, first_node) fp_tree[None] [None, 0, {}] for trans in transactions: sorted_items [item for item in sorted(trans, keylambda x: order[x])] cur fp_tree[None] for item in sorted_items: if item not in cur[2]: cur[2][item] [item, 0, {}] cur cur[2][item] cur[1] 1 header[item] header.get(item, [0, None]) header[item][0] 1 return fp_tree, headerorder是全局支持度降序表sorted(trans, keylambda x: order[x])把每笔事务按这个顺序重组。FP 树的根节点不存储项信息None只是占位每个子节点保存三项项名、计数、子节点字典。header头表是后续挖掘频繁项集的入口它同时记录了每个项在树中的总计数和第一个出现位置实际工程里还会维护一条节点链把相同项的节点串起来方便挖掘时快速回溯。这里有一个容易混淆的点节点计数和头表计数不一样。节点计数cur[1] 1表示经过该节点的路径数即该前缀出现次数头表计数header[item][0] 1表示该项在所有事务中的总出现次数。两者在大多数情况下数值相同但在 FP 树中存在多条路径时头表计数是所有同项节点的计数之和。4.2 条件模式基到条件 FP 树递归挖掘的终止边界FP 树构建完成之后挖掘过程本质上是一个递归。从支持度最低的频繁项开始找出所有包含该项的前缀路径这些前缀路径的集合就是条件模式基把条件模式基当作一个小型事务集再构建一棵条件 FP 树然后递归挖掘这棵条件树。递归的终止条件是条件 FP 树为空或者树中只有一个分支。当只剩下一条路径时这条路径上所有项的组合都是频繁项集直接枚举组合即可不需要再往下递归。这个剪枝策略是 FP-growth 效率高的另一个重要来源。def mine_fp_tree(header, min_sup, prefix, freq_items): items sorted(header.keys(), keylambda k: header[k][0]) for item in items: new_prefix prefix [item] freq_items.append((new_prefix, header[item][0])) patterns find_prefix_paths(item, header) cond_transactions [] for pattern, count in patterns: cond_transactions.extend(pattern * count) cond_fp_tree, cond_header build_fp_tree(cond_transactions, min_sup, order_for_cond(pattern)) if cond_header: mine_fp_tree(cond_header, min_sup, new_prefix, freq_items)find_prefix_paths沿着头表的节点链向上回溯收集从根到该节点的路径路径上除当前项以外的部分就是条件模式基。cond_transactions.extend(pattern * count)做的是按支持度计数重放事务这样条件 FP 树就能复用build_fp_tree的计数逻辑。order_for_cond根据条件模式基里各项的出现频率重新排序注意这个排序是针对条件库的局部排序不是全局排序。这段递归代码跑起来有两个观察点。第一freq_items里每一项都带着支持度计数这个计数是在递归过程中累加的不是最后重新统计的第二递归深度和频繁项集的最大长度相当如果数据里有一条长度为 20 的频繁路径递归会深入 20 层。Python 默认递归深度限制是 1000大多数场景够用但如果你的数据是超长序列型事务需要手动调高sys.setrecursionlimit否则会抛RecursionError。4.3 用 fpgrowth.py 跑同一份数据输出如何与 Apriori 对齐验证 FP-growth 实现正确性最直接的办法是让fpgrowth.py和apriori.py跑同一份data.txt使用相同的最小支持度阈值然后对比频繁项集是否完全一致。FP-growth 不生成候选集所以它输出的频繁项集顺序可能和 Apriori 不同但项集的集合以及对应的支持度数值必须完全一致。python apriori.py data.txt 0.2 0.6 python fpgrowth.py data.txt 0.2 0.6如果两边结果不一致先排查数据预处理差异。Apriori 脚本里我建议用set(items)去重FP-growth 脚本里也应该做同样的去重处理如果一边去重一边没去重同一笔事务里出现重复项时支持度就会出现偏差。再排查最小支持度的语义有的实现把最小支持度当作计数而不是比例比如传0.2代表至少 0.2 条事务这会导致阈值相差N倍输出完全对不上。4.4 两个脚本的差异频繁项集相同但规则生成策略不同两套算法挖掘出的频繁项集是一致的因为频繁项集的定义只依赖于支持度计数与算法无关。真正的实现差异在从频繁项集到关联规则这一步。apriori.py会遍历每个频繁项集枚举其所有真子集作为规则前件计算置信度并过滤掉低于阈值的规则fpgrowth.py如果只实现了频繁模式挖掘部分输出里就只有频繁项集没有规则。这也是为什么我建议把规则生成单独写成一个公共函数。无论你用的是 Apriori 还是 FP-growth 提供的频繁项集规则生成的逻辑都可以复用def generate_rules(freq_item, support_data, min_conf): rules [] item_list list(freq_item) for r in range(1, len(item_list)): for antecedent in combinations(item_list, r): antecedent frozenset(antecedent) consequent freq_item - antecedent conf support_data[freq_item] / support_data[antecedent] if conf min_conf: rules.append((antecedent, consequent, conf)) return rules这个函数枚举某个频繁项集的所有真子集作为前件用支持度(全集) / 支持度(前件)算置信度。注意r的取值范围是1到len(item_list) - 1不能把全集本身作为前件否则置信度恒为 1没有意义。support_data里保存的是calculate_support算出来的支持度所以在调用前要确保频繁项集和它的所有子集都已经被记录过。5. 避坑与排查跑这两个脚本最常见的五个坑5.1 现象读取 data.txt 时抛出 UnicodeDecodeError运行脚本第一行代码就报错提示utf-8 codec cant decode byte终端里出现乱码字符。原因在于data.txt可能是 GBK 或 GB2312 编码保存的尤其在 Windows 环境下用记事本默认保存时经常会这样。解决方法是先确认文件编码再在load_data的open函数里显式指定编码。我一般会用编辑器把文件另存为 UTF-8或者在图省事时把读取代码改成encodinggbk。更稳的做法是用chardet库自动检测编码但这是后话。如果你想同时兼容两种编码可以这样写try: with open(filename, r, encodingutf-8) as f: lines f.readlines() except UnicodeDecodeError: with open(filename, r, encodinggbk) as f: lines f.readlines()5.2 现象最小支持度设太低程序卡死或内存暴涨把min_sup设为 0.001 之后频繁项集数量从几十个暴增到几十万个输出刷屏运行时间从秒级变成分钟级甚至直接内存溢出。支持度阈值的选取不是纯技术问题它和业务目标是绑定的。最小支持度阈值是关联规则挖掘里最接近「玄学」的参数没有标准答案。解决方法是先把阈值从 0.5、0.4、0.3 这样从高到低逐步往下试每跑一次就观察频繁项集数量。一旦发现数量接近几千条就不要再往下压了。另外可以直接给脚本加固在apriori主循环里加一个最大频繁项集长度限制比如max_itemset_size 5超过长度就停止扩展。这样至少不会因为一条超长路径把递归和内存拖垮。5.3 现象同一笔事务里有重复项支持度统计虚高数据文件里某一行长这样milk,milk,bread,butter同一件商品在一笔订单里出现了多次。如果不做去重calculate_support用issubset判断时虽然不会重复计数但如果你改用itemset.issubset(trans)的方式而trans是list而不是set判断时每个项都可能在列表里被匹配到导致同一笔事务被算了多次。解决方法是统一在load_data阶段转换成set。这个坑的特点是输出结果不报错、不卡顿、支持度偏高但不离谱如果不用手动验证几乎发现不了。我在 3.2 节里强调的set(items)就是为了从源头堵住这个隐患。5.4 现象FP-growth 和 Apriori 跑同一份数据结果不一样两个脚本的频繁项集集合能对上但支持度数值对不上或者项集数量差一截。前面的编码、重复项问题都排除了之后还有可能是负项处理方式的差异。有的 Apriori 实现在生成候选项集时会保留单元素自身和空集的比较而 FP-growth 的条件模式基挖掘天然不生成空集项两边对比时就会差出只有单元素构成的「平凡频繁项集」。解决方法是明确两边都只对长度大于等于 2 的项集做对比。单项目集的支持度本身不构成有趣的关联规则比对它有业务意义。统一这个口径后两个算法跑同一份数据应该给出完全相同的集合。5.5 现象长路径数据触发 RecursionError 或者递归栈溢出FP-growth 的递归深度和频繁项集的最长长度直接相关。如果data.txt里有一个事务包含了 1500 个不同项并且它们都是频繁的条件 FP 树的递归挖掘就会突破 Python 默认的 1000 层递归限制。解决方法是两条路选一条。一是在脚本开头加sys.setrecursionlimit(5000)但要注意递归深度过高会挤压 C 栈导致进程直接崩溃这不是 Python 层面能兜住的。二是给数据加预处理移除掉低频长尾项。关联规则挖掘的目标是发现共性而不是为每个极端事务找规则把出现次数极少的项过滤掉既能避免递归过深也能减少噪声规则。6. 进阶用法把 CSV 订单流变成关联规则并验证规则质量6.1 把订单明细 CSV 转成事务格式实际业务数据很少直接是data.txt这种每行一笔事务的格式更常见的是订单明细表每一行是一个订单里的一个商品同一个订单号对应多行。写个小脚本把明细表聚合成事务文件是让这份资源从玩具数据走向真实数据的第一步。import csv from collections import defaultdict transactions defaultdict(list) with open(orders.csv, encodingutf-8) as f: for row in csv.DictReader(f): transactions[row[order_id]].append(row[product_id]) with open(transactions.txt, w, encodingutf-8) as out: for order_id, items in transactions.items(): if len(items) 2: # 单商品订单无法形成有效关联规则 out.write(,.join(dict.fromkeys(items)) \n)defaultdict(list)按订单号聚合商品dict.fromkeys在保留顺序的同时去掉了重复商品。过滤掉单商品订单是因为它们对频繁项集没有任何贡献还会拖慢扫描速度。聚合完之后最好再跑一个统计脚本看看单笔订单的平均商品数如果均值只有 1.5说明你的业务场景本身关联性不强跑出来的规则容易全是高置信度低支持度的偶然组合。6.2 用执行时间与规则质量做选型数据量上来之后Apriori 和 FP-growth 的性能差距会变得非常明显。我通常在十万级事务、平均订单 5 项左右的数据上做压测Apriori 每多一层迭代就要全量扫描一次而 FP-growth 两次扫描后就在内存里完成全部挖掘。选型建议是千万级以下的事务量且最小支持度高于 0.05 时Apriori 够用代码更直观好维护事务量大或者支持度阈值低到 0.01 以下时换 FP-growth否则 Apriori 的重复扫描会拖到不可接受。除了执行时间规则质量也要看。只盯着支持度和置信度容易被高置信度低支持度的规则误导比如「买了某冷门配件的人里 90% 都买了主机」但配件总共只卖出过 20 单。这时要看提升度Lift提升度(A→B) 置信度(A→B) / 支持度(B)。提升度大于 1 说明 A 和 B 正相关等于 1 说明相互独立小于 1 说明负相关。置信度高不代表有关系这个念头是我当年第一次跑出垃圾规则时留下的。过滤掉提升度小于 1.2 的规则你会发现「很多看似有价值的规则其实是数据噪声」——从那以后我每次跑完关联规则都强制走一遍多组阈值对比加提升度过滤的流程没有这套验证我根本不敢把规则直接交给业务方。希望帮到你。本文还有配套的精品资源点击获取

关于本文作者

来自尧图内容编辑团队

尧图内容编辑团队 内容团队

尧图内容编辑团队

本文由尧图网络内容编辑团队执笔。团队由资深项目经理、前端工程师与设计师组成,所有内容均来自亲手交付的真实项目,先讲清问题、再给出可落地的解法。尧图深耕北京网站建设十年,服务过京华建材集团、智造科技等各行业客户,把一线经验沉淀为可复用的行业观察。

  • 十年建站经验,覆盖建材、制造、服务、文创等
  • 项目经理把关选题与事实准确性
  • 工程师与设计师联合撰写专业细节
  • 统一编辑规范,保证文风与排版一致
  • 每月复盘转化数据,迭代选题方向

延伸阅读

相关资讯与近期热门内容

深度阅读推荐

建站决策前值得细读的三篇

网站改版的5个关键决策
2024-08-12

网站改版的5个关键决策

什么时候该改版、改到什么程度、如何避免流量掉光,京华建材集团改版复盘给出答案。

获取专属建站方案

看完文章,把您的行业与预算告诉我们,免费获取一份量身定制的官网建设方案与报价。

立即免费咨询