简介:Coursera课程“数据挖掘中的模式发现”配套代码包,面向正在学习数据挖掘基础、希望用Python和R动手实践算法的学员,帮助把抽象的算法概念转化为可运行的代码。压缩包共4个文件,包含2个Python脚本、1个R脚本及1个Markdown说明文档,整体仅2KB,体量虽小但针对性强。目前已有108人学习/下载。其中的Python与R脚本对应课程测验中的编程练习,覆盖数据清洗、特征选择、聚类与分类等典型环节,Markdown文档则简要介绍代码背景与运行方式。对照课程内容阅读这些代码,可直观理解数据挖掘从数据预处理到模型构建的完整流程,也能为完成同类作业提供思路参考,尤其适合课程同步练习或考前复习使用。对于希望快速上手数据挖掘编程实现、梳理模式发现核心步骤的学习者,这份代码包具有实用的借鉴价值。
1. 模式发现课程代码在跑什么:从频繁项集到关联规则
很多人以为《数据挖掘中的模式发现》这门课的代码,就是套一个现成库跑一遍 Apriori,输出几条“啤酒→尿布”式的关联规则就算交差。真正动手跟完课程才发现,代码里一半工作量在调阈值、核对两种算法输出是否一致、清洗脏数据。这门课围绕一个核心问题展开:如何从交易记录、点击日志、序列事件中找到频繁出现且有价值的组合与先后顺序。课程代码则是把这些算法从伪代码变成可运行实验的关键载体,适合正在跟课的学生用来验证公式,也适合做购物篮分析、行为序列分析的从业者快速对齐算法细节。这篇笔记直接按做作业、做项目的顺序把频繁项集、关联规则、序列模式三块拆开讲,每块都带可复现代码和参数说明。
2. 频繁项集挖掘:Apriori 的逐层剪枝与 FP-Growth 的两趟扫描
2.1 Apriori 在算什么:支持度、先验性质与逐层剪枝
频繁项集要回答一个业务问题:哪些东西经常一起出现。以购物篮数据为例,每一行是一笔订单,目标是找出所有项目集合,使得这个集合出现在足够多的订单中。这个“足够多”的标准就是支持度阈值 min_sup。课程里通常用相对支持度,sup(X)=count(X)/n,注意 n 是所有有效事务数,空订单和表头行如果混进 n,阈值会被稀释,结果会整体偏保守。
Apriori 能在尚可接受的时间内跑起来,靠的是先验性质:一个项集频繁,它的所有子集必然频繁;反过来,任何包含非频繁子集的超集一定不频繁。基于这条性质,算法在第 k 轮只用频繁(k-1)项集去连接生成候选 k 项集,然后立刻把那些含有非频繁(k-1)子集的候选剪掉,最后才扫描数据库计数。这个“连接—剪枝—计数—过滤”的循环会一直持续到没有新候选出现,每一轮的候选规模都被控制住了。
min_sup 是整个算法里最敏感的参数。设高了,频繁项集很快就断在 2 项或 3 项,挖不出长模式;设低了,候选按指数膨胀,内存和时间双双失控。课程作业里常见取 0.01 到 0.05 之间,但先别照抄,具体看数据和你想找的模式长度。数据越稀疏越要往低调,数据越稠密阈值可以提高一些。
2.2 手写 Apriori:最小可运行代码与关键参数
下面这段代码按课程要求的“连接—剪枝—计数”三步走实现,不依赖任何第三方库,Python 3.8 以上直接可跑。
import csv def load_baskets(path): baskets = [] with open(path, newline='', encoding='utf-8') as f: for row in csv.reader(f): items = set(row) # 去重:同一订单里重复商品只算一次 items.discard('') # 去掉空串 if items: # 空订单不参与计数 baskets.append(items) return baskets def apriori(baskets, min_sup): n = len(baskets) # 有效事务总数 # 第1轮:统计单个项目出现次数 item_cnt = {} for b in baskets: for it in b: item_cnt[it] = item_cnt.get(it, 0) + 1 freq = {frozenset([it]): cnt for it, cnt in item_cnt.items() if cnt / n >= min_sup} all_freq = dict(freq) k = 2 while freq: keys = sorted(freq) candidates = set() # 连接:两个频繁(k-1)项集共享前k-2个元素时才合并 for i in range(len(keys)): for j in range(i + 1, len(keys)): a, b = sorted(keys[i]), sorted(keys[j]) if a[:-1] == b[:-1]: candidates.add(frozenset(a + [b[-1]])) # 剪枝:任一k-1子集不频繁就直接丢弃 candidates = {c for c in candidates if all(frozenset(c - {x}) in freq for x in c)} if not candidates: break # 计数:扫描事务,用issubset判断包含关系 freq = {} for c in candidates: cnt = sum(1 for b in baskets if c.issubset(b)) if cnt / n >= min_sup: freq[c] = cnt all_freq.update(freq) k += 1 return all_freq if __name__ == '__main__': baskets = load_baskets('baskets.csv') freq = apriori(baskets, min_sup=0.02) for itemset, cnt in sorted(freq.items(), key=lambda x: -x[1])[:10]: print(sorted(itemset), cnt)这段代码把课程作业里最常见的三个坑顺手填了:读文件时用 set(row) 去重同一订单里的重复商品,避免一个商品被重复计多次;空篮子被 discard 后不进入事务总数 n;连接条件用 a[:-1] == b[:-1] 保证只合并共享前 k-2 个元素的项集,合并结果恰好是 k 项集。剪枝一步用 c - {x} 枚举候选的所有 k-1 子集,再通过字典 O(1) 查是否在 freq 里,这种写法比写两层循环判断子集要快,也更不容易漏。
关键参数只有两个。min_sup=0.02 代表“至少出现在 2% 的有效订单中”,10000 笔订单对应 200 笔,不同数据集之间比较时都用比例,别用绝对计数。load_baskets 返回 list[set] 的格式,后续生成关联规则、做支持度换算都依赖这个统一结构。返回的 all_freq 是字典,键为 frozenset,值为绝对出现次数,规则的支持度和置信度都能直接从这里取值。
注意:比较时用 cnt / n >= min_sup,不要写成 cnt >= min_sup。同一阈值在 1000 笔和 100 万笔数据上含义完全不同,课程作业中因为这个写错导致结果对不上的情况非常普遍。
2.3 FP-Growth 为什么快:FP 树、节点链与条件模式基
Apriori 的瓶颈在于每一轮都要全表扫描事务,加上候选集指数增长,数据量过了十万行就明显吃力。FP-Growth 换了一个思路:把事务压缩成一棵 FP 树,只扫两遍原始数据。第一遍扫描统计每个项目的全局频次,过滤掉低于阈值的高频项,然后把每个事务按项目频次降序重排;第二遍扫描把这些重排后的事务逐条插入树中,共享前缀的事务落在同一条路径上,路径上的节点计数不断累加。
树的每个节点除了 item 和 count,还通过 header 表把相同项目的所有节点串成链表,这样挖掘时不必回扫原始事务。挖掘从支持度最低的频繁项开始,顺着 header 表找到该项的所有节点,沿 parent 链回溯出前缀路径,路径出现次数乘以节点计数,就得到该项的条件模式基。条件模式基相当于“只包含该项关联上下文”的小事务库,对它递归建条件 FP 树,就能逐步长出所有频繁项集。
和 Apriori 相比,FP-Growth 不做候选连接,没有逐层剪枝的额外开销,数据越稠密、事务越长,优势越明显。但它的代码复杂度也高,节点指针、计数累加、条件库构造都是容易写错的地方,所以课程作业里通常要求两个算法结果对齐,用 Apriori 校验 FP-Growth 的正确性。
2.4 复现 FP-Growth 并与 Apriori 对齐
下面是一个能跑的 FP-Growth 教学实现。为了控制篇幅,条件模式基用“路径+计数”显式存储,工程上可以用指针投影继续优化,但逻辑完全一致。
class FPNode: def __init__(self, item, count=1, parent=None): self.item = item self.count = count self.parent = parent self.children = {} def build_fp_tree(transactions, min_cnt): # 第1趟扫描:统计全局频次 item_cnt = {} for t in transactions: for item in set(t): item_cnt[item] = item_cnt.get(item, 0) + 1 item_cnt = {i: c for i, c in item_cnt.items() if c >= min_cnt} header = {i: [c, []] for i, c in item_cnt.items()} # [总计数, 节点链表] # 第2趟扫描:建树 root = FPNode(None) for t in transactions: items = [i for i in t if i in item_cnt] items.sort(key=lambda i: item_cnt[i], reverse=True) # 全局频次降序 cur = root for item in items: if item in cur.children: cur.children[item].count += 1 # 共享前缀计数累加 else: node = FPNode(item, 1, cur) cur.children[item] = node header[item][1].append(node) cur = cur.children[item] return root, header def mine_fp_tree(header, min_cnt, prefix): results = [] for item in sorted(header.keys(), key=lambda i: header[i][0]): new_prefix = prefix + [item] results.append(new_prefix) # 提取该项目的条件模式基:所有祖先路径 x 节点计数 cond_base = [] for node in header[item][1]: path = [] p = node.parent while p and p.item is not None: path.append(p.item) p = p.parent if path: cond_base.append((path, node.count)) # 统计条件库中每个项目的频次 cond_cnt = {} for path, cnt in cond_base: for i in path: cond_cnt[i] = cond_cnt.get(i, 0) + cnt cond_cnt = {i: c for i, c in cond_cnt.items() if c >= min_cnt} if cond_cnt: # 对条件模式基递归建条件FP树 sub_header = {i: [c, []] for i, c in cond_cnt.items()} sub_root = FPNode(None) for path, cnt in cond_base: items = [i for i in path if i in cond_cnt] items.sort(key=lambda i: cond_cnt[i], reverse=True) cur = sub_root for it in items: if it in cur.children: cur.children[it].count += cnt else: node = FPNode(it, cnt, cur) cur.children[it] = node sub_header[it][1].append(node) cur = cur.children[it] results.extend(mine_fp_tree(sub_header, min_cnt, new_prefix)) return results if __name__ == '__main__': # 复用第2.2节的数据,换成同样的购物篮列表 transactions = [set(items) for items in baskets] min_cnt = int(min_sup * len(transactions)) root, header = build_fp_tree(transactions, min_cnt) fp_results = mine_fp_tree(header, min_cnt, []) apriori_set = {frozenset(s) for s in freq} fp_set = {frozenset(s) for s in fp_results} print('结果一致:', apriori_set == fp_set, 'Apriori:', len(apriori_set), 'FP:', len(fp_set))两个关键点需要在跑之前确认。min_cnt 是绝对支持度,int(min_sup * len(transactions)) 与 Apriori 的 min_sup 等价,事务数很大时取整误差不影响项集集合,但课程里要严格对齐可以用分数比较。建树排序用的是项目全局频次降序,目的让公共前缀尽可能长,树更紧凑,挖掘时条件模式基更短。
对齐测试是课程代码里非常值得做的一步。两个算法从不同路径出发,结果必须完全一致;不一致往往出在实现细节上。第一是 header 计数,插入共享前缀时忘了给已有子节点累加 count,条件模式基的频次就会偏低。第二是路径回溯时把 item 自身也算进去,条件库会凭空多出当前项。第三是条件树里的过滤阈值,应该用条件库内的频次 cond_cnt,而不是原 header 里的全局计数。这三个点都对上,结果基本就一致了。
3. 关联规则生成:支持度、置信度与提升度怎么组合才靠谱
3.1 频繁项集算完,为什么还要生成关联规则
频繁项集回答的是“哪些东西一起出现”,但业务要的是“出现 A 之后是否更可能发生 B”。购物篮里同时出现啤酒和尿布,运营想知道的是:看到啤酒的用户是否更可能买尿布。于是需要把频繁项集拆成前件 X 和后件 Y,形成规则 X → Y。课程代码从这一步开始从“找组合”变成“找规则”,输出也从项集列表变成规则列表。
规则的质量不能只看一个指标。置信度是条件概率 sup(X∪Y)/sup(X),回答“前件出现时后件概率有多高”;提升度是置信度除以后件先验概率 sup(Y),回答“相比随机情况提升了多少”。提升度大于 1 才是正相关,等于 1 表示独立,小于 1 表示反而抑制。课程作业里通常要求三个指标同时输出,因为只看置信度会踩到后件本身高频带来的假象。
3.2 从频繁项集枚举规则的代码:三个指标一次算全
在上一章 all_freq 字典的基础上,生成规则只需要枚举每个频繁项集的非空真子集作为前件,后件就是补集。注意前件枚举到长度为 len(itemset)-1 就停,否则后件为空没有意义。
import itertools def generate_rules(freq, n, min_conf=0.5, min_lift=1.0): rules = [] for itemset, xy_cnt in freq.items(): if len(itemset) < 2: continue items = list(itemset) # 枚举所有非空真子集作为前件 for r in range(1, len(items)): for x_tuple in itertools.combinations(items, r): x = frozenset(x_tuple) y = itemset - x sup_xy = xy_cnt / n # X∪Y的支持度 sup_x = freq[x] / n # X的支持度,Apriori保证存在 conf = sup_xy / sup_x # 置信度 lift = conf / (freq[y] / n) # 提升度 if conf >= min_conf and lift >= min_lift: rules.append((x, y, sup_xy, conf, lift)) return rules rules = generate_rules(freq, len(baskets), min_conf=0.3, min_lift=1.2) rules.sort(key=lambda r: -r[4]) # 按提升度降序 for x, y, sup, conf, lift in rules[:10]: print(set(x), '->', set(y), f'sup={sup:.3f} conf={conf:.3f} lift={lift:.3f}')枚举时用 itertools.combinations 而不是自己写嵌套循环,避免前件和后件重复枚举同一组合。freq[x] 一定取得到,因为 X 是频繁项集 itemset 的子集,Apriori 性质保证它也在 all_freq 字典里;如果这里 KeyError,说明上一章实现丢了项集,先回去查 Apriori。min_conf 和 min_lift 是两道闸,conf 保证条件概率足够高,lift 保证不是“看起来高但实际负相关”。
输出时把支持度也打印出来是有意的。支持度太低的规则哪怕提升度很高,也只是小样本上的偶然,没法支撑规模化决策。排序按提升度降序是为了让业务方先看最强的信号,实际项目中这一页列表通常直接进运营报表。
3.3 三个指标组合使用的选择逻辑
支持度、置信度、提升度三者回答的是不同问题,组合使用时需要先明确场景。高支持度加高置信度,代表规则覆盖的人多、命中率高,适合做货架摆放、默认推荐这类铺量运营;高提升度但低支持度,代表规则只在少数人身上成立,但一旦成立相关性很强,适合做个性化推荐和冷启动名单,不适合投放到全量用户。
min_sup 和 min_conf 之间还有一层制约关系。min_sup 调低,长项集变多,规则数量暴涨;此时如果把 min_conf 也调低,输出会淹没在大量无效规则里。常见的做法是固定一个向,比如先定支持度筛出能规模化执行的项集,再单独调 min_conf 控制规则条数,最后用 min_lift 过滤负相关。课程作业里最容易犯的错就是把 min_conf 压到 0.1 来“多拿几条规则”,结果规则置信度无限接近后件先验概率,业务上根本没有增量。
3.4 规则数量失控时的筛选套路
一万个项集能生成百万条规则,全部输出没有意义。我一般会按四步收窄。第一步,按提升度降序取前 100 条人工过一遍,快速判断这个数据源里到底有没有真信号;第二步,限制规则长度在 2 到 3 项,太长的规则没法落成可执行动作;第三步,把包含“首页”“登录页”“搜索框”这类入口事件的规则过滤掉,它们几乎和所有模式共现,只会制造噪音;第四步,按后件分组,每组保留提升度最高的一条,避免几十条规则都在说同一个后件。
过滤代码只需要在生成 rules 之后加一个集合交集判断,例如排除前件或后件落在停用项集合里的规则。这一步在课程里可能不是必做项,但在真实数据上不做,规则列表基本没法读。后面避坑章里还会专门展开高频噪音项的处理。
提示:规则评估没有万能阈值。0.5 的置信度在某个类目下可能已经很高,在另一个类目下却低于先验概率。先看后件本身的出现概率,再判断置信度有没有增量。
4. 序列模式挖掘:把“同时出现”升级成“先后出现”
4.1 从购物篮到点击流:序列模式到底换了什么假设
频繁项集和关联规则都有一个隐含假设:项目之间没有顺序,{A, B} 和 {B, A} 是同一个项集。但点击流、操作日志、订单状态流转这类数据,顺序本身就是信息。“先看商品页再支付”和“先支付再看商品页”是两种完全不同的行为。序列模式把每条数据当作一个有序事件列表,例如某个用户的一次会话表示为 <登录, 商品页, 加购, 支付>,模式挖掘的目标是找出频繁出现的子序列。
子序列的定义是“在不改变相对顺序的情况下删除若干元素后仍然匹配”。比如模式 <登录, 支付> 匹配 <登录, 商品页, 支付>,因为登录在支付之前出现;但不匹配 <支付, 登录>。课程代码这部分通常是一个独立模块,输入格式和购物篮完全不同:购物篮是一行一个集合,序列数据库是一行一个有序列表,甚至每个元素本身还可以是集合,代表同一时刻发生的多个事件。
4.2 PrefixSpan 简化实现:前缀投影与递归挖掘
PrefixSpan 的核心思想是不生成候选,而是不断用前缀投影数据库。对每个频繁项,把序列数据库中所有匹配该前缀的后缀收集起来,形成投影数据库,再在投影库里递归找下一个频繁项。下面是教学简化版,假设每个事件元素是单项集,序列以 list of set 表示。
def prefixspan(db, min_sup): n = len(db) results = [] def mine(prefix, proj): item_cnt = {} item_pos = {} # item -> [(序列, 元素下标, 元素集合)] for seq in proj: seen = set() for i, elem in enumerate(seq): for item in elem: if item in seen: continue seen.add(item) item_cnt[item] = item_cnt.get(item, 0) + 1 item_pos.setdefault(item, []).append((seq, i, elem)) for item, cnt in item_cnt.items(): if cnt / n < min_sup: continue results.append(prefix + [item]) new_proj = [] for seq, pos, elem in item_pos[item]: tail = elem - {item} suffix = [tail] if tail else [] suffix += seq[pos+1:] new_proj.append(suffix) mine(prefix + [item], new_proj) mine([], db) return results # 示例:3条用户会话序列 db = [ [{'登录'}, {'商品页'}, {'支付'}], [{'登录'}, {'商品页'}, {'加购'}, {'支付'}], [{'支付'}, {'登录'}], ] for pat in prefixspan(db, min_sup=0.5): print(pat)这个实现有几个需要说明的限制。item_pos 里每个序列只记录项目第一次出现的位置,标准 PrefixSpan 会为同一项目在序列中多次出现保留多条投影,因此这里的结果对“重复出现”不敏感,适合点击流里每个动作只算一次的场景,不适合重复购买这类计数敏感的序列。另外,挖掘出的模式是单元素序列,也就是每个时间步只包含一个项目;如果同一时刻会同时发生多个事件,需要在 tail 上枚举子集组合,代码会复杂一个量级。课程作业多数场景下,先把事件按时间排序压成单元素序列,这个简化版就够用。
min_sup 的计数单位是“序列条数”,不是“事件条数”。上面例子中 <登录, 支付> 出现在第 1 和第 2 条序列,sup=2/3=0.67,所以 min_sup=0.5 时能被挖出来。注意第 3 条序列是 <支付, 登录>,登录在支付之后,不匹配 <登录, 支付>,这个顺序敏感性正是序列模式区别于频繁项集的地方。
4.3 时间窗口与间隔约束:点击流序列的三个参数
序列模式如果只按相对顺序匹配,会把相隔 5 小时的两次登录也算成同一个模式。真实点击流分析里,时间距离是模式成立与否的关键。PrefixSpan 本身不感知时间,预处理和参数约束要在投影阶段解决。课程里常见三个参数:
| 参数 | 含义 | 典型取值 | 业务说明 |
|---|---|---|---|
| min_gap | 模式内相邻事件的最小间隔 | 0~1 秒 | 防止把同一时刻的重复日志当顺序 |
| max_gap | 模式内相邻事件的最大间隔 | 5~30 分钟 | 超过间隔说明行为已中断,不算一个模式 |
| window_size | 一个元素内项目集合的时间跨度 | 1~60 秒 | 同一时间窗口内的事件视为同时发生 |
实操上,我习惯先把日志按用户和时间排序,然后用“会话切分”把订单前问题解决掉大半。切分逻辑很简单:同一用户相邻两条日志时间差超过 30 分钟,就开启新会话;一个会话内的数据再喂给 PrefixSpan。这样做的好处是 max_gap 的语义被前置处理了,算法本身的参数只剩 min_sup 一个,调参难度大大降低。
4.4 闭序列与最大序列:输出结果的两种压缩
序列模式比频繁项集更容易爆炸,因为模式可以无限变长。阈值一低,结果里塞满成百上千条互相包含的序列,人工根本看不过来。闭序列和最大序列是两种压缩思路。
| 输出类型 | 定义 | 能否恢复全部频繁模式 | 适用场景 |
|---|---|---|---|
| 频繁序列 | 支持度达标的所有序列 | 是 | 需要完整证据链 |
| 闭序列 | 不存在支持度相同的真超集 | 是 | 压缩输出,保留支持度信息 |
| 最大序列 | 不存在频繁真超集 | 否 | 只要模式骨架,不要频次细节 |
举例说明,序列 <登录, 商品页> 出现 80 次,<登录, 商品页, 支付> 也出现 80 次,那么前者就不是闭序列,因为存在一个支持度相同的超集;后者是闭序列。最大序列则要求没有“任何”频繁超集,哪怕超集支持度更低都不算。课程代码里实现闭序列通常是在挖掘完成后做一次去冗余,判断每个模式是否存在支持度相等的超集;最大序列更激进,适合项集长度特别长、只想要行为主干的场景。
5. 模式挖掘避坑手册:五个最容易翻车的场景
5.1 支持度阈值设得太低:内存和等待时间先撑不住
现象:一万条购物篮数据,min_sup 设为 0.001,任务跑了一个小时没结束,内存占用涨到几十 GB,最后进程被杀。原因:低频项激活了大量候选组合,Apriori 的候选集按指数膨胀,FP-Growth 虽然不生成候选,但条件模式基也会因为低频项而变得碎片化,递归层数深不见底。解决:先跑 min_sup=0.05 看频繁项集数量级,再逐级下调,每次只降一半;发现项集数从几百跳到几万的那一级,就是阈值悬崖,别越过它。数据量大时优先换 FP-Growth,并按用户或门店分片跑,最后合并结果。
5.2 读数据没去重没过滤:支持度统计整个失效
现象:Apriori 跑出来的单项目支持度比预期高一倍,或者某些明显常见的商品反而没进频繁项集。原因:CSV 里表头行没跳过,把“商品名”这种列名当成了真实项目;同一订单里同一个商品重复出现多次,set(row) 这步缺了,单项目计数被重复累加;空购物篮行参与计数,把 n 撑大,稀释了所有项目的支持度。解决:读文件时跳过表头,每行强制 set() 去重,过滤掉空行和全空项目。清洗函数很短,但直接影响后面所有指标。
def clean_baskets(raw_rows): baskets = [] for row in raw_rows: items = {x.strip() for x in row if x.strip()} if items: baskets.append(items) return baskets5.3 置信度很高但提升度小于 1:规则其实是负相关
现象:“收藏商品 → 下单”这条规则置信度 0.85,看起来很强,但提升度只有 0.94。原因:平台整体下单率本来就是 0.9,收藏行为出现后下单率反而从 0.9 降到了 0.85,说明收藏这个动作对下单不是促进而是干扰,只是被高基础概率掩盖了。解决:规则评估必须同时看 lift,筛选条件写成 lift > 1.0 而非只设 min_conf。输出报表里 conf 和 lift 并列展示,业务方一旦看到 lift 小于 1 的规则就该意识到这是负相关信号。
5.4 序列数据没排序没切会话:挖出来的全是“伪顺序”
现象:点击日志直接喂给 PrefixSpan,结果“登录 → 支付”和“支付 → 登录”几乎同时出现在频繁模式里,业务上完全说不通。原因:日志没有按用户和时间排序,或者时间戳还是字符串,group by 之后顺序混乱;同一个用户跨度一天的日志被算成一条序列,顺序被随机拼接。解决:先把时间戳转成统一时间格式,按用户和时间排序,再按 30 分钟间隔切分会话,最后把每个会话压成事件序列。排序和切分两步做完,模式才有业务含义。
df = df.sort_values(['user_id', 'ts']) df['gap'] = df.groupby('user_id')['ts'].diff() df['new_session'] = (df['gap'] > pd.Timedelta(minutes=30)) | df['gap'].isna() df['session_id'] = df['new_session'].cumsum()5.5 结果全是噪音模式:高频入口项把有价值模式全淹了
现象:频繁项集 TOP 10 里九个都包含“首页”“登录页”这类入口事件,真正的业务组合被挤到榜单后面。原因:入口事件本身超高频,几乎所有序列都包含它们,支撑度天然高,但它们不携带业务意图。解决:建一个停用项表,在统计前直接过滤;或者进一步用提升度排序代替支持度排序,因为提升度能剔除“高频但无增量”的共现。输出仍然太多时,用闭序列或最大序列压缩,保留骨干模式。
STOP_ITEMS = {'首页', '登录页', '搜索框'} freq = {k: v for k, v in freq.items() if not (k & STOP_ITEMS)}6. 收敛在验证上:算法选型、阈值扫描与业务回归
6.1 算法选型:数据规模与模式类型决定方案
最后一步不是写代码,而是想清楚在什么情况下换工具。课程作业用小数据集跑通 Apriori 是必要的,因为它逻辑透明、方便对答案;但真实项目里十万级以上的稠密事务,FP-Growth 才是首选。序列数据则直接用 PrefixSpan,不要在 Apriori 上硬造顺序特征。结果过多时,闭序列和最大序列是输出压缩手段,不是新算法,它们基于任意一种频繁模式挖掘结果做后处理。
| 数据形态 | 数据规模 | 首选实现 | 选型理由 |
|---|---|---|---|
| 短事务购物篮 | 万级以内 | Apriori | 实现透明,便于课程排查和单步调试 |
| 长事务稠密数据 | 十万级以上 | FP-Growth | 无候选生成,内存与耗时可控 |
| 点击流/操作日志 | 任意规模 | PrefixSpan | 天然处理先后顺序 |
| 结果爆炸 | 所有场景 | 闭/最大序列 | 压缩输出,保留主干信息 |
6.2 用“支持度—模式数”曲线定阈值:别拍脑袋
我现在的固定习惯是先画一条曲线再定阈值。横轴是候选 min_sup,纵轴是频繁项集数量,曲线出现陡峭上升的地方就是噪声区间起点,阈值选在陡坡之前的平台段,既不漏模式,也不被噪音淹没。
import matplotlib.pyplot as plt thres = [0.10, 0.08, 0.06, 0.05, 0.04, 0.03, 0.02, 0.01] counts = [] for s in thres: counts.append(len(apriori(baskets, min_sup=s))) plt.plot(thres, counts, marker='o') plt.xscale('log') plt.xlabel('min_sup') plt.ylabel('frequent itemsets') plt.show()6.3 把规则放回业务做回归:最后一个验证习惯
算法跑完不意味着交付完成。我习惯从规则列表里抽 100 条让人工标注“合理/不合理”,统计 lift 大于 1.5 的占比,如果连一半都达不到,说明数据清洗或阈值选择还有问题。再用下一周的数据完整重跑一遍,对比两次频繁项集的 Jaccard 重合度,重合度低于 0.6 时要警惕数据本身的稳定性,而不是继续调参。
这三步做完,模式挖掘才算是闭环。以前我也跳过验证直接交结果,后来被业务方拿着一条诡异规则问回来,不得不回去重跑整个流程。现在固定下来,先清洗、再画曲线定阈值、最后拿新数据做稳定性验证,省掉的无谓返工比算法本身还多。希望帮到你。
本文还有配套的精品资源,点击获取