
简介这是一份面向数据分析与机器学习初学者的电影数据集关联规则挖掘实战资源。内容围绕Apriori算法在电影选角与导演偏好分析中的应用展开结合电影数据集演示如何通过关联规则发现导演选择演员的倾向并针对sklearn缺少Apriori的问题给出基于PyPI筛选并使用efficient-apriori工具包的思路。压缩包共9个文件包含Python脚本movie_apriori.py与movie_download.py、Markdown说明文档以及5张结果或流程示意图总大小约1.01MB结构紧凑便于快速上手。已有2359人学习下载适合希望理解关联规则实际落地并掌握Python工具选择方法的读者。通过阅读说明文档和运行脚本可以复现挖掘过程学习数据下载、预处理、频繁项集提取与规则解读的完整链路并迁移到其他商品捆绑或行为分析场景。1. 电影数据集关联规则挖掘从观影行为中挖出真实的“搭配偏好”很多做内容平台或视频业务的人手里都有一堆用户的观影记录却不知道怎么用起来。最常被问到的问题不是“哪些电影好看”而是“看完这部电影的人接下来还会看什么”。电影数据集关联规则挖掘解决的就是这个具体问题把“用户看过的电影”看作一笔交易把每部电影看作交易中的一件商品然后找出那些频繁一起出现的影片组合并算出一条条明确的规则。它不需要复杂的用户画像也不需要训练模型只要你手里有用户ID和电影ID就能跑起来。这篇文章会从数据清洗、事务编码、算法选型、参数调整到实际避坑完整走一遍可复现的流程。2. 把电影数据变成关联规则挖掘需要的“购物篮”从评分表到事务列表2.1 为什么关联规则挖掘不认普通评分表关联规则挖掘这套方法最早是用来分析超市购物篮的。算法本身只认一种数据结构一条事务包含若干个“项”。放到超市场景里一条事务就是一张小票项就是商品。放到电影场景里一条事务就是一个用户在一段时期内看过的所有电影项就是电影ID。原始评分表长什么样子你肯定不陌生每一行是一个用户对一部电影的评分附带时间戳。这种长表看起来信息很全但算法没法直接消化因为它不是事务结构——同一用户的多个行为散落在不同行里算法无法知道哪些电影属于同一次“购买”。所以要做的第一件事就是把用户维度拉出来让同一个用户看过的所有电影聚合成一个列表再转成算法可读的格式。这一步在整个挖掘流程里被很多人当成纯体力活实际上它决定了后续所有规则的质量。如果事务聚合错了比如把不同账号也算成同一个用户或者把用户只看了一部的电影也拉进来都会产生大量噪音后面调参数怎么调都别扭。2.2 从评分表到事务列表标准三步下面用一份常见的电影评分数据来走一遍标准流程。这里假定原始数据字段为 userId、movieId、rating、timestamp评分行为代表用户对影片有明确态度所以没有做额外过滤直接把所有行都纳入事务构建。import pandas as pd # 读入原始评分数据字段约定为 userId, movieId, rating, timestamp df pd.read_csv(ratings.csv) df df.dropna(subset[userId, movieId]) # 只看有效评分把打过分的行为视为“对该片有明确态度” df df[df[rating] 1] # 按用户分组把同一用户的 movieId 聚合成列表 transactions ( df.groupby(userId)[movieId] .apply(list) .reset_index(namemovie_list) ) # 转成 list of lists方便后续编码 transaction_list transactions[movie_list].tolist() print(事务数:, len(transaction_list)) print(平均每用户看过多少部:, sum(len(t) for t in transaction_list) / len(transaction_list))这段代码做的事有三步。第一步是删除 userId 或 movieId 为空的行这种脏数据在导出时很常见尤其当你从多张表 join 出数据时经常会出现个别行字段缺失。第二步是按用户分组把 movieId 聚合为列表。第三步是把 DataFrame 转成 Python 的列表结构因为后面无论用哪种编码器接收的都是这种格式。几个参数值得注意。rating 过滤里我用了 1意思是所有评分都保留因为只要能确认用户看过这部电影就已经构成一次事务行为评分高低对“看没看过”没有影响。如果你只想用高分评价来定义偏好可以改成 4这会导致事务变小、数据更稀疏规则挖掘的结果会更偏向“强烈偏好”而不是“泛看行为”。聚合时用 list 而不是 set是因为有些编码器处理列表时会更平滑去重可以放在聚合之后做。2.3 热编码与稀疏矩阵事务编码的两种写法得到事务列表后下一步是把文本化的电影 ID 转成算法能算的矩阵。最常见的做法是 one-hot 编码每一行代表一个用户每一列代表一部电影单元格为 True 或 False表示该用户是否看过这部电影。这种编码方式简单直观列名就是电影 ID但会出现一个绕不开的问题列数等于全部电影的数量而每个用户看过的电影可能只占其中极小一部分。矩阵会非常稀疏这直接影响后面频繁项集挖掘的效率。用 mlxtend 的 TransactionEncoder 是最省事的路线from mlxtend.preprocessing import TransactionEncoder te TransactionEncoder() te_ary te.fit(transaction_list).transform(transaction_list) df_encoded pd.DataFrame(te_ary, columnste.columns_) print(df_encoded.shape) print(df_encoded.iloc[:3, :5])TransactionEncoder 会扫描一遍全部事务找出出现过哪些电影然后为每个用户生成一个布尔向量。这里有一个非常容易忽略的坑如果你在事务构建阶段没有清理掉那些只有极少数人看过的冷门电影编码后的 DataFrame 列数会非常庞大。我碰到过一次跑了上万个事务的挖掘任务最后发现列里有一大半是只看过一次的电影不仅让编码矩阵占用内存暴增还让后续频繁项集计算慢到像卡死一样。另一种编码写法是手动构造稀疏矩阵。用 scikit-learn 的 MultiLabelBinarizer 可以做到同样的效果from sklearn.preprocessing import MultiLabelBinarizer mlb MultiLabelBinarizer() sparse_matrix mlb.fit_transform(transaction_list) df_encoded_sparse pd.DataFrame(sparse_matrix, columnsmlb.classes_)MultiLabelBinarizer 与 TransactionEncoder 的核心区别不大但它的输出是 numpy 数组可以直接转 scipy 稀疏矩阵对内存压力小很多。如果你的数据量在百万级事务以上建议直接用这个方案后续跑 FP-Growth 时能明显感受到差别。手动编码的好处是对数据格式有完全控制权坏处是要自己处理列名映射容易在电影 ID 转换时出错。3. 选 Apriori 还是 FP-Growth电影数据场景下的取舍3.1 Apriori 的结构与瓶颈Apriori 是最经典的关联规则挖掘算法它的核心思想用一句话说就是如果一个项集不频繁那它的任何超集也不可能频繁。基于这个剪枝策略它逐层生成候选集每次生成后扫描全表统计支持度筛掉不达标的然后用剩下的项集继续生成更大长度的候选项。这个逻辑清晰但有一个致命的效率问题每一次迭代都要完整扫描事务数据库。在电影数据集上这个瓶颈体现得非常明显。假设你有十万用户每用户平均看过三十部电影那么 1 项集只有几千个但 2 项集的数量可能就到百万级。Apriori 需要反复扫描十万条事务来统计这些组合的支持度每轮扫描都是不小的开销。如果频繁项集数量比较多迭代到 3 项集、4 项集时运行时间会以指数级别增长。我一般在两种情况下会坚持用 Apriori一是数据量不大几千到几万条事务跑起来毫无压力二是刚接触关联规则挖掘需要看中间结果来理解算法逻辑。Apriori 的所有中间候选集都是可读的适合调试。如果你的目的是在大数据集上快速产出规则那它并不是最佳选择。3.2 FP-Growth 为什么更适合电影库这类高维数据FP-Growth 的出发点就是解决 Apriori 反复扫描数据库的问题。它把全部事务压缩到一棵 FP-Tree 上树的每条路径代表一组共同出现的项每个节点记录该项在路径中出现的次数。整个过程只需要扫描两次原始数据第一次统计每个项的频率用于过滤不频繁的项第二次构建树。之后直接在树上递归挖掘频繁项集不再返回原始数据。对于电影数据来说FP-Growth 几乎总是优于 Apriori。原因很简单电影数据集通常是高维稀疏的——电影数量动辄几千上万单用户行为只有几十条。这种数据放到 FP-Tree 上树的高度不会太高但节点数量会被压缩得非常好内存占用和计算时间都远低于 Apriori 的候选集枚举方式。mlxtend 库里两种算法的接口几乎一样切换成本很低。下面是用 FP-Growth 挖掘频繁项集的写法from mlxtend.frequent_patterns import fpgrowth # df_encoded 是上一步得到的布尔矩阵列名为 movieId frequent_itemsets fpgrowth(df_encoded, min_support0.03, use_colnamesTrue) # 观察候选项集 frequent_itemsets[length] frequent_itemsets[itemsets].apply(lambda x: len(x)) print(frequent_itemsets.sort_values(support, ascendingFalse).head(10))参数里最关键的是 min_support0.03它表示只有出现在 3% 以上用户行为中的电影及其组合才会被保留下来。这个值直接决定频繁项集的数量设得太低会产生大量没啥实际意义的冷门组合设得太高又会漏掉有商业价值的垂直偏好。use_colnamesTrue 让结果直接以电影 ID 呈现而不是列索引调试时能省去查映射关系的步骤。length 列是我自己加上的用来标记每个项集里含多少部电影这在排序时非常有用——因为以 support 排序时单部热门片的支持度必然高于任何组合项集不按长度过滤时你会看到结果前几行全是单个电影名误以为算法没跑对。3.3 从频繁项集到关联规则方向与拆分频繁项集只告诉你“这些电影经常一起出现”但没告诉你方向。比如项集 {电影A, 电影B} 出现频率很高它既能拆成规则“看A的人也会看B”也能拆成“看B的人也会看A”。这两条规则在实际推荐场景里方向不同价值也不同所以需要在频繁项集基础上再做一步规则生成。from mlxtend.frequent_patterns import association_rules rules association_rules(frequent_itemsets, metriclift, min_threshold1.2) rules rules[rules[antecedent_len] 1] print(rules[[antecedents, consequents, support, confidence, lift]].head(20))association_rules 会对每个频繁项集生成所有非空真子集的前后件组合。metriclift 和 min_threshold1.2 是筛选规则的两道关卡先按提升度大于 1.2 保留正相关规则再按置信度观察强度。这里需要注意新版 mlxtend 的输出里有 antecedent_len 字段老版本没有如果你用老版本可以自己加一列计算。这一步最容易犯的错误是不做任何过滤直接把全部规则输出。如果一个频繁项集有 5 部电影它能拆出 30 条规则全部输出会让结果爆炸。所以生成规则后第一件事就是过滤掉那些前后件长度不合理的规则这个具体操作我会在第 5 章详细讲。4. 参数怎么设支持度、置信度、提升度在电影数据里的落地调整4.1 支持度先看懂数据分布再定阈值支持度衡量的是一个项集在全部事务中的覆盖率。电影场景里如果一部电影只被几百个用户看过它的支持度就非常低如果一部热门片被几万人看过支持度可能高达 0.2 甚至 0.3。这个差异反映到规则上就是频繁项集结果的底部堆满长尾冷门组合顶部被大众热门霸榜。所以你需要在跑算法之前先搞清楚数据的支持度分布。item_freq df_encoded.sum(axis0) / len(df_encoded) print(item_freq.describe(percentiles[0.5, 0.8, 0.9, 0.95])) # 看看最常见的10部电影能到多少支持度 print(item_freq.sort_values(ascendingFalse).head(10))看分布的意义在于选阈值。如果数据有 1 万用户其中 50% 的电影只有不到 1% 的用户看过那把 min_support 设为 0.01 会导致频繁项集数量爆炸因为大量 1% 附近的项集都会进来。我通常的做法是先看 50 分位和 80 分位如果 80% 的电影支持度都在 0.02 以下说明数据长尾严重min_support 定在 0.02 到 0.05 之间比较合适。如果目标是做长尾内容推荐可以把阈值降到 0.005但你要准备好接受大量噪音。一个常见的误区是直接把 min_support 设得很低来追求“挖掘出更多规则”。最后你会发现规则多了但真正能解释、能落地执行的比例反而下降了因为大量规则是只有个位数用户看过的冷门组合统计上根本不稳定。4.2 置信度与提升度一个看概率一个看独立性置信度的公式是 P(后件|前件)它回答“看了前件的人里面有多少人看了后件”。这个指标很直觉单独用它做排序却容易踩坑。举个具体例子假设《某超级英雄片》有 30% 的用户都看过。那任何一部前件片推导到这部影片的规则置信度都不会太低因为这个数值的基数已经很大了。你看到一条规则置信度 0.6感觉很强实际上拆分两部电影单独看后者本来就是大热门0.6 一点都不稀奇。提升度的公式是 P(前件∩后件) / (P(前件)·P(后件))它衡量的是“这两个电影共同出现是否比独立出现更频繁”。提升度大于 1说明确实是搭配关系在起作用而不是单品热度造成的假象。所以我的原则是用提升度做筛选用置信度做排序。先保住“真相关”再在真相关里找“高概率”。这里有一个很重要的习惯跑完规则后记得按提升度降序看一眼前 20 条你会发现很多之前被置信度排序埋没的组合。比如《冷门纪录片A》和《冷门纪录片B》置信度可能只有 0.25但提升度高达 4说明看 A 的人看 B 的概率是全局平均概率的 4 倍。这类规则在专题运营里价值极高。4.3 一个参数不对引发结果变形的实际例子之前处理一份电影数据时我一开始把 min_support 设成 0.01然后跑 FP-Growth结果频繁项集数量从预期的一千多条暴涨到七万多条生成规则时内存直接溢出。当时第一反应是代码写错了反复检查后发现是数据里混了约 25% 的冷门实验影片这些影片的支持度极低但架不住数量庞大把频繁项集的规模顶上去了。之后把阈值策略改成两段式先用支持度阈值筛掉纯冷门影片比如只保留出现在至少 50 个用户行为里的电影再基于过滤后的影片集合重新构建事务然后跑规则。这样处理后频繁项集数量下降了一个数量级以上剩下的规则也更有业务解释空间。所以参数不是拍脑袋设的它必须跟着数据分布走你在调 min_support 前先花两分钟看支持度分布能省掉后面大量的排错时间。5. 电影关联规则挖掘避坑记录热编码、冷门片与虚假规则5.1 现象一热编码后数据稀疏编码矩阵占满内存现象事务数只有几万条但编码后的 DataFrame 列数上万内存占用轻松超过几个 GB程序运行慢到像卡死。原因没有在编码前过滤掉低频电影。原始数据里可能包含大量“只被一个用户看过一次”的冷门片它们的列在布尔矩阵里几乎全是 False但对空间和计算时间的影响却是实实在在的。解决在构建事务列表前先统计每部电影的出现次数过滤掉出现次数低于某个阈值的电影。阈值可以按绝对用户数设比如至少被 10 个用户看过也可以按相对比例设比如不低于总用户数的 1%。from collections import Counter movie_counter Counter() for txn in transaction_list: movie_counter.update(set(txn)) # 保留至少被 10 个用户看过的电影 valid_movies {mid for mid, cnt in movie_counter.items() if cnt 10} transaction_list_filtered [ [mid for mid in txn if mid in valid_movies] for txn in transaction_list ] # 清除可能出现空事务的行 transaction_list_filtered [txn for txn in transaction_list_filtered if len(txn) 0]这里用 set 去重后再计数避免同一用户多次看同一部电影被重复累计。过滤后的 transaction_list_filtered 就是后续编码的输入这一步做完列数通常会减少一大半运行效率提升立竿见影。5.2 现象二冷门电影干扰候选集导致热闹的组合被淹没现象挖掘出的规则里有大量冷门文艺片和实验影片它们的前件后件组合都只有个位数用户支持业务上完全没法用。原因支持度阈值设得太低或者过滤逻辑只看了绝对数量没看相对比例。冷门片虽然不是最多的但数量庞大低阈值下它们会大量涌进来挤掉那些真正有商业价值的中间层内容。解决采用两级过滤。第一级按绝对出现用户数过滤长尾第二级在算频繁项集时按相对支持度再筛一次。两级过滤的目标不同第一级是为了控制编码矩阵规模第二级是为了控制规则输出质量。我一般把第一级阈值设在“至少 30 个用户看过”第二级阈值设在 0.02 到 0.05 之间具体看业务线。5.3 现象三置信度高但提升度接近1规则全是废话现象排序前 20 条规则全是“看了《热门片A》的人会看《热门片B》”置信度都挺高但提升度只有 1.0 左右换谁都看得出来这两部片本来就看的人多不需要挖掘也知道。原因置信度天然偏向高频后件。热门片因为基础概率高作为后件时几乎每条规则都表现良好实际上这是一种幸存者偏差。解决把筛选和排序分开。先用提升度过滤保留 lift 1.2 的规则再在剩余规则里按置信度排序。如果你想专门找冷门片带动热门片的组合可以再加条件前件支持度低于后件支持度。这种规则对内容冷启动运营特别有用因为你可以把冷门片推给对应热门片的观众。5.4 现象四评分等级混入事务导致同一电影被拆成多个项现象规则里出现“电影A评分5星”和“电影A评分4星”两个项它们作为独立项参与计算导致同一部电影的规则碎片化支持度被稀释。原因构建事务列表时把 rating 也拼在了项里。比如把“movieId:rating”作为一个复合项本意是区分高分和低分用户但实际效果是把一部电影拆成三四个项每个项的支持度都被拉低挖掘出来的规则既乱又难解释。解决事务里只保留电影 ID。评分等级的信息可以留到规则生成后再过滤比如只保留那些用户评分均值高于 4 的影片的规则。如果想分析特定评分群体的偏好可以在事务构建阶段先把评分低于 4 的行删掉再按用户聚合而不是把评分融进项目名称里。5.5 现象五未设置最小项集长度规则里全是单件项集现象规则输出里出现前件或后件为空的情况比如一条规则只有后件没有前件或者前件只含一部电影而它就是整个频繁项集本身。这类规则没有任何推荐价值。原因关联规则生成时任何一个频繁项集都会被拆分成左右两侧的规则。如果项集只有一部电影它只能生成一条“空前件”的规则这种规则在数学上代表“整部电影出现的概率”但放在“看A推荐B”的业务语境里毫无意义。解决规则生成后立刻过滤掉前件或后件为空的行并把后件长度限制为 1。前件长度建议控制在 1 到 2这样推荐理由可读性强前件超过 3 部电影时普通用户完全无法感知推荐逻辑。# 过滤前后件非空且后件只保留一个影片 rules rules[rules[antecedents].map(len) 0] rules rules[rules[consequents].map(len) 1] # 前件长度限制为 1 或 2 rules rules[rules[antecedents].map(len) 2]这一小段过滤代码我几乎每次都会写上。它能立刻清掉接近一半的无效规则让结果表变得正常许多。很多人忽略这一步最后拿着满是空前件规则的输出在研究半天这是典型的没读懂规则生成逻辑导致的冤枉时间。6. 让关联规则挖掘真正落地从规则结果到可用的观影偏好分析6.1 用效果验证规则而不是只信统计指标离线挖掘出的规则如果不经过验证很容易给你一种“好像很有道理”的错觉。我常用的验证方法是留一验证把用户行为按时间切成前后两段用前段数据挖掘规则再用后段数据检验规则是否成立。具体来说每条规则的后件电影在多少用户的真实后续行为中出现过这个命中率就是规则的业务价值参考。# 假设 train_transactions 是前段行为test_transactions 是后段行为 train_rules association_rules( fpgrowth(train_encoded, min_support0.03, use_colnamesTrue), metriclift, min_threshold1.2 ) hit_count 0 total_count 0 for txn in test_transactions: for _, rule in train_rules.iterrows(): ante set(rule[antecedents]) cons set(rule[consequents]) if ante.issubset(txn) and cons.issubset(txn): hit_count 1 if ante.issubset(txn): total_count 1 print(规则命中率:, hit_count / max(total_count, 1))这段代码逻辑很简单在测试事务里找那些满足前件条件的样本再看这些样本中有多少也满足后件条件。命中率越高规则在真实行为中的可复现性越强。注意这里没有考虑规则数量太多导致的重复计数所以我会在跑之前先做一次规则裁剪保留前 200 条置信度最高的。另一种验证方法是直接做人工核查。每次挖掘结果出来后抽取排序最靠前的 50 条规则让运营或内容团队标注“是否可推荐”。不需要多复杂就三个选项可用、不可用、说不好。跑一次下来你就能得到一批规则在业务侧的真实接受度这些标注结果反过来还能作为调整筛选参数的依据。6.2 一套可复制的最小落地流程我把电影关联规则挖掘的流程固定成了四个步骤每次换数据都照着跑一遍。第一步导数据限定字段范围和时间窗口第二步清洗并构建事务第三步用 FP-Growth 挖掘频繁项集并生成规则第四步输出规则表按业务分数排序。# 给规则加一个可调节的业务分方便运营侧排序 rules[business_score] ( rules[confidence] * rules[lift] * 0.8 ) rules rules.sort_values(business_score, ascendingFalse)业务分公式里的 0.8 是时间衰减系数你可以替换成更复杂的权重比如最近行为权重或内容质量分。这里要提醒一点不要追求把所有指标融进一个公式业务分的核心目标是让运营人员有一个稳定、可解释的排序依据而不是做一个黑匣子分数。规则本身的可解释性才是它最大的价值。流程跑熟之后你会发现真正的效率瓶颈往往不在算法上而在“规则生产”和“规则消费”之间缺一条通道。产出规则只是第一步怎么把规则推给下游推荐系统怎么生成运营专题才是投入产出比更高的环节。我目前的做法是把规则存成一张宽表键是前件电影 ID 集值是后件电影 ID 加规则指标下游业务按需查询可以做到每日更新一次。6.3 关联规则之外方向与迭代关联规则挖掘在电影数据上还能往两个方向延伸。第一个方向是引入内容属性把项从“电影ID”换成“类型”“导演”“演员”等维度比如挖掘“喜欢某导演的人还会关注哪些演员”“看某类型片的人还会看哪些导演的作品”。这类规则在一些电影库内容稀疏的新平台特别有用因为单纯靠电影 ID 做关联可能没有足够的共现数据但聚合到类型和创作者维度后数据就足够支撑有意义的分析。第二个方向是加时间维度规则变成“看完A之后的一周内看了B”。这属于序列模式挖掘的范畴但起步阶段你完全可以用关联规则打底先把时间窗口切割好比如以 7 天为一个窗口切分用户行为再在每个窗口内构建事务挖掘窗口内的共现规则。这个方法比直接上序列挖掘算法更直观而且结果很容易做对比分析识别出“短期搭配”和“长期偏好”的差异。我自己做了多次规则挖掘后最大的收获是先看负相关规则。提升度小于 1 的组合告诉你哪些内容不应该放在一起推荐这种信息在运营排布上比正向规则更稀缺。每次跑完规则我都会单独拉一屏负相关组合看看凡是那些统计上负相关但业务直觉上“好像有关系”的组合几乎都是埋着数据问题的线索。把这一步也做成日常习惯之后关联规则挖掘就成了我处理电影数据时最稳定的产出工具——它不追求预测准确率不依赖复杂的模型训练就是用最朴素的共现逻辑把用户行为中的结构清晰地暴露出来。希望这套流程能帮到你。本文还有配套的精品资源点击获取