简介:这份PDF文档围绕电子商务场景下的商品推荐问题,整理了一套基于关联规则的商品推荐系统模型,面向数据挖掘初学者、电商技术研究者以及需要撰写相关论文或课程设计的高校学生。内容从推荐系统的基本作用讲起,梳理了简单关联、时序关联与因果关联的分类,并重点介绍Apriori算法、基于划分的算法和FP-树频集算法三种频繁项集挖掘思路,随后给出从数据采集、预处理到规则库建立、模型引擎计算的完整推荐流程,附有模型结构示意图与Apriori算法的伪代码描述。资源包仅含1个PDF文件,约118KB,篇幅精炼,便于快速通读与重点查阅,适合作为关联规则学习与电商推荐模型搭建的入门参考。目前已有102人学习,读者可借此理清频繁项集、支持度与可信度等核心概念,掌握将关联规则落地为推荐列表的整体思路。
1. 关联规则做商品推荐,为什么在小数据量下依然打得过深度模型
一个日订单几千单的垂直电商,算法同学上来就搭双塔召回,训了两个月,线上 CTR 还没跑过运营手工配的"买了 A 的人还买了 B"。这类场景里,一瓶啤酒和一包纸尿裤之间的关系不需要 128 维向量去表达,一条{啤酒} → {纸尿裤}、支持度 2.1%、置信度 34%、提升度 2.8 的规则就够用了。关联规则挖掘(Association Rule Mining)本质是在事务数据集里找出「同时出现频率显著高于随机」的项集组合,再用置信度、提升度把它翻译成可执行、可解释、可运营干预的推荐理由。它不需要 GPU,增量成本低,冷启动商品只要被买过一次就有机会进规则,SKU 在几千到几万、订单在百万级以下的业务里性价比极高。这里要讲的是:怎么把一张订单明细表变成一个能上线的商品推荐模型,支持度/置信度/提升度这三组参数怎么定,规则怎么打分进召回通道,以及线上最容易在哪儿翻车。
2. 关联规则的三组核心参数:支持度、置信度与提升度怎么定
关联规则的全部可调空间几乎都压在三个指标上。很多人第一次跑apriori就卡在这里:min_support 设 0.1 出来几十条规则,设 0.001 出来几十万条,然后随手挑了几条看着顺眼的就上线了。这三个参数各自控制的是不同阶段,必须分开定,不能一起拍脑袋。
2.1 支持度:决定候选集规模的第一个阀门
支持度描述的是"这个组合在所有订单里出现的概率",分母是订单总数(去重后的 transaction 数),不是商品件数。
support(X) = 包含项集 X 的订单数 / 订单总数这个定义看起来简单,但分母搞错的人非常多:把明细行数当分母,算出来的支持度会系统性偏小,因为你把同一订单里的多行商品都算成了独立事务。正确做法是先按 order_id 聚合成"一单一个商品集合"。
支持度的量纲直接决定候选集大小。假设有 5000 个活跃 SKU,理论上的二元组合是 1250 万,Apriori 靠反单调性剪枝把它压下来,但压到什么程度完全取决于 min_support:
| 业务规模 | 单量级 | min_support 建议区间 | 预期频繁项集量级 |
|---|---|---|---|
| 垂直小店 | 日均 < 1 千单 | 0.005 ~ 0.02 | 10^3 ~ 10^4 |
| 中型电商 | 日均 1 万单 | 0.001 ~ 0.005 | 10^4 ~ 10^5 |
| 大促期间全量 | 日均 > 50 万单 | 0.0002 ~ 0.001 | 10^5 ~ 10^6 |
我的习惯是先跑一遍支持度分布,再回推阈值:把商品对按共现次数排序,看第 5000 对、第 20000 对分别落在哪个支持度上,取那个点作为 min_support。这样得到的规则量是可预期的,而不是"跑完再删"。
注意:min_support 每下降一半,频繁项集数量通常增长 3~5 倍,内存占用随之上升。用 8G 内存的机器跑 0.0005 以下的支持度之前,先确认一下数据规模。
2.2 置信度与提升度:把"买了又买"和"顺便买"分开
置信度回答的是"买了 X 的人里有多少买了 Y":
confidence(X → Y) = support(X ∪ Y) / support(X)它有个致命缺陷:如果 Y 本身是爆品,买什么都可能顺手带上它,置信度天然就高。比如"矿泉水"在 40% 的订单里出现,那{任意商品} → {矿泉水}的置信度都不会低于 0.2,这种规则没有任何推荐价值。
提升度修的就是这个问题:
lift(X → Y) = confidence(X → Y) / support(Y)lift = 1 表示 X 和 Y 相互独立,lift > 1 表示正相关。经验上,我一般把 lift 阈值卡在 1.2~1.5 之间:
- lift < 1.2:过滤掉,基本是噪声
- 1.2 ≤ lift < 3:主力规则池,稳定可解释
- lift ≥ 3:强关联,但数量少,很多是同一系列商品(手机和手机壳),要人工看一遍
除了 lift,还有两个补充指标值得算上。Kulc 度量 =(confidence(X→Y) + confidence(Y→X)) / 2,它对称,适合判断双向关联强度;不平衡比 IR =|support(X) - support(Y)| / (support(X) + support(Y) - support(X∪Y)),IR 越接近 1 说明两个商品的热度差距越大,这时候的高置信度往往是热门商品带出来的假象。把lift >= 1.3和IR <= 0.7组合起来过滤,规则池会干净很多。
2.3 用 Python 在订单表上跑通第一次频繁项集挖掘
数据从订单明细表出发,先把 order_id 和 item_id 两列抽出来,聚合、编码、挖掘、过滤四步走。下面是能直接抄的最小可运行版本。
import pandas as pd from mlxtend.preprocessing import TransactionEncoder from mlxtend.frequent_patterns import apriori, association_rules # 1. 读取订单明细,只需两列 orders = pd.read_csv("order_detail.csv")[["order_id", "item_id"]] # 2. 按订单聚合成事务集合,单笔订单内商品去重 baskets = orders.groupby("order_id")["item_id"].apply( lambda s: sorted(set(s.astype(str))) ).tolist() # 3. One-Hot 编码,得到 订单 x 商品 的布尔矩阵 te = TransactionEncoder() te_array = te.fit(baskets).transform(baskets) df = pd.DataFrame(te_array, columns=te.columns_) # 4. 频繁项集挖掘,max_len 控制规则长度 freq = apriori(df, min_support=0.002, use_colnames=True, max_len=3) print(f"频繁项集数量: {len(freq)}") # 5. 生成规则并按提升度过滤 rules = association_rules(freq, metric="lift", min_threshold=1.3) rules = rules[rules["confidence"] >= 0.15] rules = rules[rules["support"] >= 0.001] print(f"过滤后规则数量: {len(rules)}")逐行说明几个容易踩的点。第 2 步的set(s)不能省,同一订单里同一个 SKU 买了 3 件只应算一次事务出现,否则支持度会被重复计数扭曲。第 4 步的max_len=3很关键:不限制长度时,Apriori 会一直挖到没有频繁项集为止,四元、五元项集在几万 SKU 上的组合爆炸足以把内存打满;推荐场景里三元规则已经很少用得上,因为前件太长会导致覆盖用户太少。
association_rules返回的 DataFrame 一定包含这几个字段,后面接推荐系统全靠它们:
| 字段 | 含义 | 典型取值 |
|---|---|---|
| antecedents | 规则前件(frozenset) | {A, B} |
| consequents | 规则后件(frozenset) | {C} |
| support | 前件后件同时出现的订单占比 | 0.0021 |
| confidence | 前件出现时后件出现的概率 | 0.238 |
| lift | 相对随机共现的提升倍数 | 2.41 |
导出成 CSV 时记得把 frozenset 转成字符串,不然下游读回来还要再解析一遍。我一般再加一列rule_id = "|".join(sorted(antecedents)) + "=>" + "|".join(sorted(consequents))作为主键,方便做规则的增量比对和版本管理。
3. Apriori 与 FP-Growth 的实现选择:从订单表到规则库
模型跑通之后马上会遇到第二个问题:换一批数据、换一个时间窗口,同样一份代码要跑两三个小时。这时候该考虑换算法了。Apriori 和 FP-Growth 解决的是同一个问题,但代价结构完全不同,选错了会白白浪费机器。
3.1 数据准备:把订单流水转成事务矩阵
进入算法之前,清洗环节决定了最终规则的上限。几个常规动作:
- 剔除退款、取消、测试订单,用一个
order_status in ('paid', 'shipped', 'completed')的白名单,而不是黑名单 - 剔除只买了一次的用户(可选)。这类用户的订单占总量可能到 40%,保留会稀释支持度
- 商品粒度归一到 SKU,但在 SKU 极多时按"类目 + 品牌"上卷一层,得到更深、更稳的规则
- 时间窗口取最近 90 天,大流量业务取 30 天,因为商品生命周期在缩短
清洗完之后,把数据落成两列格式(order_id, item_id),每行是一个订单-商品对。这个中间态建议用 Parquet 存,一方面列存读得快,另一方面后续做增量更新时可以按日期分区,只重跑最近 N 天的数据。
3.2 Apriori 逐层剪枝的实现细节与性能拐点
Apriori 的核心是反单调性:如果一个项集是频繁的,那么它的所有子集也是频繁的。反过来,如果一个项集不频繁,那么它的所有超集都不频繁。这条性质让算法可以按长度逐层推进:
- 扫描一次数据,统计所有单项的支持度,得到 L1
- 由 L1 两两连接生成候选二元项集 C2,再扫一次数据计数,剪掉不满足 min_support 的,得到 L2
- 由 L2 生成 C3,剪枝,计数,得到 L3
- 重复直到某一层为空
每一层都要完整扫描一遍数据,这是它最贵的地方。在事务数为 100 万、频繁项集到 4 层时,我实测过大概要扫 5~6 遍全表,单机跑一次 40~90 分钟。
优化的抓手有三个。第一是降事务数,用采样或者缩短时间窗口;第二是提高 min_support,哪怕从 0.002 提到 0.003,候选集可能就少一半;第三是用更快的计数实现。mlxtend 的apriori内部已经用了 numpy 的布尔矩阵运算,比纯 Python 循环快得多,但它是单线程的。
注意:Apriori 生成的频繁项集结果里,同一长度的项集是按支持度降序排的,取 TopN 做规则时直接 head 就行,不要再排序一遍。
3.3 FP-Growth 建树与条件模式基,什么时候值得换
FP-Growth 换了个思路:不生成候选集,而是把数据压缩成一棵前缀树(FP-Tree),然后递归地在树上挖。
建树过程分三步:第一次扫描统计单项支持度,按降序排列每个事务里的商品;第二次扫描把每个事务按排序后的顺序插入树中,共享前缀路径只记一次,路径上的计数累加;同时维护一个头指针表,记录每个商品在树中出现的位置链。挖的时候从支持度最低的项开始,沿着它的节点链往上找所有祖先路径,得到"条件模式基",再在条件模式基上递归建树,直到路径为空。
换成 FP-Growth 只需要改一行:
from mlxtend.frequent_patterns import fpgrowth # 同样的 One-Hot 矩阵 df,只扫两遍数据 freq = fpgrowth(df, min_support=0.002, use_colnames=True, max_len=3)参数含义和 apriori 完全一致,输出结构也一致,所以下游代码不用动。区别在代价:FP-Growth 只扫两遍数据,建树之后全在内存里递归,理论复杂度远低于 Apriori。
那是不是无脑换?不是。三个场景下 FP-Growth 反而更慢:
| 场景 | 更优选择 | 原因 |
|---|---|---|
| 事务数万级、SKU 百级 | Apriori | 建树和递归开销大于收益 |
| min_support 很高(> 0.05) | Apriori | 候选集本身就小,扫几遍无所谓 |
| min_support 极低(< 0.0005) | Apriori | FP-Tree 分支爆炸,内存扛不住 |
| min_support 低、SKU 中等 | FP-Growth | 典型的树形收益区间 |
实操上我一般这样做:拿一周数据分别用两个算法在 min_support = 0.002、max_len = 3 下跑一遍,比一下耗时和内存,选快的那一个写进调度。因为数据分布会变,这不是一次性决策,每隔一两个月值得重跑一次这个对比。
4. 把规则接到推荐系统:召回、排序与线上打分
规则库挖出来只是半成品。{A, B} → {C}(支持度 0.0021,置信度 0.238,提升度 2.41)这种三元组直接丢给推荐接口,会遇到两个问题:同一个用户触发了多条规则、后件重复,需要合分;规则是按支持度排序的,不是按业务收益排序的。这一章解决从规则库到推荐结果之间的那段路。
4.1 规则库落库与增量更新策略
规则表建议按这个结构建,字段都来自association_rules的输出,再补两个业务字段:
CREATE TABLE rec_assoc_rule ( rule_id VARCHAR(512) PRIMARY KEY, -- "A|B=>C" 形式,天然去重 antecedent VARCHAR(512) NOT NULL, -- 前件,多个用 | 分隔 consequent VARCHAR(128) NOT NULL, -- 后件,目前只保留单商品 support DECIMAL(10,6) NOT NULL, confidence DECIMAL(10,6) NOT NULL, lift DECIMAL(10,6) NOT NULL, kulc DECIMAL(10,6), imbalance DECIMAL(10,6), item_cnt INT, -- 前件商品数,2 或 3 version VARCHAR(16), -- 批次号,如 20240610 created_at DATETIME DEFAULT CURRENT_TIMESTAMP, KEY idx_antecedent (antecedent), KEY idx_consequent (consequent) );idx_antecedent这个索引是线上查询的生命线。推荐接口拿到用户购物车里的商品集合后,要把集合的所有子集去规则表里查前件,二元的 3 个商品会展开成 7 个子集(含单项),三元的会展开成 15 个。索引没建好,这里就是全表扫。
增量更新我一般用"滑动窗口 + 全量重算"的折中方案:每天凌晨用最近 90 天数据全量跑一次,生成新版本号,写进临时表;比对新旧版本的规则差异,只把新增和指标变化超过 20% 的规则合并进主表;旧版本保留 7 天用于回滚。为什么不纯增量?因为支持度是会衰减的,一条规则去年支持度 0.01,今年可能已经降到 0.001,纯增量只会不断往表里加规则,永远不删。
4.2 从规则到 TopN 推荐列表的打分公式
同一个用户可能命中十几条规则,后件还可能重复。这时候需要一个统一打分函数把候选排出来。我常用的形式是加权乘积:
import math from collections import defaultdict def score_candidates(hit_rules, alpha=0.6, beta=0.4, decay_lambda=0.05): """ hit_rules: 命中的规则列表,每条是 dict: {'consequent': 'C', 'lift': 2.41, 'confidence': 0.238, 'age_days': 12} alpha/beta: lift 与 confidence 的权重,两者之和不必为 1 decay_lambda: 时间衰减系数,越大衰减越快 """ scores = defaultdict(float) for r in hit_rules: # 时间衰减:规则越老,权重越低 decay = math.exp(-decay_lambda * r["age_days"]) # 核心打分项:提升度与置信度的加权几何平均 base = (r["lift"] ** alpha) * (r["confidence"] ** beta) scores[r["consequent"]] += base * decay # 按分数降序取 TopN ranked = sorted(scores.items(), key=lambda x: x[1], reverse=True) return ranked[:20]几个参数的经济含义要说清楚。alpha 大于 beta 表示更看重"这条规则比随机强多少",适合新品曝光的场景;beta 大于 alpha 表示更看重"命中的概率有多大",适合转化率优先的场景。我一般从 alpha=0.6、beta=0.4 起调,在 AB 实验里对比 alpha=0.5/0.7 两组。
decay_lambda 控制规则的新鲜度。取 0.05 意味着一条 30 天前生成的规则权重衰减到 0.22,接近失效;取 0.01 则 30 天只衰减到 0.74。生鲜、服饰这类季节敏感品类建议 0.05~0.1,标准品、图书可以放宽到 0.01。
多个后件累加而不是取最大值,这个设计是有意为之的:一个商品被三条不同的规则同时指向,说明它在多个商品组合里都是合理的下一步,应该被推得更靠前。这是关联规则相比协同过滤的一个优势——它有天然的"理由叠加"。
4.3 与协同过滤、深度模型融合的边界
规则通道在任何推荐系统里都应该是召回层的一个通道,而不是排序层的最终决策。典型架构是:关联规则通道出 200 个候选,协同过滤出 200 个,热门兜底出 100 个,合并去重后交给粗排和精排。
如果非要在召回层就做融合,最稳妥的做法是给每个通道一个可调的加权分,权重用线上的 CTR 反推:
final_score = w_rule * norm(rule_score) + w_cf * norm(cf_score) + w_pop * norm(pop_score)这里的 norm 建议用 min-max 归一化到 [0,1],因为三个通道的原始分数量纲完全不同,规则分可能都在 0~5 之间,协同过滤的余弦相似度在 0~1 之间。归一化之后权重才有意义。
那什么时候该用深度模型替代规则?我的判断标准是:当用户侧特征和上下文特征(时段、设备、来源渠道)对推荐结果的影响超过商品共现关系时。规则模型只能回答"买了 A 的人还买什么",回答不了"这个用户在晚上 10 点用手机访问时应该看什么"。但即便如此,规则通道通常也不需要下线——它是最好的冷启动兜底和新品曝光工具,一个没有任何行为数据的新 SKU,只要被买过几次,就能通过规则获得曝光,而深度模型要等它积累足够的样本才能学好它的 embedding。
5. 规则质量验证与线上效果排错:几个容易踩到的坑
规则模型的坑不在算法,在数据和评估口径。
5.1 用时间切分验证,避免规则穿越
最常见的错误是:用全量 90 天数据挖出规则,再用同一批 90 天数据评估命中率。这时候的指标好看得离谱,因为规则本来就是从这批数据里长出来的。
正确做法是严格按时间切:用第 1~60 天的订单挖规则,用第 61~90 天的订单做测试集,统计规则在测试集上的命中情况。评估指标至少看两个:
| 指标 | 计算方式 | 参考基线 |
|---|---|---|
| HitRate@10 | 测试集中,前 10 个推荐里命中真实下一单商品的比例 | 8% ~ 20% |
| Coverage | 规则后件覆盖的 SKU 数 / 全部在售 SKU 数 | 5% ~ 30% |
| Rule Recall | 测试集订单中,能被至少一条规则覆盖的比例 | 15% ~ 40% |
Coverage 太低说明支持度卡太狠,只有头部商品能进规则;太低的同时 HitRate 很高,说明规则池在自我循环,推荐结果会越来越集中在少数商品上,长期会损害长尾商品的曝光。
5.2 三类高频翻车:通用品、长尾品、季节性
第一类是通用品污染。塑料袋、纸巾、矿泉水、赠品这些商品几乎每单都有,它们出现在大量规则的右边,把真正有价值的推荐挤下去了。处理方式是给商品算一个逆文档频率(IDF)式的权重,或者直接建黑名单:把出现在 30% 以上订单里的 SKU 全部从后件中剔除。这个动作对规则池的清理效果通常立竿见影,规则数可能砍掉 40%,但 CTR 会涨。
第二类是长尾商品永远进不了规则。一个只卖了 20 次的 SKU,在 100 万订单里支持度是 0.00002,任何合理的阈值都捞不到它。解决办法是做粒度上卷,把长尾 SKU 归到"类目-品牌-价格带"这个组合上,用组合去挖规则,再在组合内部按销量排序选具体商品。这样规则的前件是具体商品,后件是"类目-品牌-价格带",推荐时再下一层。
第三类是季节性失效。夏季挖出的"防晒霜→晒后修复"在冬天就是噪声。除了在 4.2 的打分里加时间衰减,还可以按季度分别维护规则池,接口根据当前日期选版本。这个做法成本不高,但对服饰、食品、户外类目效果明显。
5.3 上线前跑一个拦截率检查
最后补一个我自己每次都会跑的检查:拿最近 7 天的真实订单,模拟"用前 3 个商品推荐第 4 个"的场景,统计规则给出的推荐里,有多少是用户已经买过的。这个比例叫已购拦截率,超过 30% 就说明规则池里大量是"同单共现"而非"跨单复购",推荐列表里会塞满用户刚买完的东西。处理方式是在打分函数里给已购商品乘一个 0.1 的惩罚系数,或者干脆在候选阶段就过滤掉近 30 天内已购的 SKU。这一步做完,规则的推荐列表才算真正可用。
本文还有配套的精品资源,点击获取