数据挖掘频繁项集:Apriori与FP-Growth选型及Python实战对比
简介:这份资源面向计算机、数据科学与大数据技术、人工智能等专业的在校学生及教师,提供基于Python实现的数据挖掘经典频繁项集挖掘算法对比测试项目,可用于课程设计、毕业设计、期末大作业或入门进阶练习。压缩包共3个文件,包含2个py源码文件与1个md项目说明,整体约6KB,其中py文件分别实现Apriori与FP-Growth算法,md文档则交代项目背景、运行方式与结果说明,便于快速理解代码结构。目前已有414人学习下载,说明该案例在教学中具有一定参考价值。读者可借此掌握两种算法的核心流程与差异,包括候选集生成、剪枝策略、频繁模式树构建等关键环节,并对照测试结果分析效率与适用场景,为后续数据挖掘项目或论文实验提供可复用的代码基础与排错思路。
1. 数据挖掘频繁项集:Apriori 和 FP-Growth 到底该选哪个
电商订单表里躺着几十万条交易记录,老板丢过来一句「看看哪些商品经常一起买」,这就是频繁项集挖掘最典型的落地场景。Apriori 和 FP-Growth 是解决这个问题的两把主力工具,前者靠「逐层候选 + 剪枝」的经典思路,后者用 FP-Tree 压缩数据库、递归建条件树。很多人第一次接触数据挖掘就是从这两个算法开始的,但真正上手跑数据时会发现:小数据集上两者差不多,数据量一上来差距立刻拉开。这篇笔记不讲教科书推导,而是把两个算法在 Python 里跑通、对比、调参、排错的完整路径写清楚,适合刚学完 Python 基础、想动手做数据挖掘实战的读者,也适合已经用过其中一个、想搞清楚另一个边界在哪的从业者。核心结论先放这里:Apriori 适合理解原理和小规模验证,FP-Growth 适合真实数据量下的生产使用,但选哪个不只看数据量,还要看最小支持度、内存和结果可解释性。
2. 两个算法的核心差异:从候选项集到 FP-Tree 的路线分叉
2.1 Apriori 的逐层剪枝逻辑与它的性能天花板
Apriori 的核心思想可以用一句话概括:如果一个项集是频繁的,那它的所有子集也必须是频繁的。反过来,如果一个项集不频繁,那它的所有超集都不可能是频繁的。这个「向下封闭性」让算法可以逐层筛选——先找频繁 1 项集,再由 1 项集组合出候选 2 项集,扫描数据库验证,保留频繁的,再组合出候选 3 项集,如此循环直到没有更多频繁项集。
这个思路非常直观,代码实现也不复杂,但问题出在「每一层都要重新扫描整个数据库」。假设有 10000 条交易记录,频繁 1 项集有 500 个,那候选 2 项集最多有 C(500,2) = 124750 个,每个都要回数据库数一遍出现次数。如果数据里长事务多、频繁项集层次深,扫描次数和候选数量会爆炸式增长。这就是 Apriori 的性能天花板——它把大量时间花在了「生成候选」和「重复扫描」上。
我一般会这样判断:如果数据集交易数在几千条以内、最小支持度设得不太低,Apriori 跑起来完全没问题,而且中间结果容易打印出来看,适合教学和调试。但一旦交易数上万、支持度降到 0.01 以下,Apriori 的耗时就会变得不可接受。
2.2 FP-Growth 如何用一棵树绕开候选生成
FP-Growth 换了一条路。它只扫描数据库两次:第一次统计每个项的频次,去掉不满足最小支持度的项,然后把剩下的项按频次降序排列;第二次扫描把每条交易按这个顺序插入一棵前缀树,也就是 FP-Tree。相同前缀的路径会共享节点,所以树的大小通常远小于原始数据库。
建完树之后,算法从频次最低的项开始,为每个项构建条件模式基(conditional pattern base),再递归建条件 FP-Tree,一路挖出所有频繁项集。整个过程不需要生成候选集,也不需要反复扫描原始数据库。这就是 FP-Growth 在稠密数据集上通常比 Apriori 快一个数量级以上的原因。
但 FP-Growth 不是没有代价。树结构在内存里构建,如果数据非常稀疏、项数极多,FP-Tree 的分支会很多,内存占用反而可能超过预期。另外递归建条件树的过程如果实现不当,Python 的递归深度限制也会成为问题。所以「FP-Growth 一定比 Apriori 快」这个说法有前提:数据有一定稠密度、最小支持度不是极低、内存够用。
2.3 选型判断:什么场景用哪个
把选型逻辑整理成一张表,方便对照自己的数据情况做决定:
| 判断维度 | 选 Apriori | 选 FP-Growth |
|---|---|---|
| 交易数量 | 几千条以内 | 上万条以上 |
| 最小支持度 | 较高(0.05 以上) | 较低(0.01 甚至更低) |
| 数据稠密度 | 稀疏、短事务 | 较稠密、长事务 |
| 内存限制 | 内存紧张但数据小 | 内存充足 |
| 可解释性需求 | 需要看中间候选过程 | 只看最终频繁项集 |
| 实现复杂度 | 低,容易手写 | 中,建议用现成库 |
实际项目中我的习惯是:先用 Apriori 在小样本上跑一遍,确认支持度阈值大概在什么范围合理,再换 FP-Growth 上全量数据。这样既不会一上来就被全量数据卡住,也能借助 Apriori 的中间输出验证数据质量。
3. 用 Python 跑通 Apriori:从数据加载到频繁项集输出
3.1 环境准备与依赖安装
Python 环境建议 3.8 以上,主要依赖就两个:pandas用来读数据,mlxtend提供了现成的 Apriori 和 FP-Growth 实现。如果你只是想理解算法,也可以手写,但生产环境没必要重复造轮子。
pip install pandas mlxtendmlxtend的优点是接口统一,Apriori 和 FP-Growth 的函数签名几乎一样,切换算法只需要改一个函数名,对比测试非常方便。注意mlxtend要求输入是 one-hot 编码的布尔 DataFrame,不是原始的交易列表,这一点后面会详细说。
3.2 把交易数据转成 one-hot 矩阵
假设原始数据是一张订单表,每行是一条交易,商品名用逗号分隔。第一步是把它拆成 one-hot 矩阵:
import pandas as pd from mlxtend.preprocessing import TransactionEncoder # 原始交易数据,每条是一个商品列表 transactions = [ ['牛奶', '面包', '鸡蛋'], ['面包', '鸡蛋', '可乐'], ['牛奶', '面包', '鸡蛋', '可乐'], ['牛奶', '面包', '尿布'], ['牛奶', '尿布', '啤酒'], ['面包', '鸡蛋', '啤酒'], ] # TransactionEncoder 把列表转成布尔矩阵 te = TransactionEncoder() te_array = te.fit(transactions).transform(transactions) df = pd.DataFrame(te_array, columns=te.columns_) print(df) print(f"交易数: {df.shape[0]}, 商品数: {df.shape[1]}")TransactionEncoder做的事情就是收集所有出现过的商品作为列名,然后每条交易对应一行,买过的商品标 True,没买的标 False。te.columns_保存了所有商品的列名顺序,后面关联规则输出时会用到。这一步看起来简单,但实际数据里如果有商品名前后空格、大小写不一致,会导致同一个商品被拆成两列,所以加载数据后最好先做一轮清洗。
3.3 调用 mlxtend 的 apriori 并读懂输出
有了布尔矩阵就可以直接跑 Apriori:
from mlxtend.frequent_patterns import apriori # min_support=0.5 表示项集至少在 50% 的交易中出现 frequent_itemsets = apriori(df, min_support=0.5, use_colnames=True) # 按支持度降序排列 frequent_itemsets = frequent_itemsets.sort_values( by='support', ascending=False ).reset_index(drop=True) print(frequent_itemsets)输出是一个 DataFrame,两列:support是支持度,itemsets是频繁项集(frozenset 类型)。min_support是最关键的参数,设 0.5 意味着一个项集至少要在 3 条交易里出现(总共 6 条)。这个值设得越高,结果越少但越可靠;设得越低,能挖出更多长尾组合,但计算量也越大。
use_colnames=True让输出显示商品名而不是列索引,调试时更直观。如果数据量大,建议先设一个较高的支持度跑通流程,再逐步降低看结果变化。
3.4 从频繁项集到关联规则:confidence 和 lift 怎么设
频繁项集本身只是「哪些商品经常一起出现」,要变成「买了 A 的人大概率会买 B」还需要生成关联规则:
from mlxtend.frequent_patterns import association_rules # 生成关联规则,metric 可选 confidence / lift / support rules = association_rules( frequent_itemsets, metric='confidence', min_threshold=0.7 ) # 只看关键列 rules = rules[['antecedents', 'consequents', 'support', 'confidence', 'lift']] rules = rules.sort_values(by='lift', ascending=False) print(rules)三个指标的含义要分清楚:support是规则覆盖的交易比例,confidence是买了前件的人里买后件的比例,lift是前件和后件的实际共现比独立假设下的期望共现高多少倍。lift > 1说明正相关,lift = 1说明独立,lift < 1说明负相关。
我一般会先用confidence >= 0.6筛一轮,再按lift排序看 Top 20。如果lift很高但support极低,说明这个规则虽然强但覆盖用户太少,业务价值有限,要不要用取决于场景。
4. 用 Python 跑通 FP-Growth:同一份数据的另一条路径
4.1 接口切换与参数对照
mlxtend的 FP-Growth 接口和 Apriori 几乎一模一样:
from mlxtend.frequent_patterns import fpgrowth # 同样的布尔矩阵,同样的 min_support frequent_itemsets_fp = fpgrowth( df, min_support=0.5, use_colnames=True ) frequent_itemsets_fp = frequent_itemsets_fp.sort_values( by='support', ascending=False ).reset_index(drop=True) print(frequent_itemsets_fp)参数对照很直接:min_support含义完全一致,use_colnames也一样。区别在于fpgrowth内部先建 FP-Tree 再递归挖掘,不需要生成候选集。在mlxtend的实现里,fpgrowth还支持max_len参数限制项集最大长度,数据量大时可以用来控制内存。
4.2 对比测试脚本:同一数据同一阈值下的耗时与结果
要真正对比两个算法,需要写一个统一的测试脚本,控制变量:
import time from mlxtend.frequent_patterns import apriori, fpgrowth def benchmark(df, min_support, max_len=None): results = {} # Apriori start = time.perf_counter() fi_ap = apriori(df, min_support=min_support, use_colnames=True, max_len=max_len) results['apriori_time'] = time.perf_counter() - start results['apriori_count'] = len(fi_ap) # FP-Growth start = time.perf_counter() fi_fp = fpgrowth(df, min_support=min_support, use_colnames=True, max_len=max_len) results['fpgrowth_time'] = time.perf_counter() - start results['fpgrowth_count'] = len(fi_fp) # 验证两者结果是否一致 set_ap = set(fi_ap['itemsets']) set_fp = set(fi_fp['itemsets']) results['same_result'] = (set_ap == set_fp) return results # 用不同支持度跑对比 for ms in [0.5, 0.3, 0.1]: r = benchmark(df, min_support=ms) print(f"min_support={ms}: " f"Apriori {r['apriori_time']:.4f}s/{r['apriori_count']}项, " f"FP-Growth {r['fpgrowth_time']:.4f}s/{r['fpgrowth_count']}项, " f"结果一致={r['same_result']}")这个脚本做了三件事:分别计时、统计频繁项集数量、用集合比较验证两个算法输出是否一致。time.perf_counter()比time.time()精度更高,适合测短耗时。max_len参数在两个函数里都支持,用来限制项集最大长度,避免长事务导致组合爆炸。
在小数据集上,两者结果应该完全一致,耗时差异也不明显。真正的差距要在数据量上来之后才能看到。
4.3 用真实规模数据看差距:一个可复现的测试方法
手头没有大数据集时,可以用程序生成模拟交易数据来观察趋势:
import random import pandas as pd from mlxtend.preprocessing import TransactionEncoder def generate_transactions(n_trans, n_items, avg_len, seed=42): random.seed(seed) items = [f"item_{i}" for i in range(n_items)] transactions = [] for _ in range(n_trans): length = max(1, int(random.gauss(avg_len, avg_len * 0.3))) length = min(length, n_items) trans = random.sample(items, length) transactions.append(trans) return transactions # 生成 5000 条交易,200 种商品,平均每条 8 件 trans = generate_transactions(5000, 200, 8) te = TransactionEncoder() df_large = pd.DataFrame( te.fit(trans).transform(trans), columns=te.columns_ ) # 在较大数据上对比 for ms in [0.05, 0.02, 0.01]: r = benchmark(df_large, min_support=ms) print(f"min_support={ms}: " f"Apriori {r['apriori_time']:.2f}s, " f"FP-Growth {r['fpgrowth_time']:.2f}s, " f"结果一致={r['same_result']}")生成数据时用高斯分布控制每条交易的长度,比均匀分布更接近真实场景。seed固定保证可复现。在 5000 条交易、200 种商品的规模下,当min_support降到 0.01 时,Apriori 的耗时通常会明显高于 FP-Growth,具体倍数取决于数据稠密度和机器性能。这个脚本的价值在于:你可以用自己的数据替换生成部分,直接观察两个算法在自己场景下的表现。
5. 避坑与排查:频繁项集挖掘里最容易翻车的五个地方
5.1 现象:min_support 设太低导致内存爆掉
原因:支持度阈值直接决定候选集和频繁项集的数量。设 0.01 意味着一个项集只要在 1% 的交易里出现就算频繁,在几万条交易的数据上,频繁 1 项集可能就有几百个,候选 2 项集轻松上十万,Apriori 的候选表会吃掉大量内存,FP-Growth 的递归条件树也会撑爆调用栈。
解决:先用min_support=0.1跑一遍看频繁项集数量,如果只有几十个,逐步降到 0.05、0.02,每次观察数量和耗时变化。同时给max_len设一个上限,比如 3 或 4,避免生成超长项集。生产环境建议把支持度阈值和业务方对齐,不要单纯为了「多挖点」把阈值压到没有业务意义。
5.2 现象:两个算法结果不一致
原因:最常见的是输入数据没有对齐。比如 Apriori 用的 DataFrame 和 FP-Growth 用的不是同一份,或者中间做了过滤导致行数不同。另一个可能是mlxtend版本差异,不同版本对max_len的默认处理可能不同。
解决:在对比脚本里强制用同一个 DataFrame 变量,并且在调用前打印df.shape确认。比较结果时用set而不是列表,因为两个算法输出的项集顺序可能不同。如果确认输入一致但结果仍有差异,检查mlxtend版本,用pip show mlxtend看版本号,必要时固定版本。
5.3 现象:商品名有空格或大小写不一致,导致项集被拆散
原因:TransactionEncoder按字符串精确匹配列名。「牛奶」和「牛奶 」会被当成两个不同商品,「Apple」和「apple」也是。这种问题在真实数据里非常常见,尤其是从 Excel 或 CSV 读进来的数据。
解决:在TransactionEncoder之前做一轮清洗:
def clean_transactions(transactions): cleaned = [] for trans in transactions: items = [item.strip().lower() for item in trans if item.strip()] cleaned.append(list(set(items))) # 去重 return cleaned transactions = clean_transactions(transactions)strip()去首尾空格,lower()统一小写,set()去掉同一条交易里的重复商品。这一步看起来不起眼,但不做的话后面所有结果都不可信。
5.4 现象:关联规则 lift 很高但业务不认
原因:lift高只说明统计上正相关强,不代表业务上有因果关系或可操作性。比如「尿布 → 啤酒」这种经典案例,如果支持度只有 0.001,意味着只有极少数交易同时包含两者,规则虽然强但覆盖用户太少,没法支撑运营决策。
解决:同时看support、confidence、lift三个指标,不要只看一个。我一般会设一个最低支持度门槛(比如 0.01),在这个基础上按lift排序,再人工筛选有业务含义的规则。另外可以把规则按前件分组,看同一前件下哪些后件的lift最高,这样更容易发现可操作的组合。
5.5 现象:FP-Growth 递归报 RecursionError
原因:FP-Growth 递归建条件树时,如果数据里存在大量长事务且支持度设得很低,递归深度可能超过 Python 默认的 1000 层限制。
解决:优先用mlxtend的fpgrowth而不是自己手写递归版本,库实现通常做了优化。如果确实需要自己实现,可以用sys.setrecursionlimit()提高限制,但更稳妥的做法是改用迭代方式或者限制max_len。另外检查数据里有没有异常长的交易记录,比如某条交易包含几百个商品,这种记录往往需要单独处理。
6. 进阶技巧:用支持度扫描和结果缓存把对比测试做扎实
跑通两个算法只是第一步,真正做对比测试时,单次运行的结果受数据分布、机器负载影响很大,需要更系统的验证方法。我一般会做支持度扫描:固定数据集,让min_support从 0.5 逐步降到 0.01,每个阈值下分别跑 Apriori 和 FP-Growth,记录耗时和频繁项集数量,画成曲线看趋势。
import time import pandas as pd from mlxtend.frequent_patterns import apriori, fpgrowth def support_sweep(df, thresholds): records = [] for ms in thresholds: for name, func in [('apriori', apriori), ('fpgrowth', fpgrowth)]: start = time.perf_counter() fi = func(df, min_support=ms, use_colnames=True) elapsed = time.perf_counter() - start records.append({ 'algorithm': name, 'min_support': ms, 'time': elapsed, 'n_itemsets': len(fi) }) return pd.DataFrame(records) thresholds = [0.5, 0.3, 0.2, 0.1, 0.05, 0.03, 0.02, 0.01] result_df = support_sweep(df_large, thresholds) print(result_df.pivot(index='min_support', columns='algorithm', values='time'))这个脚本的输出是一张透视表,行是支持度,列是算法,值是耗时。从表里能直接看出:在哪个支持度区间两个算法差距开始拉大,FP-Growth 的优势在什么条件下最明显。pivot之后还可以加一列比值apriori_time / fpgrowth_time,量化加速比。
另一个实用技巧是结果缓存。同一份数据、同一组参数反复跑很浪费时间,可以把频繁项集结果存成 CSV 或 pickle:
import os import pickle def cached_mining(df, min_support, cache_dir='cache'): os.makedirs(cache_dir, exist_ok=True) key = f"fi_{df.shape[0]}_{df.shape[1]}_{min_support}.pkl" path = os.path.join(cache_dir, key) if os.path.exists(path): with open(path, 'rb') as f: return pickle.load(f) fi = fpgrowth(df, min_support=min_support, use_colnames=True) with open(path, 'wb') as f: pickle.dump(fi, f) return fi缓存键用数据形状加支持度拼成,简单但够用。注意frozenset可以直接 pickle,不需要额外转换。这个习惯在调参阶段能省下大量重复计算时间。
最后说一个我踩过的坑:早期做对比测试时,我只跑了一次就下结论说 FP-Growth 比 Apriori 快多少倍,后来发现那次 Apriori 刚好赶上机器在跑其他任务,耗时被拉高了。后来我养成的习惯是每个配置至少跑三次取中位数,并且把time.perf_counter()换成timeit模块做多次采样。对比测试这件事,单次数据永远只能当参考,趋势和量级才是可信的。希望帮到你。
本文还有配套的精品资源,点击获取