简介:这份资源是面向数据挖掘初学者与MATLAB使用者的FP-Growth关联规则挖掘实现包,用于在交易数据中高效发现频繁项集并提取关联规则。包内共8个文件,以7个.m脚本和1个.mat数据集为主,脚本分别承担主流程调度、FP树构建、幂集与子集生成、规则展示及树结构可视化等职责,mydata数据集可直接用于演示算法运行,压缩包约5KB,轻量易读。资源完整呈现了从数据预处理、构造FP树、生成条件模式基到递归挖掘与规则提取的全过程,读者可借此理解支持度与置信度的计算逻辑,并在此基础上替换自有事务数据、调整最小支持度阈值,扩展到市场篮子分析、网络行为分析等场景。目前已有191人学习,适合希望动手复现经典算法、夯实关联规则挖掘基础的读者参考。
1. 从一份 MATLAB 关联规则挖掘包说起:它到底能干什么
如果你手头有一批事务型数据——比如超市购物篮、网站点击流、设备告警日志——想找出「买了 A 的人大概率也会买 B」这类规则,那这份 FP-Growth 关联规则挖掘的 MATLAB 实现包,就是拿来就能跑的工具。它把频繁项集挖掘和关联规则生成两件事都封装好了,输入一个事务矩阵或元胞数组,输出支持度、置信度、提升度齐全的规则表。适合谁?做数据挖掘课程设计的学生、需要快速验证关联规则效果的研究人员,以及不想从零手写 FP-Tree 的 MATLAB 工程师。相比 Apriori 反复扫库的笨办法,FP-Growth 只扫两遍数据,靠一棵压缩的前缀树完成挖掘,数据量稍大时差距非常明显。
2. 核心架构拆解:FP-Tree 构建与规则生成的两段式设计
2.1 整体流程与模块划分
这个包的结构不复杂,但分工很清楚。常见做法是拆成三个部分:数据预处理与格式转换、FP-Tree 构建与频繁项集挖掘、关联规则生成与指标计算。主入口脚本负责串联,用户只需要改数据加载那几行。
先看整体调用链。假设你拿到的是一个transactions元胞数组,每个元素是一条事务里的项集合:
% main.m 主流程示意 data = load('transactions.mat'); % 加载事务数据 minSup = 0.02; % 最小支持度阈值 minConf = 0.6; % 最小置信度阈值 % 第一步:扫描数据,统计项频次,过滤低频项 [itemFreq, filteredTrans] = preprocess(data.transactions, minSup); % 第二步:构建 FP-Tree fpTree = buildFPTree(filteredTrans, itemFreq); % 第三步:从 FP-Tree 挖掘频繁项集 freqItemsets = mineFreqItemsets(fpTree, minSup); % 第四步:由频繁项集生成关联规则 rules = generateRules(freqItemsets, minConf); % 第五步:输出规则表 disp(rules);这段代码把整个流水线串起来了。preprocess负责第一遍扫描,统计每个项出现的次数,把低于最小支持度的项直接剔除——这一步很关键,能大幅缩小后续树的规模。buildFPTree是核心,把过滤后的事务逐条插入前缀树,相同前缀共享路径,节点上挂计数。mineFreqItemsets用递归的方式从树的叶节点往上挖条件模式基,构造条件 FP-Tree。generateRules则对每个频繁项集拆分子集,算置信度和提升度。
参数说明:minSup是支持度阈值,取值越小挖出的项集越多,但计算量也越大,一般从 0.01 到 0.1 之间试;minConf是置信度门槛,0.6 以上算比较强的规则。这两个参数没有万能值,得根据数据分布调。
2.2 FP-Tree 节点结构与插入逻辑
FP-Tree 的节点定义决定了整个挖掘效率。常见做法是用一个结构体数组或 MATLAB 的containers.Map来存节点,每个节点记录项名、计数、父节点索引、子节点列表,以及一个指向同项下一节点的链表指针(header table 用)。
% buildFPTree.m 核心片段 function fpTree = buildFPTree(transactions, itemFreq) % 按频次降序排列每个事务中的项 sortedTrans = cellfun(@(t) sortByFreq(t, itemFreq), ... transactions, 'UniformOutput', false); root = struct('item', [], 'count', 0, 'parent', 0, ... 'children', {{}}, 'next', 0); fpTree.nodes = root; fpTree.header = containers.Map('KeyType','char','ValueType','any'); for i = 1:length(sortedTrans) trans = sortedTrans{i}; currentNode = 1; % 从根节点开始 for j = 1:length(trans) item = trans{j}; childIdx = findChild(fpTree, currentNode, item); if isempty(childIdx) % 新建节点 newNode = struct('item', item, 'count', 1, ... 'parent', currentNode, ... 'children', {{}}, 'next', 0); fpTree.nodes(end+1) = newNode; childIdx = length(fpTree.nodes); fpTree.nodes(currentNode).children{end+1} = childIdx; updateHeader(fpTree, item, childIdx); else % 已有节点,计数加一 fpTree.nodes(childIdx).count = ... fpTree.nodes(childIdx).count + 1; end currentNode = childIdx; end end end逻辑说明:sortByFreq把每条事务里的项按全局频次从高到低排,这样共享前缀的概率最大,树也最紧凑。插入时从根往下找,有对应子节点就计数加一,没有就新建。updateHeader维护 header table 的链表,方便后面按项快速找到所有节点。参数上,fpTree.nodes是节点数组,fpTree.header是项到节点链的映射。
这里有个容易翻车的点:MATLAB 里结构体数组动态增长很慢,数据量大时建议预分配节点数组,或者改用table类型。我一般会先估一个上限,用repmat预分配再裁剪。
2.3 条件模式基与递归挖掘
频繁项集挖掘是递归过程。从 header table 里频次最低的项开始,沿着链表往上找每个节点的路径,这些路径就是条件模式基。
% mineFreqItemsets.m 递归挖掘片段 function itemsets = mineFreqItemsets(fpTree, minSup) itemsets = {}; headerKeys = keys(fpTree.header); % 按项频次升序处理 [~, order] = sort(cellfun(@(k) getFreq(fpTree, k), headerKeys)); headerKeys = headerKeys(order); for i = 1:length(headerKeys) item = headerKeys{i}; % 收集条件模式基 paths = collectPaths(fpTree, item); % 构造条件 FP-Tree condTree = buildCondTree(paths, minSup); if ~isempty(condTree.nodes) && length(condTree.nodes) > 1 % 递归挖掘 subItemsets = mineFreqItemsets(condTree, minSup); % 合并当前项 for j = 1:length(subItemsets) itemsets{end+1} = [{item}, subItemsets{j}]; end end % 单项集本身 itemsets{end+1} = {item}; end endcollectPaths从 header table 拿到项的所有节点,沿父指针往上走到根,每条路径的计数取该节点的计数。buildCondTree用这些路径重新建一棵小树,再递归调用挖掘函数。递归终止条件是条件树只剩一条路径或为空。参数minSup在条件树构建时同样用来过滤低频项。
这段递归是 FP-Growth 的精髓,也是最容易出 bug 的地方。常见问题是路径收集时忘了把当前项本身排除,导致条件模式基里混入目标项,挖出的项集重复。排查时可以在collectPaths里打印路径长度和内容,对比预期。
3. 跑起来:数据准备、参数调优与结果解读
3.1 数据格式与加载
这个包对输入数据的要求不严,常见两种格式:元胞数组,每个元素是一条事务的项名列表;或者数值矩阵,每行是一条事务,列是项 ID,用 0 或 NaN 填充。如果你手上是 CSV,先转成元胞数组。
% loadData.m 从 CSV 加载并转换 function transactions = loadData(filename) raw = readcell(filename); % 读取为元胞 [m, n] = size(raw); transactions = cell(m, 1); for i = 1:m row = raw(i, :); % 去掉空值和 NaN valid = ~cellfun(@(x) isempty(x) || ... (isnumeric(x) && isnan(x)), row); transactions{i} = row(valid); end % 去掉空事务 transactions = transactions(~cellfun(@isempty, transactions)); end逻辑说明:readcell保留原始类型,避免数值项被转成字符串后对不上。valid过滤空单元格和 NaN。最后去掉空事务,否则建树时会插入空路径。参数上,filename是 CSV 路径,要求每行一条事务,项之间用逗号分隔。
提示:如果项名里有逗号或引号,
readcell可能解析错位,建议先用文本编辑器检查一遍,或者改用readtable指定分隔符。
3.2 参数调优的实操方法
minSup和minConf直接决定输出规则的数量和质量。我的习惯是先固定minConf = 0.5,把minSup从 0.1 往下调,观察频繁项集数量的变化曲线。当项集数量开始指数增长时,那个点就是合理下限。
% tuneParams.m 参数扫描 supRange = [0.1, 0.05, 0.02, 0.01, 0.005]; for s = supRange [~, filtered] = preprocess(transactions, s); tree = buildFPTree(filtered, getFreq(filtered)); itemsets = mineFreqItemsets(tree, s); fprintf('minSup=%.3f, 频繁项集数=%d\n', s, length(itemsets)); end跑一遍就能看到拐点。如果minSup降到 0.005 时项集数从几百跳到几万,说明再低就没意义了,噪声规则会淹没真正有用的模式。minConf一般不低于 0.5,低于这个值的规则可信度太差,业务上没法用。
3.3 规则表字段与解读
生成的规则表常见字段有:前件、后件、支持度、置信度、提升度。支持度是前后件同时出现的概率,置信度是前件出现时后件出现的条件概率,提升度大于 1 说明正相关,等于 1 说明独立,小于 1 说明负相关。
| 字段 | 含义 | 典型取值 |
|---|---|---|
| 前件 | 规则左侧项集 | {牛奶, 面包} |
| 后件 | 规则右侧项 | {黄油} |
| 支持度 | 前后件共现频率 | 0.03 |
| 置信度 | 前件→后件的条件概率 | 0.72 |
| 提升度 | 置信度与后件独立概率之比 | 1.8 |
解读时优先看提升度大于 1.5 且支持度不太低的规则。支持度太低(比如 0.001)的规则虽然置信度高,但可能只是小样本巧合,业务上不可靠。我一般会按提升度降序排,再人工筛一遍。
4. 避坑与排查:那些跑不通和跑不对的情况
4.1 内存溢出:树节点爆炸
现象:数据量稍大(几万条事务)时 MATLAB 报Out of memory,或者卡在建树阶段不动。原因:结构体数组动态增长导致内存碎片,加上 FP-Tree 节点数随数据量线性增长,没有预分配。解决:建树前预估节点上限,用repmat预分配节点数组,插入时用计数器索引而不是end+1。另外可以把项名映射成整数 ID,减少字符串存储开销。
4.2 规则为空:阈值设太高
现象:程序跑完没报错,但规则表是空的。原因:minSup或minConf设得过高,没有项集能同时满足。解决:先把minSup降到 0.01 以下试跑,确认有频繁项集输出,再逐步往上调。如果频繁项集有但规则为空,说明minConf太高,降到 0.3 看看。
4.3 项集重复:条件模式基没排除自身
现象:输出里出现{A} -> {A}这种自反规则,或者同一项集被挖出多次。原因:collectPaths收集路径时把目标项本身也包含进去了,导致条件树里混入目标项。解决:在收集路径时跳过目标项节点,只取它到根路径上除自身外的部分。排查时打印条件模式基,确认没有目标项。
4.4 结果对不上:项排序不一致
现象:同样的数据跑两次结果不同,或者和别的实现对比对不上。原因:事务内项排序不稳定,MATLAB 的sort对相同频次的项顺序可能不同,导致树结构有差异。解决:排序时用项名做次级排序键,保证频次相同时顺序确定。sortByFreq里加一个tiebreak参数,按字母序排。
4.5 递归过深:栈溢出
现象:数据里存在很长的频繁项集(比如几十项),递归挖掘时 MATLAB 报递归深度超限。原因:mineFreqItemsets递归层数等于最长频繁项集的长度。解决:把递归改成显式栈的迭代实现,或者调大 MATLAB 的递归限制(不推荐,治标不治本)。更稳妥的做法是在预处理阶段限制单条事务的最大长度。
5. 二次开发:从能跑到好用,几个改造方向
5.1 加一个规则过滤与排序模块
原始输出是一堆规则,直接看很乱。我一般会在generateRules后面加一层过滤,按提升度和支持度双阈值筛,再按业务字段排序。
% filterRules.m 规则后处理 function filtered = filterRules(rules, minLift, minSup) keep = false(length(rules), 1); for i = 1:length(rules) keep(i) = rules(i).lift >= minLift && ... rules(i).support >= minSup; end filtered = rules(keep); % 按提升度降序 [~, idx] = sort([filtered.lift], 'descend'); filtered = filtered(idx); endminLift建议设 1.2 以上,minSup根据数据量定。这样输出的规则表直接能用,不用再手动筛。
5.2 并行化挖掘
FP-Growth 的递归挖掘天然适合并行——每个项的条件树挖掘互不依赖。MATLAB 的parfor可以派上用场,把 header table 里的项分组,每组一个 worker 独立挖。
% parallelMine.m 并行挖掘片段 headerKeys = keys(fpTree.header); results = cell(length(headerKeys), 1); parfor i = 1:length(headerKeys) item = headerKeys{i}; paths = collectPaths(fpTree, item); condTree = buildCondTree(paths, minSup); if ~isempty(condTree.nodes) && length(condTree.nodes) > 1 subItemsets = mineFreqItemsets(condTree, minSup); results{i} = cellfun(@(s) [{item}, s], subItemsets, ... 'UniformOutput', false); else results{i} = {{item}}; end end itemsets = [results{:}];注意parfor里不能直接改共享变量,每个 worker 的结果先存到独立 cell,最后合并。并行加速比取决于项数和 CPU 核数,项多时效果明显。
5.3 验证改造是否正确的土办法
改完代码最怕引入隐性 bug。我的习惯是留一个小规模基准数据集(比如 20 条事务、10 个项),手动算出所有频繁项集和规则,存成expected.mat。每次改完跑一遍对比函数:
% verify.m 结果对比 function ok = verify(newItemsets, expectedFile) load(expectedFile, 'expectedItemsets'); ok = isequal(sortCell(newItemsets), sortCell(expectedItemsets)); if ~ok disp('不一致,检查条件模式基和排序逻辑'); end endsortCell把每个项集内部排序后再整体排序,消除顺序影响。这个土办法帮我抓过好几次排序不一致和路径收集的 bug。
从那以后我每次改挖掘逻辑,都强制先跑一遍这个小基准,确认输出和预期完全一致,再上大数据集。希望帮到你。
本文还有配套的精品资源,点击获取