简介这份资源是面向数据挖掘初学者与MATLAB使用者的FP-Growth关联规则挖掘实现包针对交易数据中频繁项集发现与规则提取这一典型问题帮助读者理解FP树构建、条件模式基生成与递归挖掘的完整流程。压缩包共8个文件以7个m脚本和1个mat数据集为主脚本分别承担主流程调度、FP树构建、幂集与子集计算、规则展示及树结构绘制等职责mydata数据集可直接用于演示算法运行整体约5KB轻量易读。目前已有191人学习下载。读者可借此掌握支持度与置信度的计算方式理解从频繁项集到关联规则的推导逻辑并在此基础上替换自有事务数据、调整最小支持度阈值将算法迁移至市场篮子分析、网络行为分析等场景是入门关联规则挖掘与MATLAB工程实践的一份实用参考。1. 从一份 FP-Growth 压缩包说起它到底能帮你省下多少造轮子的时间如果你正在做购物篮分析、交叉销售推荐或者需要从一堆交易记录里挖出“买了 A 的人大概率也会买 B”这种规则那你大概率绕不开关联规则挖掘。自己从零写 Apriori 不是不行但候选集爆炸和反复扫库的代价跑一次中等规模数据集就能让你等到怀疑人生。这份 FP-Growth 关联规则挖掘的 MATLAB 实现核心价值就在于把频繁模式树的构建和递归挖掘封装成了可直接调用的函数你不需要再手撸树结构也不用为了一个课程设计或原型验证去啃几百行指针操作。它适合两类人一类是刚接触数据挖掘、想跑通完整流程的新手另一类是已经有业务数据、想快速验证规则有效性的开发者。下面我就按“能干什么、怎么跑、怎么改”的顺序把这包东西拆开讲清楚。2. 项目结构与技术选型为什么是 MATLAB 加 FP-Growth2.1 目录里通常有什么拿到这个压缩包解压后你会看到一个典型的 MATLAB 工程目录。虽然不同人打包习惯略有差异但常见做法是分成数据、函数、脚本三层。数据层放交易样本函数层放 FP-Growth 的核心实现脚本层放调用示例和结果展示。我一般会先看有没有main.m或demo.m那是入口再看fp_growth.m或FPGrowth.m那是算法本体最后翻data/目录确认数据格式。如果包里带了README优先读它里面往往写了运行环境和依赖版本。一个合理的目录结构大概长这样FP-Growth-Association-Rule-Mining/ ├── data/ │ └── transactions.mat % 交易数据集每行一个事务 ├── src/ │ ├── fp_growth.m % FP-Growth 主函数 │ ├── build_fp_tree.m % 构建频繁模式树 │ ├── mine_tree.m % 递归挖掘频繁项集 │ └── generate_rules.m % 从频繁项集生成关联规则 ├── demo.m % 调用示例 └── README.md这个结构的好处是职责清晰。build_fp_tree只负责扫一遍数据建树mine_tree只负责在树上递归找频繁项集generate_rules只负责算置信度和提升度。你改其中任何一个环节不会把其他部分带崩。常见做法是把最小支持度和最小置信度作为参数传进去而不是写死在函数里这样调参时不用反复改代码。2.2 为什么选 FP-Growth 而不是 AprioriApriori 的思路是“先产生候选集再验证候选集”每轮都要重新扫描整个数据库。当数据量上去以后候选集数量可能是指数级的I/O 开销直接成为瓶颈。FP-Growth 换了个思路先把数据压进一棵 FP 树树上的每个节点代表一个项路径代表项之间的共现关系。建树只需要扫两遍数据库之后所有的频繁项集挖掘都在内存里的树上递归进行不再碰原始数据。这个差异在 MATLAB 里尤其明显。MATLAB 的循环效率本来就不算强项如果你用 Apriori 写多层嵌套循环去生成候选集数据量稍微大一点就会卡到没法看。FP-Growth 把大部分计算转化成了树操作和递归配合 MATLAB 的矩阵运算习惯实际跑起来会顺畅很多。当然MATLAB 也不是没有代价递归深度受限于栈空间事务特别长的时候需要留意。2.3 输入数据长什么样这个项目对输入数据的要求很宽松。最常见的是元胞数组每个元胞是一个事务里面装该项集。比如% 每一行代表一个事务事务内的字符串是商品名 transactions { {牛奶, 面包, 尿布} {可乐, 面包, 尿布, 啤酒} {牛奶, 尿布, 啤酒, 鸡蛋} {面包, 牛奶, 尿布, 啤酒} {面包, 牛奶, 尿布, 可乐} };如果你手上是数值型 ID也完全可以只要保证同一个项在所有事务里用同一个标识就行。有些实现会要求你先做一次编码转换把字符串映射成整数这样建树时比较和哈希更快。我一般会在预处理阶段加一步unique和ismember把原始标签转成连续整数跑完再映射回去。这一步不复杂但能省掉后面很多类型判断的麻烦。注意事务里的项不要有重复同一个事务里出现两次“牛奶”会让支持度计数偏大规则也会失真。预处理时用unique去个重几行代码的事。3. 核心模块拆解从建树到规则生成的完整链路3.1 第一遍扫描统计频繁 1 项集FP-Growth 的第一步和 Apriori 一样先数每个项出现了多少次。这一步决定了哪些项有资格进入 FP 树。低于最小支持度的项直接扔掉不参与后续建树。这样做的好处是树能小很多尤其是长尾数据里大量只出现一两次的项会被提前过滤掉。function [freq_items, support] scan_data(transactions, min_support) % 统计所有项的出现次数 all_items [transactions{:}]; % 把所有事务拼成一个长向量 [unique_items, ~, idx] unique(all_items); counts accumarray(idx(:), 1); % 每个项的出现次数 total numel(transactions); % 事务总数 support counts / total; % 支持度 出现次数 / 总事务数 % 只保留满足最小支持度的项 keep support min_support; freq_items unique_items(keep); support support(keep); % 按支持度降序排列这是 FP 树的排序规则 [support, order] sort(support, descend); freq_items freq_items(order); end这段代码的关键在最后几行。FP 树要求节点按支持度降序排列支持度高的项靠近根节点这样树更紧凑共享前缀更多。accumarray是 MATLAB 里做分组计数的常用手法比循环快得多。min_support一般取 0.01 到 0.1 之间具体看数据规模和业务容忍度。支持度设太高规则太少设太低树会膨胀挖掘时间直线上升。3.2 第二遍扫描构建 FP 树有了频繁 1 项集和它们的排序就可以建树了。建树的过程是对每个事务先按支持度降序过滤掉非频繁项然后从根节点开始能共享路径就共享不能共享就新建节点。每个节点维护一个计数表示经过该节点的路径有多少条。function root build_fp_tree(transactions, freq_items, min_support) root struct(name, [], count, 0, children, {{}}, parent, []); for i 1:numel(transactions) trans transactions{i}; % 只保留频繁项并按支持度降序排列 [~, ia, ib] intersect(freq_items, trans); if isempty(ia) continue; end sorted_items freq_items(sort(ia)); % 按 freq_items 的顺序排 % 从根节点开始插入 node root; for j 1:numel(sorted_items) item sorted_items{j}; child_idx find_child(node, item); if isempty(child_idx) % 没有这个子节点新建一个 new_node struct(name, item, count, 1, ... children, {{}}, parent, node); node.children{end1} new_node; node new_node; else % 已有子节点计数加一 node.children{child_idx}.count ... node.children{child_idx}.count 1; node node.children{child_idx}; end end end end这里用 struct 嵌套来模拟树节点每个节点存名字、计数、子节点列表和父指针。父指针在挖掘阶段会用到用来向上回溯找条件模式基。find_child是个辅助函数遍历当前节点的子节点找匹配名字。如果数据量大、节点多可以把子节点改成 containers.Map 来加速查找但 struct 数组在中小规模下足够用而且调试时看得清楚。建树完成后你会得到一棵压缩过的树。树上的每条路径代表一组共现的项节点的计数代表这条路径出现的次数。接下来所有挖掘都在这个结构上进行不再需要原始事务数据。3.3 递归挖掘从 FP 树到频繁项集挖掘的思路是从支持度最低的项开始为每个项构建条件模式基再递归建条件 FP 树。听起来绕但代码逻辑其实很规整。function freq_itemsets mine_tree(tree, prefix, min_support, freq_itemsets) % 收集树上所有单项及其计数 items collect_items(tree); % 按支持度升序处理从最不频繁的开始 [~, order] sort([items.count], ascend); items items(order); for i 1:numel(items) item items(i); new_prefix [prefix, {item.name}]; % 当前项本身就是一个频繁项集 freq_itemsets{end1} new_prefix; % 构建条件模式基 cond_patterns find_conditional_patterns(tree, item.name); if isempty(cond_patterns) continue; end % 用条件模式基建条件 FP 树 cond_tree build_cond_tree(cond_patterns, min_support); if ~isempty(cond_tree.children) % 递归挖掘 freq_itemsets mine_tree(cond_tree, new_prefix, ... min_support, freq_itemsets); end end endcollect_items遍历树收集所有节点和计数find_conditional_patterns沿着父指针向上走找到所有包含当前项的路径。每条路径的计数取路径上最小的节点计数因为一条路径能贡献的次数受限于最少的那个节点。条件 FP 树建好后递归调用mine_tree前缀不断累积直到没有条件模式基为止。这个递归过程是 FP-Growth 的核心也是容易出问题的地方。如果数据里存在大量长事务递归深度会很大MATLAB 的默认递归限制可能不够。我一般会在脚本开头加一句set(0, RecursionLimit, 2000)给自己留点余量。另外条件模式基的收集如果实现得粗糙重复路径会拖慢速度可以在建条件树之前做一次合并把相同路径的计数加起来。3.4 规则生成从频繁项集到关联规则频繁项集只是中间产物真正有用的是关联规则。规则的形式是 A → B意思是“买了 A 的人也会买 B”。衡量规则强度的两个指标是置信度和提升度。置信度是 P(B|A)提升度是 P(B|A) / P(B)提升度大于 1 才说明 A 和 B 正相关。function rules generate_rules(freq_itemsets, min_conf, support_map) rules {}; for i 1:numel(freq_itemsets) itemset freq_itemsets{i}; if numel(itemset) 2 continue; end % 枚举所有非空真子集作为前件 for j 1:(2^numel(itemset) - 2) mask dec2bin(j, numel(itemset)) 1; antecedent itemset(mask); consequent itemset(~mask); if isempty(antecedent) || isempty(consequent) continue; end supp_a get_support(antecedent, support_map); supp_ab get_support(itemset, support_map); conf supp_ab / supp_a; if conf min_conf lift conf / get_support(consequent, support_map); rules{end1} struct(antecedent, {antecedent}, ... consequent, {consequent}, confidence, conf, ... lift, lift); end end end enddec2bin那行是在枚举子集用二进制掩码的方式比递归枚举更紧凑。support_map是一个映射表存每个项集的支持度避免重复计算。min_conf一般取 0.5 到 0.8 之间太低会产出大量弱规则太高又会漏掉有意义的组合。提升度我建议一定要看置信度高但提升度接近 1 的规则说明后件本身就很常见前件并没有带来额外信息。提示规则数量爆炸时先按提升度排序再按置信度过滤。提升度前 20 的规则通常比置信度前 100 更有业务价值。4. 跑起来从解压到看到第一条规则的完整步骤4.1 环境准备与数据加载这个项目对 MATLAB 版本要求不高R2016b 以上基本都能跑。不需要额外工具箱核心函数只用了基础语法和数据结构。如果你用的是 Octave大部分代码也能兼容但 struct 嵌套和元胞数组的行为可能有细微差异遇到报错优先检查这两处。加载数据的方式取决于你拿到的数据格式。如果是.mat文件直接load如果是 CSV 或文本用readtable或textscan读进来再转成元胞数组。我一般会写一个load_data.m做统一入口把不同格式的数据都转成标准的事务元胞数组。function transactions load_data(filepath) % 根据扩展名选择读取方式 [~, ~, ext] fileparts(filepath); switch lower(ext) case .mat data load(filepath); fn fieldnames(data); transactions data.(fn{1}); case .csv tbl readtable(filepath); transactions table2cell(tbl); % 去掉空值 transactions cellfun((x) x(~cellfun(isempty, x)), ... transactions, UniformOutput, false); otherwise error(不支持的文件格式: %s, ext); end % 确保每个事务是行向量 transactions transactions(:); end这段代码处理了两种常见格式。.mat文件直接取第一个变量.csv用readtable读成表格再转元胞。cellfun那行是清理空值CSV 里经常有缺失项不清掉会在建树时报错。transactions(:)保证是列向量后面循环时不用再判断方向。4.2 参数设置与主流程调用主脚本一般长这样% demo.m clear; clc; % 1. 加载数据 transactions load_data(data/transactions.mat); fprintf(事务总数: %d\n, numel(transactions)); % 2. 设置参数 min_support 0.02; % 最小支持度 min_conf 0.6; % 最小置信度 % 3. 第一遍扫描 [freq_items, support] scan_data(transactions, min_support); fprintf(频繁1项集数量: %d\n, numel(freq_items)); % 4. 建树 root build_fp_tree(transactions, freq_items, min_support); % 5. 挖掘频繁项集 freq_itemsets {}; freq_itemsets mine_tree(root, {}, min_support, freq_itemsets); fprintf(频繁项集总数: %d\n, numel(freq_itemsets)); % 6. 生成规则 support_map build_support_map(freq_itemsets, transactions); rules generate_rules(freq_itemsets, min_conf, support_map); fprintf(关联规则数量: %d\n, numel(rules)); % 7. 展示前10条规则 show_rules(rules, 10);这个流程把前面讲的模块串起来了。min_support和min_conf是两个需要反复调的参数。我的习惯是先设一个中等支持度跑通流程看看频繁项集数量是否合理再逐步降低支持度观察规则变化。如果频繁项集数量在几百以内说明支持度设得比较合适如果上万要么调高支持度要么检查数据里有没有重复事务。build_support_map是个辅助函数把频繁项集和它们的支持度存成映射供规则生成时查询。实现方式可以用containers.Map键用项集排序后拼接的字符串值是对应的支持度。这样查起来是 O(1)比每次重新扫描数据快得多。4.3 结果解读与规则筛选跑完之后你会拿到一个规则列表每条规则包含前件、后件、置信度和提升度。新手容易只看置信度觉得 0.9 以上就一定是好规则。但置信度高不代表有商业价值如果后件本身的支持度就有 0.85那前件带来的提升非常有限。提升度才是衡量“这个规则比随机猜好多少”的指标。我一般会按提升度降序排取前 20 条人工过一遍。如果前件和后件在业务上明显不相关那可能是数据里的伪相关比如两个商品恰好都在促销期销量高。这时候需要结合时间维度做分层分析或者把促销期数据单独拿出来看。注意规则数量太多时不要一次性全部导出。先按提升度过滤再按置信度排序最后人工审核。全量导出只会让你淹没在噪声里。5. 避坑与排查跑 FP-Growth 时最容易翻车的几个地方5.1 报错“Out of memory”或跑着跑着卡死现象是建树阶段内存飙升或者递归挖掘时 MATLAB 无响应。原因通常是支持度设得太低导致频繁 1 项集数量巨大树节点爆炸。另一个可能是数据里有超长事务单个事务包含几百个项建树时路径极深。解决办法分两步先把min_support调高到 0.05 或 0.1跑通后再逐步降低然后检查事务长度分布用cellfun(numel, transactions)看有没有异常长的记录。如果有考虑对长事务做截断或拆分或者换用支持度更高的采样数据先验证逻辑。5.2 规则为空或数量极少现象是频繁项集挖出来不少但规则生成后没几条。原因一般是min_conf设得太高或者支持度映射表没建对导致置信度计算时除零或取到错误值。先检查min_conf是不是设到了 0.9 以上降到 0.5 试试。然后确认support_map里每个项集的支持度是否正确特别是单项的支持度。如果单项支持度算错置信度分母就错了规则自然出不来。可以在generate_rules里加一句打印看看每条规则的supp_a和supp_ab是否合理。5.3 递归深度超限现象是 MATLAB 报Maximum recursion limit错误。原因是数据里存在大量共现项条件 FP 树递归层数太深。MATLAB 默认递归限制是 500长事务场景下很容易触顶。在脚本开头加set(0, RecursionLimit, 2000)能缓解但治本的方法是优化条件模式基的构建减少不必要的递归分支。如果数据本身就有很多长事务可以考虑先做一步事务合并把相同项集的事务计数合并减少树的分支数。5.4 结果每次跑都不一样现象是同样的数据和参数两次运行得到的规则数量或内容有差异。原因通常是项的顺序不稳定导致建树结构不同。虽然 FP-Growth 理论上对项的顺序不敏感但实现里如果用了哈希或未排序的集合顺序就会影响结果。解决办法是在scan_data里对频繁项做稳定排序支持度相同的项按名称或 ID 排序保证每次运行顺序一致。另外检查unique和intersect的返回值顺序MATLAB 的这些函数默认排序是稳定的但如果数据里有 NaN 或空值行为可能不一致。5.5 支持度计数偏大现象是某个项集的支持度明显高于预期甚至超过 1。原因通常是事务里有重复项或者建树时计数逻辑写成了累加而不是取最小值。先检查原始数据用unique对每个事务去重。然后检查build_fp_tree里的计数逻辑节点计数应该是“经过该节点的路径数”而不是“该节点被访问的次数”。在条件模式基收集时路径计数要取路径上所有节点计数的最小值不是求和。这两处搞混支持度就会偏大。6. 二次开发与改造把这套代码接进你自己的流程6.1 替换数据源与增量更新这套代码默认是一次性加载全量数据。如果你要接数据库或实时流最直接的办法是把load_data换成查询接口每次取一个时间窗口的事务。但 FP-Growth 本身不支持增量建树新数据来了要么全量重跑要么在现有树上做局部更新。局部更新实现复杂我一般建议按小时或按天全量重跑数据量不大的话几分钟就能出结果。如果一定要做增量可以考虑把 FP 树序列化到磁盘新数据来了先加载旧树再插入新事务。但要注意旧树的计数会过时需要配合时间衰减因子否则老数据会一直主导规则。这个改造工作量不小建议先用全量重跑验证业务价值再考虑增量。6.2 规则后处理与业务映射原始规则里的项是编码后的 ID直接给业务方看没人看得懂。你需要一个映射表把 ID 转回商品名或业务标签。这个映射表可以在预处理阶段就存下来规则生成后统一替换。function readable_rules map_rules(rules, id2name) readable_rules rules; for i 1:numel(rules) readable_rules{i}.antecedent ... cellfun((x) id2name(x), rules{i}.antecedent, ... UniformOutput, false); readable_rules{i}.consequent ... cellfun((x) id2name(x), rules{i}.consequent, ... UniformOutput, false); end endid2name可以是一个containers.Map或者简单的元胞数组索引就是 ID。cellfun把每条规则的前件和后件都转成可读名称。转换之后你可以直接把规则导出成 CSV 或 Excel交给业务方筛选。6.3 参数调优的实用技巧支持度和置信度的组合空间很大手动一个个试效率太低。我一般会写一个简单的网格搜索脚本把支持度从 0.01 到 0.1 按步长 0.01 遍历置信度从 0.5 到 0.9 按步长 0.1 遍历记录每组参数下的规则数量和平均提升度。然后画个热力图一眼就能看出哪个区域规则数量适中且提升度较高。support_range 0.01:0.01:0.1; conf_range 0.5:0.1:0.9; results zeros(numel(support_range), numel(conf_range)); for i 1:numel(support_range) for j 1:numel(conf_range) % 跑一遍完整流程记录规则数量 % ...省略重复代码 results(i, j) numel(rules); end end imagesc(results); colorbar; xlabel(置信度); ylabel(支持度);这个热力图能帮你快速定位参数区间。规则数量太少说明支持度太高太多说明支持度太低。平均提升度可以在另一个维度上评估最终选一个规则数量在几十到几百之间、提升度均值大于 1.2 的组合。6.4 性能优化的几个方向如果数据量到了百万级事务MATLAB 的原生实现可能会吃力。这时候有几个方向可以试一是把建树和挖掘用 MEX 写成 C 接口核心循环用 C 跑MATLAB 只做调度二是把数据预处理和规则后处理放到 Python 里做MATLAB 只负责算法核心三是用 MATLAB 的parfor并行化条件模式基的收集但要注意递归部分不好并行只能并行最外层的项遍历。我自己的习惯是先用小样本验证逻辑再逐步放大数据量观察瓶颈在哪。如果瓶颈在建树优化数据结构和内存布局如果瓶颈在递归挖掘考虑剪枝策略比如提前终止支持度太低的分支。不要一上来就追求极致性能先跑通、再跑快这个顺序不能反。从那以后我每次拿到一个新的关联规则项目都会先用 1000 条事务的小样本跑通全流程确认支持度、置信度和提升度的计算逻辑没问题再换全量数据。这个习惯帮我省下了大量调试时间也避免了一上来就被内存和递归深度问题卡住。希望这套 FP-Growth 的拆解能帮到你拿到压缩包后先跑demo.m看到第一条规则输出后面的改造就有方向了。本文还有配套的精品资源点击获取