数据挖掘模式发现实战:频繁项集与关联规则算法代码全解析
简介:这是Coursera公开课“数据挖掘中的模式发现”的配套代码资源,面向正在学习数据挖掘基础、希望结合Python与R动手实践的初学者。包内共4个文件,以2个Python脚本、1个R脚本和1个Markdown说明文档为主,整体压缩包仅2KB,体量虽小但聚焦课程核心练习,适合用于对照理解聚类、分类、关联规则等常见模式的编程实现。目前已有106人学习下载,适合配合课程进度逐题演练,或作为期末复习时快速回顾代码思路的参考资料。借助这些脚本,读者可以了解数据导入、特征处理、模型训练与结果输出等基础流程,并通过Markdown中的文字说明快速定位每个文件的作用,减少摸索时间,提升学习效率。
1. 课程代码的整体设计思路
拿下Coursera这门“数据挖掘中的模式发现”并不是一件轻松的事,尤其是代码部分。很多人以为数据挖掘课程的核心是理论,刷完PPT就能过关,但实际上这门课的代码作业才是真正拉开差距的地方。我最初也抱着“理论先看懂、代码随便写写”的心态,结果第一次作业就被频繁模式挖掘的实现细节卡住了,后来老老实实把代码重写了一遍,才把整个课程的脉络理顺。
这门课的核心内容其实非常聚焦:频繁模式挖掘、关联规则、序列模式发现。说白了,就是从海量事务数据里找“哪些东西经常一起出现”“哪些行为有先后关联”。代码作业主要围绕这几块展开,数据通常是市场购物篮数据或者用户行为日志数据,格式多为CSV或文本型事务列表。
我做的第一件事不是急着写算法,而是把课程作业全部过了一遍,把每个作业对应的知识点列成一张清单:
| 作业模块 | 核心知识点 | 数据格式 | 代码难点 |
|---|---|---|---|
| 频繁项集挖掘 | Apriori、FP-Growth | 事务型CSV | 候选项集生成、剪枝 |
| 关联规则生成 | Support、Confidence、Lift | 频繁项集输出 | 规则组合、阈值筛选 |
| 序列模式挖掘 | GSP、PrefixSpan | 序列数据库 | 前缀投影、递归构造 |
| 模式评估 | 兴趣度度量、冗余模式剔除 | 规则输出结果 | 指标计算、可视化 |
这个表格建议先收藏,因为整个课程代码的路线图基本就在这里了。搞清楚每个模块“要交什么”,再动手写,比盲写效果好得多。
2. 环境准备与数据预处理细节
2.1 开发环境与库的选型
代码部分我推荐直接用Python + Jupyter Notebook,原因很简单:这门课的代码量不大,但需要反复调试和查看中间结果,Notebook的交互式体验比写脚本高效太多。依赖库方面,pandas负责数据处理,numpy处理数组计算,matplotlib和seaborn用于模式可视化,mlxtend这个库强烈建议提前安装好,因为它内置了Apriori和关联规则的高效实现,可以用来对照自己的代码结果。
pip install pandas numpy matplotlib seaborn mlxtend这里要特别说明一下mlxtend的定位:它不是让你直接抄作业用的,而是当“参考答案”。自己手写的Apriori和库里的结果对比,如果支持度排序后的频繁项集一致,说明你的实现没问题;如果对不上,就能快速定位是候选项集生成的问题还是剪枝逻辑的问题。这个对照调试的方法我后面会展开讲。
2.2 事务数据的清洗策略
课程提供的数据集看起来干净,实际上坑不少。最容易踩的一个坑是:CSV文件里的事务ID列并不是唯一的,同一笔交易可能包含重复的商品记录。如果不做去重直接跑算法,生成的频繁项集置信度会虚高。我处理的第一步是把同一事务ID下的商品去重,再统一转为小写、去掉首尾空格,避免“Milk”和“milk”被当成两个不同的项。
import pandas as pd df = pd.read_csv('transactions.csv') df.columns = ['transaction_id', 'item'] df['item'] = df['item'].str.strip().str.lower() df = df.drop_duplicates(subset=['transaction_id', 'item']) transactions = df.groupby('transaction_id')['item'].apply(list).tolist() print(f'总事务数: {len(transactions)}')第二个需要处理的坑是稀有项。频繁模式挖掘的核心是“频繁”二字,那些只出现一两次的物品,对全局模式贡献极小,却会让候选集生成阶段的计算量明显增加。我的做法是先统计所有商品的频次,剔除出现次数小于总事务数0.5%的商品,再重新构造事务列表。
3. Apriori算法的代码实现与调优全记录
3.1 从候选生成到剪枝的完整逻辑
Apriori算法的核心思想只有一句话:一个项集是频繁的,那它的所有子集也必须是频繁的。反过来推理,如果一个项集的某个子集不频繁,那这个项集根本不需要生成。这个性质被称为Apriori性质,也是整个算法剪枝策略的根基。
代码实现上,我把整个流程拆成了三个函数。
第一步,生成初始候选集。所有单个商品就是第一轮候选集,对事务数据扫描一遍,统计每个商品的频次,筛选出支持度达标的项作为频繁1项集。
from itertools import combinations def generate_candidates(itemsets, k): """基于频繁(k-1)项集生成候选k项集""" candidates = set() itemsets = sorted(itemsets) for i in range(len(itemsets)): for j in range(i + 1, len(itemsets)): # 仅当前k-1项相同才合并 if itemsets[i][:-1] == itemsets[j][:-1]: candidate = tuple(sorted(set(itemsets[i]) | set(itemsets[j]))) if len(candidate) == k: candidates.add(candidate) return candidates第二步是剪枝。生成的候选k项集里,有不少其实不可能成为频繁项集,因为它的某些k-1项子集根本不频繁。剪枝阶段就是把这样的候选项提前删掉,减少后续扫描事务数据的开销。
def prune_candidates(candidates, frequent_subsets): """剔除子集不频繁的候选项集""" pruned = set() for candidate in candidates: all_subsets = set(combinations(candidate, len(candidate) - 1)) if all_subsets.issubset(frequent_subsets): pruned.add(candidate) return pruned这里有个非常容易出错的地方:combinations返回的是一个可迭代对象,直接取子集判断时会消耗掉,一定要先转成set再判断关系。我之前在这里卡了整整一晚上,频繁项集数量总是对不上,最后打印中间结果才发现迭代器被用完以后,后续判断永远返回False。
第三步,扫描事务数据统计支持度。这一步可以用嵌套循环暴力做,但对于大规模数据效率很低。我实测下来,把每笔事务转成set,再用issubset判断候选集是否出现在事务中,比逐项遍历快一个数量级。
def support_count(transactions, candidates): """统计候选项集的支持度计数""" counts = {cand: 0 for cand in candidates} transaction_sets = [set(t) for t in transactions] for cand in candidates: cand_set = set(cand) for tset in transaction_sets: if cand_set.issubset(tset): counts[cand] += 1 return counts3.2 支持度阈值的选择与影响
这个课程作业里最关键的参数就是最小支持度阈值。选大了,频繁项集少得可怜,关联规则也挖不出几个;选小了,候选集爆炸式增长,运行时间陡增。我在购物篮数据集上做了几轮实验,数据规模约5000笔事务、120个商品。当最小支持度设为0.05时,频繁1项集只有7个,跑出的规则一眼看尽;降到0.02后,频繁项集数量跳到30多个,规则也丰富起来,但运行时间多了近3倍。
所以阈值的选择不是越小越好,要看具体业务场景。课程作业里如果只是要求“挖掘出有意义的模式”,建议先用0.05跑一轮看结果,如果觉得规则太少,再逐步降到0.03或者0.02。每轮调整时把频繁项集数量、最大项集长度记录下来,观察变化趋势,这样提交作业时还能在报告里写一段参数敏感性分析,这反而是加分项。
3.3 性能调优实测
在实现的一版串行Apriori跑5000笔事务时,耗时还能接受,但换成课程提供的2万笔数据集后,跑3项集就开始明显变慢了。我从几个方向做了优化:
第一个优化是把事务数据向量化。提前把所有商品编码成整数,事务里存整数ID而不是字符串,这样比较时直接比int比比str快很多,内存占用也小。第二个优化是提前终止迭代:如果当前频繁k项集不存在任何可以合并成k+1项集的组合,就直接跳出循环,不再生成下一轮候选集。第三个优化是支持度计数提前退出——统计某个候选集时,当计数值已经达到最小支持度计数,就不需要继续扫描剩下的事务了。
这几个优化加起来,在我的测试集上运行时间从38秒降到了7秒左右。建议你也按这个思路去调节自己的代码,尤其当你想用更大规模的数据验证算法效果时,性能优化是绕不开的环节。
4. FP-Growth与Apriori的对比实测
4.1 FP-Growth的实现思路
Apriori有个天然的短板:每生成一轮候选集,就要完整扫描一遍事务数据库。即使有了剪枝优化,在大数据集上仍然昂贵。FP-Growth换了个思路——把事务数据压缩成一棵FP树,在内存里保留项集之间的关联结构,挖掘时直接在树上操作,不需要反复扫描原始数据。
我实现的FP-Growth分为两步。第一步,扫描事务数据,统计频次,过滤掉不满足最小支持度的商品,剩下的按频次降序排列。第二步,逐条插入事务,构建FP树。每个节点保存商品ID和计数,同时用一张头表记录每个商品在树中的所有位置。
class TreeNode: def __init__(self, item_name, count, parent): self.item = item_name self.count = count self.parent = parent self.children = {} self.node_link = None # 指向下一个相同商品的节点 def increment(self, count): self.count += count挖掘频繁项集时,FP-Growth用的是递归的方式:对头表里的每个商品,先找到它所有的路径,构造条件模式基,基于条件模式基递归构建条件FP树。这个递归过程最容易出错的地方是条件FP树构造时的频繁项过滤,条件模式基里的商品频次需要重新统计,不能直接沿用全局的频次。
4.2 算法选择建议
我在同一个数据集上对比了手写的Apriori和FP-Growth,结果很有意思:稀疏数据集上两者性能差距不大,但事务数据越稠密、频繁模式越长,FP-Growth的优势越明显。如果你用的是市场购物篮这种典型稀疏数据,Apriori已经够用,代码也更直观、容易向别人解释;但如果是生物信息、文本共现这类稠密数据,建议直接用FP-Growth。
课程作业其实不强制要求两种算法都实现。但我强烈建议至少把Apriori完整写一遍,因为它的逻辑更贴合“模式发现”这个主题的理论脉络,面试或者项目答辩时也更容易讲清楚。FP-Growth可以在掌握Apriori之后,作为进阶练习来补充实现。
5. 关联规则生成与评估指标详解
5.1 规则生成的核心逻辑
频繁项集挖掘只是前半段工作,课程作业的后半段是从频繁项集里生成关联规则。一条关联规则的形态是X -> Y,其中X和Y都是同一频繁项集的非空真子集。生成所有可能的组合很简单,但真正难的是筛选出有趣、有用的规则。
我写的规则生成代码是这样的:对任意一个频繁项集,先用combinations生成所有非空子集作为前件X,后件Y就是项集减去X的部分。然后计算每条规则的置信度——置信度等于项集的支持度除以前件X的支持度。最后用最小置信度阈值过滤。
def generate_rules(frequent_itemsets, min_confidence=0.6): rules = [] for itemset in frequent_itemsets: if len(itemset) < 2: continue itemset = sorted(itemset) for i in range(1, len(itemset)): for antecedent in combinations(itemset, i): antecedent = set(antecedent) consequent = set(itemset) - antecedent if not consequent: continue conf = support[tuple(itemset)] / support[tuple(sorted(antecedent))] if conf >= min_confidence: rules.append((frozenset(antecedent), frozenset(consequent), conf)) return rules这里有个隐藏的坑:当频繁项集数量很多时,所有子集组合的数量会非常大,生成规则的过程也会很慢。我的建议是,先用置信度快速过滤掉明显不达标的组合,再计算Lift等其他指标,而不是先把所有指标算完再过滤。代码执行顺序看似微小,实际对运行时间的影响很大。
5.2 支持度、置信度、提升度的组合解读
很多人以为关联规则只要置信度高就是好规则,实际上这是一个非常常见的误区。置信度只衡量了“前件发生时后件也发生的概率”,但如果后件本身出现的概率就很高,哪怕前件对它没有任何实质影响,置信度也可能虚高。
举个例子,假设所有事务中90%都包含“咖啡”,“面包”出现在60%的事务中。即使“牛奶 -> 咖啡”的置信度达到85%,也没有任何惊喜——因为不看牛奶,咖啡本来就有90%的概率出现。这时候必须看提升度(Lift),它可以理解为“前件对后件出现概率的放大倍数”。Lift等于1表示两个事件独立,大于1才是正相关。上面这个例子里,“牛奶 -> 咖啡”的Lift约是0.94,其实是负相关的,完全不能说明“买牛奶的人更可能买咖啡”。
课程作业的评估部分,我建议把支持度、置信度和提升度三个指标全部输出,并且明确指出:支持度筛选的是“统计上不稀罕”的模式,置信度衡量的是规则的可信程度,提升度衡量的才是规则的“惊喜度”。报告里写出这三个指标的组合分析,比单纯列出几十条规则要深刻得多。
5.3 用可视化直观呈现模式
数据挖掘的代码作业,除了算法和规则,还应该配上可视化图表,这在评判标准中通常属于加分部分。我最常画的是三种图:
第一是频繁项集支持度柱状图,直观展示哪些项集出现最频繁。第二是支持度-置信度散点图,每个点代表一条规则,横轴是支持度,纵轴是置信度,颜色深浅代表提升度大小。这种散点图能一眼看出哪些规则“既常见又可靠”。第三是规则网络图,用节点表示商品,连线表示规则,线宽对应置信度,可以很清晰地看出商品之间的关联强度。
import matplotlib.pyplot as plt plt.figure(figsize=(10, 6)) scatter = plt.scatter(rules['support'], rules['confidence'], c=rules['lift'], cmap='viridis', s=50) plt.colorbar(scatter, label='Lift') plt.xlabel('Support') plt.ylabel('Confidence') plt.title('Association Rules: Support vs Confidence') plt.show()绘制网络图建议用networkx库,节点布局选spring_layout,视觉效果相对均衡。如果规则数量太多,可以先按提升度降序取前20条再画,否则图会乱成一团根本没法看。
6. 序列模式挖掘的进阶实现
6.1 序列数据处理的特殊性
课程后半段会进入序列模式挖掘。这个概念跟前面关联规则最大的区别在于:关联规则只看“同时出现”,序列模式还看“先后顺序”。比如“买手机 -> 买手机壳”是一条有价值的序列模式,但光靠Apriori是发现不了这种顺序关系的。
序列数据的格式通常是这样的:一个序列包含多个有序事件,每个事件里可以包含多个元素。比如用户访问日志里,一个用户的浏览记录是一个序列,每个页面浏览是一个事件。处理这种数据时,首先要把原始日志按用户和时间戳排序,生成序列格式,再决定用GSP还是PrefixSpan算法。
6.2 PrefixSpan的代码实践
PrefixSpan是我在这门课里觉得最难啃的部分,核心思想可以概括为一句话:递归地构造前缀,投影后缀序列数据库,然后在投影数据库中挖掘频繁项。它全程不需要生成候选序列,这也是比GSP高效的原因。
实现时最关键的数据结构是“后缀序列”的表示。我用一个二维列表存储投影数据库,每个元素是一条后缀序列,同时还要记录每条后缀序列剩余部分的长度。递归的终止条件是投影数据库为空,或者新发现的前缀模式支持度低于阈值。
这个模块在课程代码里属于挑战题,如果只是要求及格,不实现也不影响;但如果想做优秀项目,建议认真写一遍。我当时的做法是在网上找了经典的PrefixSpan测试用例,先跑通小数据,再对照课程数据集验证,确保代码没有边界错误。
7. 常见问题排查与避坑指南
7.1 代码运行报错高频问题
数据挖掘课程作业的代码报错,看起来千奇百怪,实际翻来覆去就那几类。我把自己的排查经验整理成一张速查表,基本能覆盖九成以上问题:
| 报错场景 | 根本原因 | 解决方案 |
|---|---|---|
TypeError: unhashable type: 'list' | 把list当作set元素或dict键 | 转成tuple或frozenset再使用 |
| 频繁项集循环内重复 | combinations迭代器被重复消费 | set(combinations(...))先固化 |
| 支持度计算结果为0 | 项集里混入空格/大小写不一致 | 数据清洗阶段统一strip和lower |
| 内存溢出 | 最小支持度太低导致候选集爆炸 | 适当提高阈值,增加剪枝逻辑 |
| 递归深度超限 | PrefixSpan递归层数太多 | 检查终止条件,或改迭代实现 |
7.2 算法结果对不上的排查思路
如果你手写的Apriori结果跟mlxtend库不一致,先别急着怀疑自己的代码有问题。我用过的排查思路是:先固定同一份数据、同一组参数(最小支持度一致),然后从上到下逐步对比中间结果。第一步看频繁1项集是否一致,如果这一步就出问题,说明数据清洗环节有问题;第二步看频繁2项集的候选集生成逻辑,重点检查项集的排序和去重是否可靠;最后检查支持度计数时有没有把事务数据参数传错,比如不小心传入的是商品列表而不是事务列表。
我在对比时发现过一个特别隐蔽的问题:mlxtend的transaction_df格式要求事务数据是one-hot编码的DataFrame,而我把原始事务列表直接传了进去,结果频繁项集数量比别人多了一大截。仔细看文档才发现,它对输入格式有严格要求。
7.3 课程作业提交时的加分细节
代码能跑出结果只是基本盘,想让作业拿到高分,有几种细节值得花时间。第一时间复杂度分析,在代码注释里写明每部分的时间复杂度,并且解释剪枝为什么能减少候选集数量,这能直接体现你对算法的理解深度。第二是参数实验部分,找一个有趣的问题展开,比如不同最小支持度阈值对规则数量的影响,配上折线图,这个展示效果比单纯贴代码好太多。第三是业务解读,不要只写“发现了规则A->B”,要结合课程提供的数据背景,解释这条规则在实际场景中可能有什么价值。
我见过不少同学代码全对,但报告只是把结果罗列了一遍,分数反而不如代码实现稍弱但报告分析透彻的人。数据挖掘课程考察的核心能力,不只是写代码,而是“从数据中发现模式并解释它”的完整链路。
8. 写在最后的实操心得
回头来看这门课,代码作业是最耗时间但也最值得的部分。我最大的感受是:千万别把课程代码当成“跑通交差”的任务,把它当成一次完整的数据挖掘项目来做。数据清洗、算法实现、结果评估、可视化展示,每一个环节都值得打磨,这些技能在真实项目中全是直接复用的。
按照我自己的经验,给准备选这门课或者正在做作业的同学三个建议:第一,Apriori一定要手写一遍,哪怕跑出来的结果比别人慢,这个过程对理解频繁模式挖掘的底层逻辑非常有帮助。第二,遇到结果对不上的情况,先从数据结构检查起,大部分问题都出在数据格式不对,而不是算法逻辑错误。第三,报告里多做参数实验,多截图核心图表,用可视化证明你的代码不是“只在测试集上能跑”。
最后再分享一个小技巧:课程讨论区里经常有人贴出各种测试数据模板,把这些模板全部下载保存下来,作为自己代码的回归测试集。每次改完代码,先用这些模板跑一遍,确保没把之前的功能改坏,再继续往下写。这种方法能帮你省下大量复查时间,亲测有效。
本文还有配套的精品资源,点击获取
