简介:资源面向数据挖掘初学者与算法实现人员,聚焦关联规则挖掘中Apriori与FP-growth两大经典算法,广泛应用于购物篮分析、用户行为挖掘等场景。压缩包内共3个文件,包含两个Python实现脚本以及一份测试数据集,整体仅80KB,轻量易用,无需复杂环境即可运行。已有824人学习下载。apriori.py实现了基于迭代候选集生成与支持度、置信度计算的经典流程,fpgrowth.py则通过构建FP树与条件模式基递归挖掘频繁项集,配套的data.txt可直接用于验证算法效果;两个脚本均包含清晰注释,支持自定义最小支持度和置信度阈值,方便不同场景下的实验对比。读者可对比两种算法在执行时间、内存消耗及关联规则质量上的差异,深入理解频繁项集挖掘原理,并灵活选择适合业务场景的算法方案,适用于课程作业、项目实战或算法原理学习。
1. 关联规则挖掘:两个脚本加一份数据,把 Apriori 和 FP-growth 一次跑明白
做数据挖掘的人基本都绕不开关联规则挖掘,最常见的场景就是购物篮分析——一堆订单明细摆在面前,想知道“买了 A 的人是不是大概率也会买 B”,手动翻数据翻到眼瞎也看不出规律,这时候就需要 Apriori 这类算法来替你算。这份关联规则挖掘资源包里就三个文件:apriori.py、fpgrowth.py和data.txt,前者实现了经典的 Apriori 算法,后者是 FP-growth 的完整实现,配套的文本数据可以直接拿来跑实验。如果你正在学数据挖掘课程,或者工作中要基于交易数据找物品组合规律,这份资源足够让你在半小时内把两个算法的完整流程走一遍,连环境都不用额外装——只要本地有 Python 3 就能跑。
2. Apriori 核心机制与代码拆解:支持度、置信度、候选集剪枝
2.1 先搞清楚 Apriori 在算什么:支持度与置信度的直观理解
Apriori 算法想回答的问题很简单:哪些商品组合频繁出现,以及当用户买了某些商品后,有多大可能还会买另一个商品。这背后就是两个基础指标——支持度(Support)和置信度(Confidence)。
支持度的计算方式是“包含某项集的订单数除以总订单数”。比如一共有 100 笔订单,其中 20 笔同时包含牛奶和面包,那 {牛奶, 面包} 这个项集的支持度就是 20%。支持度度量的是“这个组合有多普遍”,如果某个商品组合只在 1% 的订单里出现,那基于它做的推荐意义就不大,因为哪怕置信度再高,覆盖面也太窄。
置信度算的是条件概率,公式为置信度(A→B) = 支持度(A∪B) / 支持度(A)。它回答的是“买了 A 的客户里有多少人同时也买了 B”。支持度告诉你规则覆盖了多少人,置信度告诉你规则在已覆盖人群中的准确程度。实际使用中通常还会引入提升度(Lift),用来判断 A 和 B 是正相关、独立还是负相关,这个到后面进阶部分再细说。
Apriori 算法在计算过程中依赖一个关键性质:如果一个项集是频繁的,它的所有子集也必然是频繁的;反过来,如果一个项集不频繁,那所有包含它的超集也不可能频繁。这个性质被称为 Apriori 性质,也是算法名字的来源。它的作用就是剪枝——在生成候选集时,直接扔掉那些含有非频繁子集的组合,从而大幅减少需要扫描数据库的次数。
2.2 apriori.py 代码逐段解析:从候选集生成到规则输出
打开apriori.py,核心流程基本分为两步:第一步是发现频繁项集,第二步是从频繁项集中提取强关联规则。先看频繁项集的生成逻辑,代码中通常会用字典来存储候选项集的支持度计数,每次迭代都扫描一次数据:
def generate_candidates(frequent_items, k): """ 由 k-1 阶频繁项集生成 k 阶候选集 frequent_items: 上一轮的频繁项集列表,每个项集是一个 frozenset k: 当前要生成的项集长度 """ candidates = [] n = len(frequent_items) for i in range(n): for j in range(i + 1, n): # 取两个 k-1 项集,只有当它们前 k-2 个元素完全相同时才合并 set1 = list(frequent_items[i]) set2 = list(frequent_items[j]) set1.sort() set2.sort() if set1[:-1] == set2[:-1]: # 合并构成新的 k 项集 new_candidate = frozenset(set1[:] + [set2[-1]]) candidates.append(new_candidate) return candidates这段代码的关键在于“前 k-2 个元素相同才合并”的判断。比如上一轮的频繁项集里有 {牛奶, 面包} 和 {牛奶, 苹果},前一个元素都是牛奶,就能合并成 {牛奶, 面包, 苹果}。如果直接两两任意合并,候选集会爆炸式增长,剪枝就失效了。frozenset的使用也很有讲究——它可哈希,能安全地作为字典键使用,方便后续做支持度计数的存储。
生成候选集后,需要对候选集做剪枝。剪枝操作就是把每个候选集拆成所有 k-1 阶子集,逐一检查是否都在上一轮的频繁项集中,任何一个子集不在就说明该候选集不可能频繁,直接丢掉:
def prune_candidates(candidates, prev_frequent): """ 剪枝:候选集中任一 k-1 子集如果不在上轮的频繁项集里,就删除该候选集 """ pruned = [] for candidate in candidates: valid = True # 生成候选集的全部 k-1 子集 for item in candidate: subset = frozenset(candidate - {item}) if subset not in prev_frequent: valid = False break if valid: pruned.append(candidate) return pruned参数说明:prev_frequent必须是集合类型(用frozenset作为元素),这样subset not in prev_frequent的成员判断才能达到 O(1) 的复杂度。如果这里传入列表,数据量一大每次判断就是一次线性扫描,性能会明显变差。
接下来的支持度计算就是一次全表扫描,统计每个候选集在订单中出现的次数,再除以订单总数。到这里,频繁项集就算找完了。找完频繁项集后,规则提取的逻辑也不复杂——每个频繁项集都可以拆成前件和后件,比如频繁项集 {牛奶, 面包, 苹果} 可以拆成{牛奶, 面包} → {苹果}、{牛奶} → {面包, 苹果}等多种形式,算每种拆分方式的置信度,大于最小置信度阈值的规则就保留下来。
2.3 把 apriori.py 跑起来:参数设置与输出解读
运行这个脚本前先确认数据格式。data.txt中每一行代表一笔订单,项目之间用逗号分隔,类似牛奶,面包,苹果这样的形式。直接在项目根目录执行:
python apriori.py data.txt 0.1 0.5这里0.1是最小支持度阈值,0.5是最小置信度阈值,分别表示“项集至少要出现在 10% 的订单里”和“规则的置信度至少要达到 50%”。程序跑完后会打印出两类结果:频繁项集列表和满足置信度要求的关联规则。
第一次跑的时候建议把最小支持度调高一些,比如 0.2,这样频繁项集数量少,看得清楚;跑通之后再逐步调低,观察规则数量的变化。如果发现规则输出了一大堆没意义的组合,多半是支持度阈值设得太低了——数据量小的时候,低阈值会把噪声也带进来。
3. FP-growth 算法实战:FP 树构建、条件模式基与效率对比
3.1 Apriori 的痛点在哪里,FP-growth 又是怎么绕过去的
Apriori 算法最大的问题是每生成一阶候选集就要完整扫描一次数据库,而且要生成大量候选项集。假如有 1000 个商品,光是 2 阶候选集就有接近 50 万个,每轮扫描全表做计数,时间开销非常大。FP-growth 的思路完全不同——它只需要扫描数据库两次,第一次扫描统计所有单项目的频次并排序,第二次扫描把所有订单压缩成一棵频繁模式树(FP 树),之后所有的频繁项集挖掘都在树上完成,不再回头扫数据库。
FP 树的每个节点代表一个商品,节点上记录了从根节点到该节点路径出现的次数。同一商品在树中可能出现多次(因为不同订单的前缀路径不同),但相同前缀的订单会共享路径。这棵树的构建过程相当于把订单数据做了高度压缩,所有频繁项集的信息都保留在这棵树里了。
拿data.txt里的实际数据举例:假设跟牛奶相关的订单有 300 笔,其中 250 笔同时包含面包,那 FP 树中牛奶的子节点面包的计数就是 250。这些节点计数就是后续挖掘的条件模式基的统计基础。
3.2 fpgrowth.py 代码解析:头表、建树与递归挖掘
fpgrowth.py的实现里,第一步是扫描数据构建头表(header table)。头表记录了每个商品的总频次,并按频次降序排序。这一步非常关键,因为排序直接影响 FP 树的压缩率——频次高的商品放在路径前面,前缀相同的订单才能有效共享节点。看一下核心的建树逻辑:
def build_fp_tree(transactions, min_support): """ 构建 FP 树 transactions: 所有订单的列表,每个订单是商品列表 min_support: 最小支持度阈值(绝对计数或比例) """ # 第一次扫描:统计所有单项目的频次 item_count = {} for trans in transactions: for item in trans: item_count[item] = item_count.get(item, 0) + 1 # 过滤掉不满足最小支持度的项目,并按频次降序排序 freq_items = {item: cnt for item, cnt in item_count.items() if cnt >= min_support} sorted_items = sorted(freq_items.items(), key=lambda x: x[1], reverse=True) # 建立项目到排序索引的映射 item_index = {item[0]: i for i, item in enumerate(sorted_items)} # 第二次扫描:逐条事务插入 FP 树 root = FPTreeNode(None, 0) for trans in transactions: # 过滤并按频次排序当前事务中的项目 filtered = [item for item in trans if item in item_index] filtered.sort(key=lambda item: item_index[item]) insert_transaction(root, filtered) return root, item_count逻辑说明:第一次扫描统计频次后,先做一次最小支持度过滤——支持度达不到阈值的项目连建树的机会都没有。排序索引item_index的作用是避免每次插入时都重新排序。第二次扫描会取出每笔订单中的项目,过滤掉低频项,再按头表顺序排列后插入树中。插入时采用共享前缀策略,路径上已存在的节点直接累加计数,不存在的节点才新建,这样相同的订单前缀就不会在树中产生重复分支。
建树完成后就进入递归挖掘阶段,核心操作是找条件模式基。FP-growth 的递归思路是:从长度 1 的频繁项开始,在 FP 树上找到包含该节点的所有前缀路径,每条路径上的节点集合加上该节点就构成了条件模式基,然后基于这些条件模式基构建一棵条件 FP 树,继续递归挖掘。
这里有个性能关键点:fpgrowth.py里递归挖掘时通常会维护一个前缀路径的列表,而不是每次从整棵树重新遍历。如果不做这个优化,同一个节点可能被反复遍历多次,复杂度会指数级上升。
def mine_fp_tree(tree, header_table, prefix, min_support, result): """ 递归挖掘 FP 树上的频繁项集 tree: 当前 FP 树 header_table: 头表,记录每个项的节点链 prefix: 当前挖掘的前缀项集 min_support: 最小支持度阈值 result: 收集结果的列表 """ items = list(header_table.keys()) for item in items: new_prefix = prefix + [item] result.append(new_prefix) # 收集当前项的所有前缀路径 conditional_patterns = [] node = header_table[item].node_link while node is not None: prefix_path = [] parent = node.parent while parent.parent is not None: prefix_path.append(parent.item) parent = parent.parent # 路径出现次数等于当前节点的计数 conditional_patterns.extend([prefix_path] * node.count) node = node.node_link # 基于条件模式基构建条件 FP 树并递归 if conditional_patterns: cond_tree, cond_header = build_conditional_tree( conditional_patterns, min_support) if cond_header: mine_fp_tree(cond_tree, cond_header, new_prefix, min_support, result)参数说明:node_link是把所有相同商品的节点串成一条链的指针,通过它可以找到当前商品在树中的所有位置。prefix_path收集的是从根到当前节点的路径,但不包含当前节点本身。递归的终止条件是条件模式基为空,或者条件模式基中所有项目的总频次低于最小支持度。
3.3 同一份数据跑两个算法:执行时间与适用场景的取舍
用data.txt分别跑apriori.py和fpgrowth.py,在最小支持度为 0.05 的设置下,FP-growth 通常能比 Apriori 快几倍到十几倍。Apriori 需要反复生成候选集并扫描数据库,复杂度受数据维度影响很大;FP-growth 只在建树前扫两次数据,后面全在内存中的树上做递归。
但这不意味着 Apriori 就该被淘汰。Apriori 的天然优势是简单、稳定、易手写实现,适合数据量小或只在学习阶段理解算法原理的场景。FP-growth 虽然快,但树结构在内存中的占用也不容忽视——如果数据维度极高(比如十多万个不同的商品),FP 树本身可能吃满内存。另外 FP-growth 的代码复杂度明显更高,出问题时的排查难度也更大。实际项目中我通常的做法是:数据量百万级以内、特征列不多时直接用 Apriori,跑一次大概几十秒能接受;如果数据量到了千万级、商品种类也多,就换 FP-growth,或者考虑用 Spark MLlib 的 FPGrowth 做分布式计算。
4. 关联规则挖掘常见坑与排查记录:编码、支持度阈值、空数据处理
4.1 现象:中文商品名导致规则输出乱码或程序直接报错
原因:data.txt文件编码不是 UTF-8,或者脚本里用open()读取时没有指定编码参数。Windows 环境下常见的 GBK 编码文件,在 macOS 或 Linux 上默认按 UTF-8 读取会直接抛出UnicodeDecodeError。
解决:读取文件时明确指定编码,同时容错处理:
with open('data.txt', 'r', encoding='utf-8') as f: transactions = [line.strip().split(',') for line in f.readlines()]如果文件是 GBK 编码,把utf-8换成gbk即可。不确定编码时,可以用chardet库检测,或者直接用编辑器打开看中文是否正常。从资源包解压后的文件一般能直接用,但复制到其他平台或改过文件保存格式后,这一步最容易出问题。
4.2 现象:程序跑完后没有输出任何频繁项集
原因:最小支持度阈值设置得比数据中任何项集的实际支持度都高。比如data.txt里有 50 条订单,最小支持度设成 0.1,那就是要求某个项集至少出现在 5 条订单里;如果数据里最频繁的商品也只出现在 3 条订单中,结果集就是空的。
解决:先跑一个低阈值版本,比如 0.01,确认数据里有频繁项集,再逐步上调。或者直接跑一次统计数据里的单项目频次分布,看看最频繁商品的支持度水平,以它为参考设置最小支持度。调试阶段我的习惯是写一个几行的统计脚本,人工看一下数据分布再定参数,而不是盲猜阈值。
4.3 现象:挖掘出的规则置信度很高,但实际业务中完全不适用
原因:忽视了支持度的约束。置信度是条件概率,只要A→B中 A 出现得足够少,哪怕只出现几次且每次都买了 B,置信度也能接近 100%。比如数据中只有两笔订单包含“寿司醋”,这两笔都买了“电饭煲”,那寿司醋→电饭煲的置信度就是 100%,但支持度只有 0.2%,这条规则对业务没有任何指导意义。
解决:同时设置最小支持度和最小置信度两个阈值,并且建议把最小支持度优先设置合理。如果只需要高端商品关联,可以在数据预处理阶段过滤掉低频商品,或者按品类聚合后再跑关联规则。Apriori 和 FP-growth 的代码中如果只实现了置信度过滤,要注意手动补上支持度筛选。
4.4 现象:FP-growth 建树时内存占用飙升甚至 OOM
原因:数据中商品组合复杂度高,或者最小支持度设得过低,导致 FP 树节点数量过大。FP 树的压缩效果依赖于订单之间的公共前缀,订单差异性很大时树会非常膨胀,占用的内存可能超过原始数据几十倍。
解决:提高最小支持度阈值;过滤高频噪声项太少的订单;如果数据量太大,考虑先对订单做采样,或者改用并行版本的 FP-growth 实现。从工程经验上讲,FP 树的内存占用受“频繁项组合数”影响远大于受“订单数”影响,所以 reduce 组合维度才是治本手段。
4.5 现象:数据文件包含空行或格式不一致的行,程序中途报错
原因:data.txt末尾有空行、某行缺少分隔符,或者有订单只有一项商品。脚本如果直接用split(',')解析,空行会产生空列表[],程序处理时可能因为索引越界或键不存在而崩掉。
解决:读取时做过滤和清洗。常见做法是跳过空行,并把单项目订单保留作为有效输入。一种稳妥的加载方式是:
transactions = [] with open('data.txt', 'r', encoding='utf-8') as f: for line in f: line = line.strip() if not line: continue items = [item.strip() for item in line.split(',') if item.strip()] if items: transactions.append(items)这段代码同时过滤了空行、空项目,并去掉了项目首尾的多余空格。注意这里把if items放在后面,能过滤掉全空白行但保留合法的单项目订单,避免影响支持度计算的总订单数统计。
5. 基于 data.txt 的完整实验流程:从数据预处理到规则输出
5.1 先摸清 data.txt 的数据形态与统计口径
拿到资源包后第一步不是直接跑脚本,而是先看数据。打开data.txt大概浏览前几行,了解每行的项目数、项目类型、总订单数。这个步骤决定了后续的参数设置——最小支持度阈值一定和数据集规模与项目分布强相关。
用下面这段代码快速统计基础信息:
import collections with open('data.txt', 'r', encoding='utf-8') as f: lines = [line.strip() for line in f if line.strip()] transactions = [line.split(',') for line in lines] print("总订单数:", len(transactions)) all_items = [item for trans in transactions for item in trans] item_counter = collections.Counter(all_items) print("不同商品数:", len(item_counter)) print("出现次数最多的前10个商品:", item_counter.most_common(10)) order_size = [len(t) for t in transactions] print("每单平均商品数:", sum(order_size) / len(order_size)) print("最长的订单包含商品数:", max(order_size))从这里可以直观判断出数据的稀疏程度和频繁项的大致分布。比如订单数 3000 条左右、商品种类几十个、每单平均 3~4 个商品,那最小支持度设 0.02(约 60 个订单)就会比较合理。如果每单平均商品数很大,阈值可以适当调高,因为组合空间更大,容易挖出大量低价值规则。
5.2 从原始数据到关联规则的完整跑法
步骤一:确保目录下有apriori.py、fpgrowth.py、data.txt三个文件,Python 版本建议 3.6 以上。步骤二:用低阈值跑一遍 Apriori 验证数据格式没问 题。
python apriori.py data.txt 0.02 0.4运行后观察输出的频繁项集数量是否在合理范围——如果只输出几个项集,说明数据本身关联性弱或阈值偏高;如果输出几千个项集,说明阈值过低了,后续规则会出现大量无意义的组合。
步骤三:跑 FP-growth 对比:
python fpgrowth.py data.txt 0.02 0.4对比两个文件输出的频繁项集是否一致。两者在相同支持度阈值下应该挖出相同的频繁项集(这是算法正确性的基本校验),但 FP-growth 的运行时间会明显更短。如果两者结果不一致,优先检查实现中的”支持度计数是否重复统计了重复商品“——比如同一订单中同一个商品出现了两次,某些实现在统计时会错误地重复计数。
步骤四:调整参数观察规则变化。把最小支持度从 0.02 依次调整到 0.05、0.1,记录频繁项集数量的变化,找出数量骤降的拐点。这个拐点通常就是数据集中自然存在的关联强度分界线,实际业务中会优先选择拐点附近的阈值。
5.3 输出规则怎么读:有效规则与垃圾规则的区分
关联规则挖掘的脚本通常会输出形如牛奶、面包 -> 黄油 (支持度: 0.03, 置信度: 0.65)的结果。理解的时候要区分两类信息——支持度描述的是这条规则覆盖的订单比例,置信度描述的是在满足前件的订单中后件出现的概率。
对业务有价值的规则通常具备两个特征:一是支持度不能太低,代表可作用的用户群足够大;二是置信度明显高于后件在所有订单中的整体出现比例。比如黄油在全部订单中占比 20%,而牛奶、面包 → 黄油的置信度是 60%,说明买了牛奶和面包的用户买黄油的概率是整体水平的 3 倍,这才是真正的强关联信号。如果置信度只是从 20% 提到 25%,提升度也就 1.25,这种规则虽然置信度过了阈值,但业务价值有限。
因此拿到规则输出后,我通常会把结果按“支持度 × 置信度”组合排序优先看,而不是只看置信度单列。很多脚本默认按置信度排序,这样会把前面提到的那种“低频高置信”的噪声规则顶到最前面。
6. 进阶玩法:引入提升度筛规则,用数据分布反推最优阈值
讲完常规流程,最后给一个实用技巧:给脚本加上提升度输出,并把最小提升度作为第三个过滤条件。提升度的公式是Lift(A→B) = 置信度(A→B) / 支持度(B),它衡量的是“A 出现时 B 出现的概率”与“B 整体出现的概率”的比值。提升度大于 1 才说明 A 对 B 有正向促进作用,等于 1 说明两者独立,小于 1 则是负相关——比如买键盘的人反而更少买鼠标,这时候的规则也很有业务价值,但常被忽略。
实现方式是在规则提取处加一段计算逻辑:
for rule in generated_rules: lift = rule.confidence / support_of_consequent if lift > min_lift: filtered_rules.append(rule)其中support_of_consequent需要提前统计好后件的整体支持度。如果手头的两个脚本里没有这个功能,你自己加个十行左右就能搞定。另一个值得做的实验是记录不同阈值组合下的频繁项集数量变化曲线,找到“阈值从 0.04 降到 0.03 时规则数突然从 20 条涨到 200 条”的那种拐点,把最小支持度固定在这个拐点附近,往往是兼顾挖掘深度和结果可解释性的最佳平衡位置。我实际用这个资源包复现时,第一次跑完看到满屏的“高置信度但低支持度”规则,才意识到单看置信度多容易误导人——从那以后,我每次跑关联规则都会强制把支持度、置信度、提升度三个指标一起看,确认规则在三个维度上都说得通才敢往下游推。这也是这份资源最值得花时间实验的地方。希望帮到你。
本文还有配套的精品资源,点击获取