简介:面向数据仓库与数据挖掘课程设计/期末大作业场景的 Python 频繁模式挖掘完整项目,覆盖 Apriori 算法实现、多数据集应用与实验报告,适合需要提交可运行代码和说明文档的本科/高职学生。代码注释详细,新手也能跟着注释读懂事务数据预处理、频繁项集生成和关联规则提取的核心流程;项目在 Gutenberg、DBLP 等真实数据集上从单一商品、组合篮子、主题分组等不同粒度展开挖掘,并给出 task1/task2/task3 三个任务脚本,直接运行即可复现对应实验结果。压缩包共 41 个文件、约 5.84MB,以 8 个 Python 源码、24 个 txt 数据/结果、Markdown/PDF 文档、结果图片和 README 为主体,目录按代码、数据、文档、输出结果拆分,方便定位。已有 817 人学习/下载。除源码和数据集外,还提供完整报告 PDF、说明文档和运行指引,适合作为期末大作业、课程设计的高分参考,也能帮助快速理解关联规则挖掘与数据预处理思路。
1. 频繁模式挖掘大作业为什么难在“跑通容易、做出结论难”
频繁模式挖掘是大作业里最容易被低估的题目。很多人以为装好pandas、调通Apriori、控制台里刷出几十条关联规则就算交差,结果答辩时被问一句“这些规则对业务意味着什么”就接不上话。这篇笔记按一条完整的技术链路展开:从数据仓库取数构造购物篮事务集,到Apriori与FP-Growth的选型和Python实现,再到支持度、置信度、提升度三个参数的拉锯,最后落到结果验证和报告怎么写。适合作业要求提交源代码、文档说明和报告PDF、又不想照抄网上一段即跑代码的人。新手照着步骤能在一周内跑通,熟手可以直接跳到参数边界和避坑盘点当检查清单。
2. 数据仓库取数到事务表:频繁模式挖掘的输入准备
频繁模式挖掘的输入不是一张宽表,而是一个个“购物篮”。这一步很多人图省事,直接把订单明细丢给算法,得到的结果要么重复计算要么语义混乱。下面从数据仓库的角度把输入整理干净。
2.1 大作业的数据从哪来:星型模型中的事实表与维度表
常见的数据仓库课程作业会要求你自己建星型模型:事实表记录每次销售行为,维度表描述商品、时间、门店等属性。做频繁模式挖掘时真正有用的是订单事实表和商品维度表,两者通过商品代理键关联。
典型的取数SQL如下:
SELECT o.order_id, p.product_name FROM fact_sales o JOIN dim_product p ON o.product_sk = p.product_sk WHERE o.order_date BETWEEN '2023-01-01' AND '2023-12-31' ORDER BY o.order_id;这段SQL把事实表和维度表join起来,得到“订单-商品”明细。order_id会被用作挖掘时的篮子编号,product_name用作项。两个字段的选择都有讲究:order_id必须是单据号而不是用户号,否则你挖出来的是“某个用户的长期购买集合”,而不是一次购买行为;product_name最好选取最细的商品粒度,但如果商品编码过细导致每个商品的绝对频次都很低,可以上卷到品类粒度,这是频繁模式挖掘里常见的做法。
取数阶段还要注意同一订单内同一商品是否重复购买。如果业务上允许一个订单买两箱牛奶,明细表里就会有两行,直接转事务集会重复计数。一般做法是先对(order_id, product_name)去重,除非你要挖掘的是“数量”而不只是“购买组合”。这个决定要写进文档说明,否则老师看代码时会产生疑问。
2.2 把订单明细转成事务集:一行一个购物篮
拿到明细后,需要把数据整理成算法能吃的格式。频繁模式挖掘的标准输入是“列表的列表”:外层列表表示事务,内层列表表示该事务中的项集合。
import pandas as pd # 读入第2.1节导出的订单明细 df = pd.read_csv("order_detail.csv", encoding="utf-8-sig") # 同一订单内同一商品去重,避免重复计数 df = df.drop_duplicates(subset=["order_id", "product_name"]) # 按订单分组,把商品列聚合成一个列表 transactions = ( df.groupby("order_id")["product_name"] .apply(list) .tolist() ) print(f"事务总数: {len(transactions)}") print(f"事务示例: {transactions[0]}")groupby之后调用apply(list),每一行就变成了一个购物篮。drop_duplicates这一步不是可选项,它直接决定支持度的计算口径:一条明细占一行,和一张订单只贡献一次,两种口径跑出来的频繁项集完全不同。打印前几个事务检查一下,确认没有重复项混进去。
如果后面要用mlxtend库做关联规则,还需要把“列表的列表”转换成one-hot编码的DataFrame:
from mlxtend.preprocessing import TransactionEncoder te = TransactionEncoder() te_ary = te.fit(transactions).transform(transactions) df_encoded = pd.DataFrame(te_ary, columns=te.columns_)TransactionEncoder会扫描全部事务,把出现的所有商品做成列,每个事务对应一行True/False。要注意的是,这一步会展开成稀疏矩阵,商品种类上万时内存占用会明显上升。我一般先把出现次数低于阈值的商品过滤掉再编码,具体阈值在下节说。
2.3 数据质量检查:稀疏事务与长尾商品的影响
转成事务集后不要急着跑算法,先用几个指标判断这批数据适不适合做频繁模式挖掘:
items_per_order = df.groupby("order_id")["product_name"].count() print(items_per_order.describe()) single_item_ratio = (items_per_order == 1).mean() print(f"单商品订单占比: {single_item_ratio:.2%}")如果单商品订单占比超过一半,关联规则会非常贫瘠,因为两个商品同时出现的样本太少。应对办法有两种:一是在报告中明确画出这个分布,作为数据预处理阶段的结论;二是过滤掉商品种类特别少的订单,或者把时间窗口拉长,把一个客户一周内的订单合并成一个“篮子”。我一般优先选后者,因为合并窗口在业务上更说得通。
长尾商品同样会拖后腿。出现次数只有两三次的商品,即使偶尔与爆款同现,统计上也不可信。常见做法是按支持度阈值反推过滤:
# 以0.5%作为最低出现订单数阈值 min_count = int(0.005 * len(transactions)) valid_items = set( df.groupby("product_name")["order_id"].nunique() .loc[lambda s: s >= min_count] .index ) df_filtered = df[df["product_name"].isin(valid_items)]这段代码把出现订单数少于阈值0.5%的商品剔除。nunique统计的是“有多少个不同订单包含该商品”,这比count更接近频繁模式里的支持度定义,因为一个订单里重复买同一种商品时,count会虚高。过滤之后重新生成transactions,你会发现频繁项集的信噪比明显变好,而且跑起来快很多。
3. Apriori与FP-Growth选型:同一个购物篮,两种跑法
完成事务集构造后,下一个问题是用哪个算法挖掘频繁项集。大作业里最常见的选择是Apriori,但FP-Growth在数据量大时更实际。这一章从原理讲清楚两者的差别,再给出选型依据。
3.1 Apriori的思路:候选集逐层生成与剪枝原理
Apriori的核心是那条“频繁项集的非空子集也一定频繁”的先验性质。基于它,算法按长度逐层推进:先统计所有单项的支持度,筛掉不达标的;再基于频繁单项生成候选二项集,扫描数据统计支持度;之后重复,直到没有新的频繁项集产生。
这个流程的代价很清楚:每生成一层候选集,就要完整扫描一次事务数据。支持度阈值越低,保留的候选项越多,扫描次数和内存占用都跟着涨。Apriori在小规模数据集上很容易讲清楚,也方便画流程图画进报告,所以大作业里大量出现它。但如果你把同样的参数搬到几十万订单的数据上,跑一轮可能要等十几分钟,而且越到高层候选集越密。
在python数据分析与数据挖掘实战这门课里,Apriori通常是必选演示对象,因为它把“先生成候选、再用支持度剪枝”这个思想暴露得很彻底。手写一遍Apriori,你对项集、子集、支持度这些概念的印象会深很多。
3.2 FP-Growth的条件模式基:一次扫描建树,递归挖掘
FP-Growth换了一种思路:先统计每个项的全局支持度并排序,然后按项在事务中出现的顺序构建一棵FP树。挖掘阶段不再反复扫描原始事务,而是从树里递归提取条件模式基,在条件模式基上继续构造条件FP树。
下面是一个FP树节点的最小实现骨架,理解这个结构有助于调试:
class FPNode: def __init__(self, item, count): self.item = item # 节点代表的项 self.count = count # 经过该节点的路径计数 self.parent = None # 树形结构的前驱节点 self.children = {} # 子节点字典, key为项名 self.link = None # 指向下一个同名节点, 用于构建头表链FP树的构建顺序有讲究:每个事务里的项要先按全局支持度从高到低排序,再逐项插入树。这样做的原因是高支持度项更容易形成共享前缀,树才不至于膨胀成一张网。实现时最容易出错的是link指针,它负责把散落在不同分支里的同一项串起来,挖掘条件模式基时要从这里跳转。如果你自己实现了FP-Growth,建议先用一个小数据集(比如4个事务)逐步打印树结构验证,再上真实数据。这种“先小后大”的做法能省掉大量排错时间。
3.3 实测对比:什么数据量该选哪个算法
选型其实可以按数据规模粗略分:
| 数据规模 | 推荐算法 | 理由 |
|---|---|---|
| 千级事务 | Apriori | 实现直观,报告好讲,运行时间可接受 |
| 万级事务 | Apriori或FP-Growth | 取决于最小支持度和候选集密度 |
| 十万级以上 | FP-Growth | 避免反复全表扫描,用内存换时间 |
我一般会先估算去重后的商品种类数。商品种类少于2000、事务量在5万以内,Apriori完全能应付;一旦事务量上去但支持度阈值又设得很低,FP-Growth基本是唯一出路,因为Apriori每层扫描都是全表,层数一多就卡在IO上。
写对比实验时,可以用一段计时脚本来支撑选型结论:
import time from efficient_apriori import apriori start = time.time() itemsets, rules = apriori(transactions, min_support=0.02, min_confidence=0.5) print(f"Apriori耗时: {time.time() - start:.2f}s")如果课程允许用第三方库,这是最快拿到结果的方式;如果要求手写源代码,那efficient_apriori当benchmark用,用来验证你手写版本的结果是否一致。两种结果对不上时,先怀疑自己实现里的支持度计算口径:是除以事务总数,还是除以包含该候选项的订单数。这个细节差一点,数字就会漂移。
4. Python实现频繁模式挖掘的核心代码:支持度与置信度的设定
这一章给一套可以直接改来提交的手写Apriori,并说明参数怎么设。大作业评判通常看重三样东西:代码能不能跑、结果有没有业务解读、报告有没有分析过程。代码部分拆成三个函数,对应报告里“候选集生成、剪枝、规则提取”三个小节,每段都可以单独画流程图。
4.1 自己实现还是调库:大作业的评分逻辑
如果你的课程允许import第三方库,mlxtend和efficient_apriori是最省事的选择;但如果要求提交“源代码”,手写Apriori更能体现工作量。我见过不少同学直接调mlxtend后被追问原理答不上来,反而影响评分。稳妥的做法是:主代码手写Apriori,用mlxtend的结果做交叉验证,把验证过程写进文档说明。这样代码和报告都能站得住。
4.2 Apriori核心代码:从候选集生成到规则输出
def generate_candidates(prev_itemsets, k): """由k-1项频繁集生成k项候选集: 两两合并, 长度必须是k""" candidates = set() prev_list = list(prev_itemsets) for i in range(len(prev_list)): for j in range(i + 1, len(prev_list)): union = prev_list[i] | prev_list[j] if len(union) == k: candidates.add(union) return candidates def prune(candidates, prev_itemsets): """剪枝: 候选集的任意k-1子集必须在上层频繁集里""" return { c for c in candidates if all((c - {item}) in prev_itemsets for item in c) }两个函数配合起来完成Apriori里最经典的一步:合并生成候选,再用频繁集子集性质剪掉注定不频繁的候选,减少下一步扫描数据的开销。合并时set的并集操作天然去重,不用额外判断两个项集是否已经合并过。
主循环负责逐层挖掘:
def apriori(transactions, min_support): n = len(transactions) # 统计单项支持度, 构建第一层频繁项集 item_count = {} for t in transactions: for item in set(t): item_count[item] = item_count.get(item, 0) + 1 L1 = { frozenset([item]): cnt / n for item, cnt in item_count.items() if cnt / n >= min_support } all_itemsets = dict(L1) Lk = L1 k = 2 while Lk: candidates = generate_candidates(set(Lk.keys()), k) candidates = prune(candidates, set(Lk.keys())) support_count = {} for t in transactions: trans_set = set(t) for cand in candidates: if cand.issubset(trans_set): support_count[cand] = support_count.get(cand, 0) + 1 Lk = { cand: cnt / n for cand, cnt in support_count.items() if cnt / n >= min_support } all_itemsets.update(Lk) k += 1 return all_itemsets这段代码在干三件事:统计单项目支持度并筛出L1;通过generate_candidates和prune生成每层候选;扫描全部事务计算候选项集真实支持度,过滤后进入下一层。循环终止条件是某个k层候选全部被剪掉,即Lk为空。注意每一次扫描都是全量遍历transactions,这也是Apriori的性能瓶颈,报告中应该主动写出来。
规则提取在频繁项集基础上进行:
def extract_rules(all_itemsets, min_confidence): rules = [] for itemset, support in all_itemsets.items(): if len(itemset) < 2: continue for item in itemset: antecedent = itemset - {item} antecedent_support = all_itemsets.get(antecedent, 0) if antecedent_support == 0: continue confidence = support / antecedent_support if confidence >= min_confidence: lift = support / ( antecedent_support * all_itemsets.get(frozenset([item]), 0) ) rules.append({ "antecedent": antecedent, "consequent": frozenset([item]), "support": support, "confidence": confidence, "lift": lift, }) return rulesconfidence计算的是“买了前件中的商品时又买后件的概率”,lift进一步把这一概率和全局购买概率做了比值。lift大于1说明前件对后件有正向拉动,等于1说明两者独立,小于1则负相关。报告里真正值得写的是lift明显偏离1的规则,而不是置信度最高的那批。
注意:lift公式里依赖单项支持度,而单项必须在all_itemsets里。如果某个频繁项集的子集因为阈值没被记录,这里会取到0导致跳过。所以手写时务必保证所有非空子集都随Apriori结果一起保留,min_support也要全程一致。
调用入口:
rules = extract_rules(apriori(transactions, min_support=0.02), min_confidence=0.5) print(len(rules))4.3 最小支持度、置信度、提升度的参数设定经验
三个参数没有通吃数值,但有可复制的调参顺序。建议从min_support=0.02开始,打印频繁项集数量和最大项集长度;如果频繁项集为空,说明数据太稀疏,改成0.005再试;如果项集数量上万,说明阈值太低,升到0.05。这个来回看起来像玄学,其实背后就是看数据在什么密度上有东西可挖。
min_confidence从0.5起步,看规则条数。条数太多就把置信度提到0.7,条数太少降到0.3。这里有一个经验:规则条数控制在50到200条之间,报告才做得下去,再多就变成噪音列表。
| 参数 | 参考起点 | 过大过小的影响 |
|---|---|---|
| min_support | 0.01-0.05 | 过小导致项集爆炸,过大导致空结果 |
| min_conf | 0.5-0.7 | 过小规则全是弱关联,过大规则稀少 |
| lift | 只看>1 | 等于或小于1的规则没有业务增量 |
调参过程中我习惯把每个参数组合下的频繁项集数量记下来,画一条“min_support-频繁项集数”的折线放进报告。老师看到的就不是拍脑袋定参数,而是基于数据分布的选择过程。
5. 频繁模式挖掘避坑指南:5个血泪踩坑记录
这些坑我实打实翻车过,每一条都对应一次熬夜调试。下面按“现象→原因→解决”结构写,方便你直接对照排查。
5.1 空结果与规则爆炸:两个方向的阈值失控
现象:min_support=0.1跑出空列表,频繁项集一个都没有;改小支持度后,min_confidence=0.3又跑出几千条规则,报告根本塞不下。
原因:第一个是阈值高过数据实际分布。要衡量“两个商品同时出现”,0.1是很高的要求,几千订单的数据里很少有二项集达到10%。第二个是置信度低到足以收录随机共现,尤其长尾商品只要有一次与爆款同现,就会被算进规则。
解决:调参前先打印商品支持度分布:
item_support = ( df.groupby("product_name")["order_id"].nunique() / df["order_id"].nunique() ) print(item_support.describe())根据分位数选min_support,一般取P50到P90之间的数作为起点。规则太多时用lift>1直接过滤,别指望只调置信度。
5.2 常识规则刷屏与中文乱码:结果层面的两个翻车点
现象:按支持度排序,top规则全是“牛奶→面包”这种热门商品组合,看起来正确但毫无信息量;同时Windows控制台输出商品名变成问号,CSV用Excel打开乱码。
原因:频繁项集天然偏向高频商品,热门商品之间的规则支持度高,但不代表有业务价值。乱码则是编码问题:Python在Windows下stdout默认编码可能是GBK,遇到不常见字符直接替换;CSV写入时用了默认encoding参数,没考虑Excel的兼容性。
解决:排序指标从confidence换成lift,只保留lift>1.5的规则,常识规则会自动掉下去。编码问题用两行代码解决:
import sys sys.stdout.reconfigure(encoding="utf-8") df.to_csv("rules.csv", index=False, encoding="utf-8-sig")utf-8-sig带BOM头,Excel能正确识别。这两个小改动,能省掉答辩现场一次尴尬。
5.3 报告图表与代码对不上:提交前的自检清单
现象:截图里的规则数量和最终代码重跑结果不一致,报告里写的参数和README里记录的对不上。
原因:调参过程中改了min_support,图表没更新,或者代码最后回滚到旧版本,文档说明还停留在另一组参数上。这是数据挖掘大作业最容易被扣分的点,因为老师只要重跑一遍就能发现。
解决:定稿前把参数、输出文件、图表和代码版本做成同一个清单,重跑一遍全部脚本再截图。源代码文档说明里写清楚“运行环境、输入文件、输出文件、参数位置”四件事,任何人按文档重跑都能得到报告里的同一批数字。
| 检查项 | 具体动作 |
|---|---|
| 参数记录 | README里写清min_support、min_confidence、数据文件版本 |
| 重跑验证 | 定稿前清空输出目录,完整重跑一次脚本 |
| 截图更新 | 图表必须来自最后一次运行,不能拿旧图 |
| 环境锁定 | 记录python版本和pandas/mlxtend版本 |
提交前把源代码放进git仓库打一个tag,配合README里记录的参数,等于给这次作业上了后悔药。万一答辩现场被质疑,随时能重跑核对。
6. 把挖掘结果做成一份合格报告:验证、可视化与收尾技巧
6.1 频繁模式的可视化:支持度-置信度散点图与规则网络图
报告里只贴表格是吃亏的。matplotlib画一张散点图,x轴是支持度,y轴是置信度,点的大小映射规则前件长度,颜色映射lift。答辩时这一张图能讲三分钟。
import matplotlib.pyplot as plt plot_data = [(r["support"], r["confidence"], r["lift"]) for r in rules] plt.scatter( [d[0] for d in plot_data], [d[1] for d in plot_data], c=[d[2] for d in plot_data], alpha=0.6, ) plt.colorbar(label="lift") plt.xlabel("support") plt.ylabel("confidence") plt.savefig("rules_scatter.png", dpi=200)6.2 用数据划分验证频繁模式是否稳定
把订单按时间切前一半和后一半,分别跑同一组参数,对比两边频繁项集的交集比例。交集越高,说明挖掘结果在时间维度上越稳定,不是某一周的偶然表现。这段验证写进报告,能明显拉开“跑完就交”的差距。
6.3 源代码说明与报告的组织顺序
源代码文档说明写成一份README,包含运行环境、第三方库版本、输入文件格式、命令示例、参数定义、输出列名解释。报告PDF则按“业务背景、数据预处理、算法原理、实验参数、结果分析、业务建议”组织。最后提一个我吃过的亏:评分不是看规则有多惊人,而是看你能否解释清楚每一步为什么这样做。我当年把min_support调到0.1导致频繁项集为空,折腾一整天才发现是参数问题,后来养成了先看数据分布再定参数的习惯。希望帮到你——把第2节的稀疏度检查当作运行前第一件事,你会在调参上少花很多时间。
本文还有配套的精品资源,点击获取