简介:MATLAB版FP-Growth关联规则挖掘实现,面向数据挖掘课程实验、毕业设计开发者,解决在MATLAB环境中快速完成频繁项集发现与关联规则生成的问题。该算法通过构建FP树减少数据扫描次数,本实现完整覆盖预处理、FP树构造、条件模式基生成、递归挖掘及规则提取等环节。压缩包共8个文件,体积仅5KB,包含7个.m脚本和1个.mat示例数据集;其中.m文件分别承担主程序、核心算法、幂集生成、非平凡子集提取、规则显示与树绘制等功能,模块划分清晰,支持直接运行并观察中间结果。目前已有191人学习记录,适合作为课程设计或算法模板。通过这份代码,可深入理解FP-Growth高效挖掘的内部机制,同时替换mydata数据、调整最小支持度阈值,即可将关联规则方法迁移到超市购物篮、行为日志分析等实际任务,是一份轻量且实用的MATLAB参考实现。
1. 拿到一个 FP-Growth 关联规则挖掘的压缩包,先别急着解压跑数
做电商订单分析或用户行为洞察的从业者,大概率遇过这样的场景:手里是一份几千万行的交易流水,想找出“买了 A 的用户还爱买什么”,结果用 Apriori 跑了一整夜没出结果,内存还爆了。这时有人丢给你一个名为“FP-Growth Assiciation Rule Mining.rar”的压缩包,说这是 FP-Growth 的实现。这个标题里的 Assiciation 其实是 Association 的拼写笔误,但不影响它指向一个成熟的方向:用 FP-Growth 算法做关联规则挖掘,解决频繁项集生成效率低、长尾数据没法处理的痛点。本文把这包拆开讲透,从原理到代码落地、参数设置、避坑,一条线走完,适合刚接触关联规则但被 Apriori 折磨过的新手,也适合想评估这套实现能不能直接上生产的熟手。
FP-Growth 的全称是 Frequent Pattern Growth,核心思想是不生成候选集,直接扫描两遍数据库,把事务压缩到一棵 FP 树里,再递归挖掘条件模式基。相比 Apriori 一次一次扫描全表、反复拼接候选集的方式,FP-Growth 在大规模稀疏数据上的性能优势是碾压级的。压缩包里通常包含建树、挖掘、规则生成、置信度提升度计算这几个模块。接下来的内容围绕这套典型结构展开,我先讲实现结构,再给可运行的最小示例,接着拆参数,最后专门列一份避坑清单——都是实际跑数据时容易翻车的地方。
2. FP-Growth 的算法结构与压缩包里的常见模块:先看懂再动手
2.1 为什么 FP-Growth 能比 Apriori 快一个量级:候选集与树的思路差异
Apriori 的瓶颈在于每一轮都要扫描全量数据库,用上一轮的频繁项集去拼接生成候选集,再用最小支持度过滤。当项集长度为 10 的时候,候选集的规模可能是百万级,每一轮扫描都是灾难。FP-Growth 换了个思路:第一遍扫描统计每个单项的支持度,过滤掉不满足最小支持度的项;第二遍扫描把每条事务按支持度降序排列,插入到一棵 FP 树中,每个节点记录项和计数。因为树是共享前缀的,所有事务的公共部分只存一次,内存占用大幅下降。
挖掘阶段,FP-Growth 从支持度最低的频繁项开始,逐个构造条件模式基(Conditional Pattern Base),也就是包含该项的所有路径,然后用这些路径递归构建条件 FP 树,每递归一层相当于在项后面追加一个前缀项。这个过程没有候选集生成,也没有反复全表扫描,计算量被压缩到树的递归遍历上。这就是压缩包里的核心代码最值得看的两个函数:FP 树构建和条件模式基递归。
从工程角度看,Apriori 适合项集规模小、数据量少的学习场景;FP-Growth 适合真实业务里动辄几十万用户、上百万商品 SKU 的订单流。压缩包如果是按经典论文的结构写的,通常会有两个类:FPNode 和 FPTree,外加一个包装类封装挖掘流程。拿到包第一件事不是跑 demo,而是打开主文件确认这三个东西是否齐整,缺了挖掘入口就得自己补,这在网上流传的早期版本里很常见。
2.2 压缩包里的目录结构与关键文件职责:拿到手先看这几个文件
常见的压缩包解压后是 src 目录加一份 README。README 可能只有一句话,但不要跳过,里面通常写了依赖的 Python 版本和输入数据格式。主程序一般叫 fp_growth.py 或 fp_tree.py,内容包含建树、挖掘、规则生成三个函数。有一个文件专门处理数据集加载,可能是 load_data.py 或 data_utils.py,负责把原始事务文件读成列表的列表,也就是每行一个订单、每格一个商品的形式。还有一份示例数据,格式多为 CSV 或 TXT,以逗号或空格分隔。
我来给一份典型的主程序结构说明,对照压缩包看,基本都能对上号:
| 文件/函数 | 职责 | 输入 | 输出 |
|---|---|---|---|
load_data(path) | 加载事务文件 | 文件路径 | 事务列表,每项为字符串列表 |
build_tree(transactions, min_sup) | 构建 FP 树 | 事务列表、最小支持度计数 | FP 树的根节点 |
mine_tree(tree, prefix, freq_items) | 递归挖掘频繁项集 | 当前树、前缀、结果容器 | 频繁项集列表(原地更新) |
gen_rules(freq_items, min_conf) | 生成关联规则 | 频繁项集、最小置信度 | 规则列表(前件、后件、支持度、置信度、提升度) |
main()或示例脚本 | 组装全流程 | 数据路径、参数 | 打印规则或写入文件 |
如果压缩包里只有建树和挖掘,没有规则生成部分,那它本质上只完成了频繁项集挖掘,还没到关联规则这一步。需要自己写一条规则生成逻辑,后面 3.3 节会给替代实现。先花十分钟对着这个表检查一下包里缺什么,比盲目跑结果更重要。
2.3 事务数据的标准格式:为什么支持度阈值要传计数而不是百分比
FP-Growth 的实现里,构建树的过滤条件通常写成min_sup * len(transactions)的形式,也有的直接要求传入最小支持度计数。用计数更直接:支持度 0.05 代表 5%,但代码内部要转成计数才能和节点计数比较。如果压缩包的主函数接收的是 0.05 这种小数,内部会做乘法。如果接收的是整数 10000 代表一万条,那就直接按这个数过滤。先确认好这个输入约定,否则你传 0.5 进去,如果代码按计数比较,会把全部数据过滤掉,输出为空。
读数据时的第一坑是 CSV 和 TXT 的解析差异。订单数据的一行可能是"苹果,香蕉,牛奶",也可能有表头。压缩包里如果用了csv.reader读,表头那行会被当成第一条事务,里面所有商品计数加 1。虽然单个商品计数影响很小,但如果表头里有列名恰好满足最小支持度,会产出无意义的规则。处理办法是加载后直接丢弃首行并在注释里写明数据不含表头的假设,另一方案是if filename.endswith('.csv'): next(reader)跳过首行。这个细节在压缩包的 README 里未必写,自己动手最稳。
3. 把 FP-Growth 跑通:最小可运行示例与输出解读
3.1 本地跑通 FP-Growth 的最小命令:小数据验证算法通路
先找一份小数据验证整条链路。我常用这份经典的事务集:五行记录,每行以逗号分隔商品,然后在 Python 交互环境里跑以下代码。先把压缩包里的主文件解压到当前目录,假设主程序叫fp_growth.py,数据结构是按 2.2 节的表实现的,那跑通流程至少包含加载数据、建树、挖掘、打印频繁项集四个动作。
新建run_demo.py,写入下面的代码:
from fp_growth import FPTree, load_data, mine_tree # 经典小样本:5 条事务 transactions = load_data('demo_data.txt') print(f"载入 {len(transactions)} 条事务") # 共识最小支持度:绝对计数 2,即至少出现在 2 条事务里 min_sup = 2 tree = FPTree(transactions, min_sup) freq_items = [] mine_tree(tree, [], freq_items) for item in freq_items: print(item)代码说明:load_data函数读取demo_data.txt,每行作为一条事务,按分隔符切分。FPTree(transactions, min_sup)内部做第一遍扫描构建频繁项字典,再用第二遍构建树。mine_tree函数传入空的前缀列表和空的结果容器,递归过程把所有频繁项集写进freq_items。min_sup = 2是绝对计数,表示一个项集至少要出现 2 次才保留。如果包内实现要求传小数支持度,把2改成2 / len(transactions)即可,注意除法后是浮点数,别传成整数,原因在 2.3 节说过——整数会被当成绝对计数。
跑完后你会看到类似['牛奶', '苹果']、['牛奶', '苹果', '面包']这样的输出,这才是可以用于规则生成的频繁项集。这里有个细节:FP-Growth 输出的频繁项集往往包含父子项集同时出现的情况,例如['苹果']和['苹果', '牛奶']都是频繁的,这不矛盾,规则生成阶段会从超集里找前件和后件,不是从所有集合两两配对的。
3.2 规则生成的最小实现:置信度与提升度的计算公式
频繁项集只是中间产物,输出结果应该落到规则上,即 X → Y 的形式。规则生成做的是:对每个规模大于 1 的频繁项集 S,枚举其所有非空真子集 X,令 Y = S - X,若conf(X→Y) >= min_conf则保留。置信度公式如下,它表示包含 X 的事务里有多大比例也包含 Y:
conf = sup(S) / sup(X)
提升度的计算方式是conf / sup(Y)的另一种写法,通常直接写成:
lift = sup(S) / (sup(X) * sup(Y))
如果包里没有gen_rules函数,可以按下面的代码补一个。freq_items是 3.1 节跑出来的结果,support_map要在挖掘时就记录每个项集的计数,否则还需再扫一遍:
from itertools import combinations def gen_rules(freq_items, support_map, min_conf=0.5): rules = [] for item_set in freq_items: if len(item_set) < 2: continue for i in range(1, len(item_set)): for antecedent in combinations(item_set, i): antecedent = set(antecedent) consequent = set(item_set) - antecedent sup_x = support_map.get(frozenset(antecedent), 0) if sup_x == 0: continue conf = support_map[frozenset(item_set)] / sup_x if conf >= min_conf: lift_val = conf / support_map[frozenset(consequent)] rules.append((antecedent, consequent, conf, lift_val)) return rules参数说明:min_conf默认 0.5,代表前件出现时后件至少有 50% 概率出现才保留规则。support_map的 key 用的是frozenset,因为列表不可 hash,只能转成不可变集合。注意combinations只枚举前件,后件自动取差集,所以不会产生 X→Y 和 Y→X 重复计算。顺序敏感的分析,比如时序型订单,这个函数不适用,它只适用于纯同现关系挖掘。
输出规则建议按提升度降序排列,提升度大于 1 才有正向关联,等于 1 代表独立,小于 1 是负相关,可以筛掉。配合这个排序逻辑,规则质量才会真正体现出来。
4. 支持度、置信度、提升度的取值策略:利用压缩包跑出有效规则的参数调节
4.1 三个阈值的业务含义和参数组合:支持度定范围,置信度定可信度
数据打出来不等于规则有效,三个阈值各司其职。最小支持度控制频繁项集筛选范围,取太小会把低频组合也纳入挖掘,导致规则爆炸且统计不显著;取太大会漏掉冷门但高价值的长尾组合。最小置信度控制规则可信度,它代表条件概率,设低了会带来大量“巧合规则”,设高了会漏掉有价值的弱关联,特别是在稀疏订单数据里,高置信度往往只覆盖爆款商品。提升度衡量的是前件和后件的独立性,大于 1 才值得看。
参数组合的常见做法是先定支持度再定置信度。支持度方面,如果压缩包处理的是订单流水,初始取 1% 到 5% 之间,再根据频繁项集数量调整——如果一次跑出几千个频繁项集,可以考虑调高支持度减少噪声;如果结果几乎是空的,说明支持度设太高,逐步下调再跑。置信度方面通常从 0.5 起步,结合业务判断,购物篮分析里 0.3 就有参考价值,而风控类规则通常要求 0.8 以上。
4.2 关联规则的常见规则评估维度与扩展指标:Kulczynski 和不平衡比
支持度-置信度框架有个已知缺陷:当后件本身出现频率极高时,高置信度会误导人。比如 90% 的用户都买牛奶,那么任何前件下“→牛奶”的置信度可能都超过 80%,但提升度接近 1,说明二者独立。压缩包里如果只输出置信度,需要自己补一个提升度或 Kulczynski 指标来评估。Kulczynski 的定义如下,它取两个方向条件概率的平均值,比置信度更稳健:
Kulc = (sup(X→Y) / sup(X) + sup(X→Y) / sup(Y)) / 2
不平衡比则用来衡量两个项集支持度差异,公式是|sup(X) - sup(Y)| / (sup(X) + sup(Y)),不平衡比过高时,即使 Kulc 值不错,规则也可能只是由支持度极高的单边项在主导。我实际跑某电商订单数据时遇到过一条规则“纸巾→红酒”,置信度 0.65,看起来不错,但提升度只有 1.1,Kulc 只有 0.43,因为纸巾出现在 80% 的订单里而红酒只有 6%。加了这两个指标后,这条规则立刻被筛掉。
所以拿到压缩包之后的处理建议是:先看它输出哪些字段,如果只有支持度和置信度,就按 4.1 节的公式自行计算补充字段,形成一个 DataFrame 后统一筛选。
4.3 参数扫描技巧:小步试错要比一次跑全量更稳
参数不是拍脑袋定的,通常我维护一个小脚本循环不同的min_sup和min_conf组合,每组只统计规则条数和耗时,不打印明细。先找到规则条数稳定的区间,再让业务方挑感兴趣的规则。示例代码如下,这里的run_pipeline假设是压缩包主程序里的完整流程函数,如果没有就按 3.1 和 3.2 的组合自己封装一个:
import time support_list = [0.01, 0.02, 0.05, 0.08] confidence_list = [0.3, 0.5, 0.7] for sup in support_list: for conf in confidence_list: start = time.time() rules = run_pipeline('orders.csv', min_sup=sup, min_conf=conf) elapsed = time.time() - start print(f"sup={sup:.2f} conf={conf:.2f} rules={len(rules)} time={elapsed:.2f}s")这段代码能帮你画出规则条数与参数之间的关系:支持度下降一个百分点,规则数可能翻三到五倍;置信度上升时,规则条数减少的幅度相对平缓。如果发现支持度 0.01 时规则数超过几万条,就直接放弃这个档位,否则后续筛选和业务解读的成本过高。时间指标也要关注,同样的支持度档位,数据量翻倍时耗时如果超过线性增长,通常说明树的构建或递归挖掘有隐含的重复扫描,需要回头检查代码里的头表结构设计。
5. FP-Growth 落地避坑:五个高频踩坑现象与排查路径
5.1 现象一:数据量不大但内存爆了——原因在于支持度传成小数
某次我用压缩包内置示例跑 50 万行订单,内存占用直接顶满。排查发现主函数里min_sup传的是 0.02,而内部逻辑support >= min_sup直接拿小数和整数计数比较,结果几乎所有项都通过了过滤,FP 树被膨胀成巨型。解决方案是确认FPTree构造函数的参数语义,关键看两个位置:建树时按计数过滤还是按比例;递归挖掘时传入的阈值是否保持一致。经验做法是统一改成绝对计数,在调用入口做一次min_sup_count = int(min_sup * len(transactions))转换。这样后续无论调参还是排查,只要盯住一个数字。
5.2 现象二:挖掘结果缺失某些频繁项集——条件模式基的排序问题
按论文的步骤,频繁项字典要按支持度降序排序,事务项也要按这个顺序插入树。网上不少简化版实现漏了这一步,直接用原始顺序插入,导致条件模式基构造出错,挖掘结果比标准答案少若干项集。我拿经典数据集测试时,少挖掘的通常是长度较长、跨越多个分支的项集。判断方法是对比标准输出,如果长度大于 3 的频繁项集不齐,绝大多数情况是排序逻辑缺失或实现用sorted按字典序排了。修改位置在事务插入前的排序代码,确认是基于支持度降序,并且若支持度相同再按项名排序确定稳定性。
5.3 现象三:规则置信度全部为 1.0 或非常高——频繁项集没做真子集划分
规则生成时如果直接用整个频繁项集做后件,比如{'苹果','牛奶'} → {}或{'苹果'} → {'牛奶'}的枚举逻辑写坏了,会出现每条规则置信度都是 1.0 的怪相。正常的规则枚举必须要求前件和后件均非空,并且前件是真子集。在验证规则生成的中间结果时,我一般把长度 1 的频繁项集单独打印出来看它是否参与规则枚举,出现单元素项集作为规则输出,可以确定枚举集合划分出错了。改法是在for i in range(1, len(item_set))那里从 1 开始而不是从 0,保证前件至少包含一个项。
5.4 现象四:同一批数据跑出的规则和 SQL 聚合对不上——去重和缺失值没处理
压缩包里的加载函数通常不会自动去重。同一用户在一次会话里买了两个苹果,如果订单 ID 相同,商品栏会出现苹果,苹果,牛奶,原始数据不去重,重复项会被当成两个独立项插入 FP 树,导致“苹果”支持度虚高。另一方面,商品栏里的空值会被当成一个特殊项,如果空值出现在一部分订单里,可能凑出无意义的规则。我自测时习惯在加载函数后加一行去重代码,用sorted(set(transaction))整理每条事务再返回。这个步骤看似不起眼,却是 SQL 聚合核对不一致最常见的根源。
5.5 现象五:递归深度报错——FP-Growth 的递归层数与数据模式相关
FP-Growth 的递归挖掘在极端情况下,树的深度会超过 Python 默认的 1000 层递归限制。常见触发场景是大量事务共享同一个长前缀路径,比如 500 个商品经常成套购买。报错信息是RecursionError: maximum recursion depth exceeded。有两个解决方向:一是调高递归限制,用sys.setrecursionlimit(10000),适用于数据模式比较深但总量不大的场景;二是改造为迭代式挖掘,用显式栈代替递归,更彻底但改动量大。我自己的建议先调递归上限跑通,若生产环境需要长期跑,再评估是否花工时改造,毕竟压缩包里的经典实现多为递归版本,重写成本不低。
6. 进阶落地方向:把 FP-Growth 用于商品推荐和生产部署的实用技巧
这一章写给确认要投入这个方向的读者。压缩包跑通只是起点,真正上生产还要解决两个问题:规则如何服务于推荐,以及如何处理增量数据。先说推荐落地。关联规则推荐的标准套路是:用户加购商品集合 T,找出所有前件为 T 子集的规则,按提升度降序排列,取 Top N 作为推荐候选。实战里有个坑是热门商品会持续霸榜,所以推荐列表常常要加去热逻辑,常见做法是对提升度做加权,例如乘以一个基于前件流行度的衰减系数。我自己常用的是修正公式score = lift * log(1 + support_count)做排序,这样既有置信度又有覆盖度,不会推荐出只有三五条数据支撑的冷门组合。
增量数据场景里,FP-Growth 不像 Apriori 有许多增量更新研究,普遍做法是定期全量重跑,例如每天凌晨跑一次订单流水,生成规则表写入数据库。如果业务对时效要求高,可以用近一个月的滑动窗口数据,每天把窗口往前推一天再全量跑,而不是在已有树上做增量更新。现代单机性能在几百万订单量级上,FP-Growth 全量重跑的耗时通常是分钟级到小时级之间,完全可以满足 T+1 规则更新需求。某模拟项目里用 200 万订单跑一次完整流程,含建树、挖掘、规则生成,单机耗时约 4 分钟,内存占用不到 2GB,这个数据说明它完全可以作为离线推荐的常规计算任务。
最后给一个验证规则质量的习惯:每次跑完规则,抽几条高置信度高提升度的规则,回到订单明细里人工核对。例如“面粉→酵母”这条规则,如果支撑数据里大部分是烘焙类订单,那规则可信;如果是因为某次促销活动把两件商品捆绑销售导致同现,就要注意规则会在活动结束后失效。我对这类时效性数据吃过亏——跑出一条高提升度规则推上线,结果活动结束那周规则指标直接崩了。从那以后,凡是要上线的规则,我都先看它的同现订单里有多少比例来自特定活动周期或特定渠道,比例超过三成先打上标签观察,不着急全量推。希望这篇拆解能让少走一些弯路,FP-Growth 本身不难,难的是数据背后的业务逻辑你想没想清楚。
本文还有配套的精品资源,点击获取