ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

频繁模式挖掘实战:Apriori与FP-Growth选型及Python实现

频繁模式挖掘实战:Apriori与FP-Growth选型及Python实现 简介面向数据仓库与数据挖掘课程设计/期末大作业场景的 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, encodingutf-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编码的DataFramefrom mlxtend.preprocessing import TransactionEncoder te TransactionEncoder() te_ary te.fit(transactions).transform(transactions) df_encoded pd.DataFrame(te_ary, columnste.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_support0.02, min_confidence0.5) print(fApriori耗时: {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_support0.02), min_confidence0.5) print(len(rules))4.3 最小支持度、置信度、提升度的参数设定经验三个参数没有通吃数值但有可复制的调参顺序。建议从min_support0.02开始打印频繁项集数量和最大项集长度如果频繁项集为空说明数据太稀疏改成0.005再试如果项集数量上万说明阈值太低升到0.05。这个来回看起来像玄学其实背后就是看数据在什么密度上有东西可挖。min_confidence从0.5起步看规则条数。条数太多就把置信度提到0.7条数太少降到0.3。这里有一个经验规则条数控制在50到200条之间报告才做得下去再多就变成噪音列表。参数参考起点过大过小的影响min_support0.01-0.05过小导致项集爆炸过大导致空结果min_conf0.5-0.7过小规则全是弱关联过大规则稀少lift只看1等于或小于1的规则没有业务增量调参过程中我习惯把每个参数组合下的频繁项集数量记下来画一条“min_support-频繁项集数”的折线放进报告。老师看到的就不是拍脑袋定参数而是基于数据分布的选择过程。5. 频繁模式挖掘避坑指南5个血泪踩坑记录这些坑我实打实翻车过每一条都对应一次熬夜调试。下面按“现象→原因→解决”结构写方便你直接对照排查。5.1 空结果与规则爆炸两个方向的阈值失控现象min_support0.1跑出空列表频繁项集一个都没有改小支持度后min_confidence0.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之间的数作为起点。规则太多时用lift1直接过滤别指望只调置信度。5.2 常识规则刷屏与中文乱码结果层面的两个翻车点现象按支持度排序top规则全是“牛奶→面包”这种热门商品组合看起来正确但毫无信息量同时Windows控制台输出商品名变成问号CSV用Excel打开乱码。原因频繁项集天然偏向高频商品热门商品之间的规则支持度高但不代表有业务价值。乱码则是编码问题Python在Windows下stdout默认编码可能是GBK遇到不常见字符直接替换CSV写入时用了默认encoding参数没考虑Excel的兼容性。解决排序指标从confidence换成lift只保留lift1.5的规则常识规则会自动掉下去。编码问题用两行代码解决import sys sys.stdout.reconfigure(encodingutf-8) df.to_csv(rules.csv, indexFalse, encodingutf-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], alpha0.6, ) plt.colorbar(labellift) plt.xlabel(support) plt.ylabel(confidence) plt.savefig(rules_scatter.png, dpi200)6.2 用数据划分验证频繁模式是否稳定把订单按时间切前一半和后一半分别跑同一组参数对比两边频繁项集的交集比例。交集越高说明挖掘结果在时间维度上越稳定不是某一周的偶然表现。这段验证写进报告能明显拉开“跑完就交”的差距。6.3 源代码说明与报告的组织顺序源代码文档说明写成一份README包含运行环境、第三方库版本、输入文件格式、命令示例、参数定义、输出列名解释。报告PDF则按“业务背景、数据预处理、算法原理、实验参数、结果分析、业务建议”组织。最后提一个我吃过的亏评分不是看规则有多惊人而是看你能否解释清楚每一步为什么这样做。我当年把min_support调到0.1导致频繁项集为空折腾一整天才发现是参数问题后来养成了先看数据分布再定参数的习惯。希望帮到你——把第2节的稀疏度检查当作运行前第一件事你会在调参上少花很多时间。本文还有配套的精品资源点击获取
返回列表