小肥柴的Hadoop之旅 快速实验篇(B10)品类共现与关联规则实战
目录
- [1. 从购物篮分析说起](#1. 从购物篮分析说起)
- [2. 5 分钟速读](#2. 5 分钟速读)
- [3. 环境准备与数据概览](#3. 环境准备与数据概览)
- [4. 四步探测](#4. 四步探测)
- [4.1 探测一:购物篮规模](#4.1 探测一:购物篮规模)
- [4.2 探测二:品类频率分布](#4.2 探测二:品类频率分布)
- [4.3 探测三:两两共现矩阵](#4.3 探测三:两两共现矩阵)
- [4.4 探测四:共现熵](#4.4 探测四:共现熵)
- [5. 算法选型:从 Apriori 到 FP-Growth](#5. 算法选型:从 Apriori 到 FP-Growth)
- [5.1 Apriori 的核心思路](#5.1 Apriori 的核心思路)
- [5.2 Apriori 在稠密矩阵上的问题](#5.2 Apriori 在稠密矩阵上的问题)
- [5.3 FP-Growth 的由来](#5.3 FP-Growth 的由来)
- [5.4 FP-Growth 的核心:FP 树](#5.4 FP-Growth 的核心:FP 树)
- [5.5 FP-Growth 与 Apriori 的对比](#5.5 FP-Growth 与 Apriori 的对比)
- [5.6 B10 的算法决策](#5.6 B10 的算法决策)
- [6. 口径设计:购物篮、阈值、项集长度](#6. 口径设计:购物篮、阈值、项集长度)
- [7. 建表](#7. 建表)
- [7.1 ads_category_cooccurrence 建表](#7.1 ads_category_cooccurrence 建表)
- [7.2 ads_category_rules 建表](#7.2 ads_category_rules 建表)
- [8. 验证:15 项检查](#8. 验证:15 项检查)
- [9. 核心发现](#9. 核心发现)
- [9.1 发现一:购物篮平均 2.98 个品类](#9.1 发现一:购物篮平均 2.98 个品类)
- [9.2 发现二:共现矩阵完全稠密且均匀](#9.2 发现二:共现矩阵完全稠密且均匀)
- [9.3 发现三:没有任何 3-项集达到阈值](#9.3 发现三:没有任何 3-项集达到阈值)
- [9.4 发现四:提升度中位数低于 1.0](#9.4 发现四:提升度中位数低于 1.0)
- [9.5 发现五:4 条强规则的提升度全部低于 1.1](#9.5 发现五:4 条强规则的提升度全部低于 1.1)
- [9.6 结论](#9.6 结论)
- [10. 对分析方法的意义](#10. 对分析方法的意义)
- [10.1 提升度小于 1 不等于负相关](#10.1 提升度小于 1 不等于负相关)
- [10.2 关联规则挖掘的工程准则](#10.2 关联规则挖掘的工程准则)
- [11. 对后续实验的影响](#11. 对后续实验的影响)
- [12. 工程踩坑](#12. 工程踩坑)
- [13. 写在最后](#13. 写在最后)
- [14. 附录:完整脚本](#14. 附录:完整脚本)
1. 从购物篮分析说起
B10 的使命是做品类共现与关联规则分析,回答"哪些品类经常被一起买"。这是电商场景里很经典的一类分析,真实业务里最常见的应用是捆绑销售和搭配推荐。沃尔玛的"啤酒与尿布"就是这个领域的经典案例。
但在 B01 到 B09 的一路探测中,已经发现这份数据在多个维度上都呈现均匀分布。品类均匀、搜索词均匀、城市维度均匀。这些前置结论会直接影响 B10 的结果预期。所以 B10 的第一步不是直接跑关联规则,而是先探测共现矩阵的形状,看看它是否同样均匀。
探测结果确实是均匀的,B10 最终挖出来的规则里,最强的提升度也只有 1.074,只比随机组合高 7.4%。这是一个负面的业务结论,但它同时走通了关联规则挖掘的完整流程,并沉淀了一套工程方法论。这篇报告记录从探测到建表再到验证的完整过程。
2. 5 分钟速读
- 原始设想:品类共现与关联规则挖掘,找出可以用于捆绑销售的品类组合。
- 探测结论:购物篮平均 2.98 个品类,共现矩阵完全稠密(190 对全部有值),归一化熵 0.9996,高度均匀。
- 算法切换:最初计划用 Apriori,探测后发现共现矩阵稠密,改用 FP-Growth 避免候选集膨胀。
- 主要发现:380 条规则的提升度中位数 0.943,只有 4 条规则的提升度超过 1.05,无一超过 1.10。
- 业务结论:品类之间不存在可用于推荐的强关联规则。
- 可复用产出 :
ads_category_cooccurrence+ads_category_rules+ 15 项验证。
3. 环境准备与数据概览
所有操作都在 /home/hadoop/b10 目录下进行。先建目录结构:
bash
mkdir -p /home/hadoop/b10/{sql,py,output,docs}
cd /home/hadoop/b10
启动集群(如果没启):
bash
start-dfs.sh && start-yarn.sh
检查 Hive 能正常连上:
bash
hive -e "USE b_group; SHOW TABLES;" 2>&1 | tail -20
应该能看到 dwd_user_behavior 以及 B01 到 B09 留下的其他表。
每次写 HQL 脚本,开头必须带这一段 SET 参数,与 B08 和 B09 一致:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
客户端堆也要设,写进 ~/.bashrc:
bash
export HADOOP_CLIENT_OPTS="-Xmx512m -Xms256m"
这份数据是电商用户行为日志,180570 行,覆盖 11 天,从 2019-07-17 到 2019-07-27。每一行是一次用户行为。B10 的分析口径与 B01 到 B09 一致,全部排除 07-27,用 WHERE dt < '2019-07-27' 物理过滤。
DWD 表 dwd_user_behavior 里 B10 会用到的字段:
| 字段 | 类型 | 说明 |
|---|---|---|
| user_id | bigint | 用户 ID |
| session_id | string | 会话 ID |
| action_time | string | 行为时间 |
| behavior_type | string | click / order / pay / search |
| order_category_ids | array<string> | 下单品类 ID 列表 |
| pay_category_ids | array<string> | 支付品类 ID 列表 |
| raw_line_id | string | 原始行号 |
| dt | string | 分区日期 |
注意 order_category_ids 和 pay_category_ids 是数组类型,需要用 LATERAL VIEW explode 展开。raw_line_id 是每行行为的唯一标识,B10 用它作为购物篮的标识。
4. 四步探测
在写任何 ADS 表之前,先跑四组探测,把"能做什么"和"怎么做"钉死。
4.1 探测一:购物篮规模
第一个问题,一次下单行为里平均有几个品类。这直接决定了共现事件的丰富程度。
bash
cd /home/hadoop/b10 && hive -e "
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
SELECT 'P01_order_basket' AS probe_tag,
COUNT(*) AS basket_cnt,
SUM(SIZE(order_category_ids)) AS total_items,
AVG(SIZE(order_category_ids)) AS avg_items,
MIN(SIZE(order_category_ids)) AS min_items,
MAX(SIZE(order_category_ids)) AS max_items
FROM dwd_user_behavior
WHERE dt < '2019-07-27'
AND order_category_ids IS NOT NULL
AND SIZE(order_category_ids) > 0;
SELECT 'P02_pay_basket' AS probe_tag,
COUNT(*) AS basket_cnt,
SUM(SIZE(pay_category_ids)) AS total_items,
AVG(SIZE(pay_category_ids)) AS avg_items,
MIN(SIZE(pay_category_ids)) AS min_items,
MAX(SIZE(pay_category_ids)) AS max_items
FROM dwd_user_behavior
WHERE dt < '2019-07-27'
AND pay_category_ids IS NOT NULL
AND SIZE(pay_category_ids) > 0;
" 2>&1 | tee output/probe1.log
实测输出:
P01_order_basket 10887 32487 2.9840176357123176 1 5
P02_pay_basket 7458 22436 3.0083132207026013 1 5
两个维度购物篮规模几乎一致。下单平均 2.98 个品类,支付平均 3.01 个品类。最小 1 个,最大 5 个。没有异常大的购物篮,暂时没有刷单嫌疑。
这个数字非常关键。 平均 2.98 意味着一次行为里通常会买 3 个品类,共现事件充足。10887 个购物篮 × 平均 3 对品类组合,约 3 万次共现事件,Apriori 或 FP-Growth 有足够的数据基础。
4.2 探测二:品类频率分布
第二个问题,20 个品类里每个的出现频率是不是均匀。
bash
cd /home/hadoop/b10 && hive -e "
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
SELECT 'P03_order_cate' AS probe_tag, CAST(oc.cate_id AS bigint) AS cate_id,
COUNT(DISTINCT t.raw_line_id) AS basket_cnt
FROM dwd_user_behavior t
LATERAL VIEW explode(order_category_ids) oc AS cate_id
WHERE t.dt < '2019-07-27'
AND oc.cate_id IS NOT NULL AND oc.cate_id != ''
GROUP BY CAST(oc.cate_id AS bigint)
ORDER BY basket_cnt DESC;
SELECT 'P04_pay_cate' AS probe_tag, CAST(pc.cate_id AS bigint) AS cate_id,
COUNT(DISTINCT t.raw_line_id) AS basket_cnt
FROM dwd_user_behavior t
LATERAL VIEW explode(pay_category_ids) pc AS cate_id
WHERE t.dt < '2019-07-27'
AND pc.cate_id IS NOT NULL AND pc.cate_id != ''
GROUP BY CAST(pc.cate_id AS bigint)
ORDER BY basket_cnt DESC;
" 2>&1 | tee output/probe2.log
实测输出(截取):
P03_order_cate 5 1681
P03_order_cate 14 1657
P03_order_cate 16 1656
...
P03_order_cate 15 1532
P04_pay_cate 4 1170
P04_pay_cate 15 1165
P04_pay_cate 7 1162
...
P04_pay_cate 5 1053
下单维度最高 1681,最低 1532,最大最小比 1.097 。支付维度最高 1170,最低 1053,最大最小比 1.111。
两个维度的比值都接近 1.1,远低于"明显不均匀"的 2.0 阈值。20 个品类频率高度均匀,与 B04 的发现一致。
4.3 探测三:两两共现矩阵
第三个问题,任意两个品类同时出现在购物篮里的次数。
bash
cd /home/hadoop/b10 && hive -e "
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
WITH baskets AS (
SELECT DISTINCT t.raw_line_id, CAST(oc.cate_id AS bigint) AS cate_id
FROM dwd_user_behavior t
LATERAL VIEW explode(order_category_ids) oc AS cate_id
WHERE t.dt < '2019-07-27'
AND oc.cate_id IS NOT NULL AND oc.cate_id != ''
),
pairs AS (
SELECT a.cate_id AS cate_a, b.cate_id AS cate_b, a.raw_line_id
FROM baskets a
JOIN baskets b
ON a.raw_line_id = b.raw_line_id
AND a.cate_id < b.cate_id
)
SELECT cate_a, cate_b, COUNT(DISTINCT raw_line_id) AS cooccur_cnt
FROM pairs
GROUP BY cate_a, cate_b
ORDER BY cooccur_cnt DESC
LIMIT 60;
" 2>&1 | tee output/probe3.log
实测输出(截取):
7 13 269
5 13 266
2 6 257
5 7 255
4 6 254
...
前 60 对的共现次数全部落在 235 到 269 的窄区间里,极差只有 14%。与品类频率分布的均匀度几乎一致。
4.4 探测四:共现熵
第四个问题,用 B09 建立的熵方法给共现矩阵的均匀性做量化。
bash
cd /home/hadoop/b10 && hive -e "
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
WITH baskets AS (
SELECT DISTINCT t.raw_line_id, CAST(oc.cate_id AS bigint) AS cate_id
FROM dwd_user_behavior t
LATERAL VIEW explode(order_category_ids) oc AS cate_id
WHERE t.dt < '2019-07-27'
AND oc.cate_id IS NOT NULL AND oc.cate_id != ''
),
all_cates AS (
SELECT DISTINCT cate_id FROM baskets
),
all_pairs AS (
SELECT a.cate_id AS cate_a, b.cate_id AS cate_b
FROM all_cates a CROSS JOIN all_cates b
WHERE a.cate_id < b.cate_id
),
cooccur AS (
SELECT a.cate_id AS cate_a, b.cate_id AS cate_b,
COUNT(DISTINCT a.raw_line_id) AS cooccur_cnt
FROM baskets a
JOIN baskets b
ON a.raw_line_id = b.raw_line_id
AND a.cate_id < b.cate_id
GROUP BY a.cate_id, b.cate_id
),
full_pairs AS (
SELECT ap.cate_a, ap.cate_b, COALESCE(c.cooccur_cnt, 0) AS cooccur_cnt
FROM all_pairs ap
LEFT JOIN cooccur c ON ap.cate_a = c.cate_a AND ap.cate_b = c.cate_b
),
total AS (
SELECT SUM(cooccur_cnt) AS total_cnt, COUNT(*) AS pair_num FROM full_pairs
)
SELECT 'P05_pair_entropy' AS probe_tag,
ROUND(SUM(CASE WHEN fp.cooccur_cnt > 0
THEN -1.0 * fp.cooccur_cnt / t.total_cnt * LN(1.0 * fp.cooccur_cnt / t.total_cnt)
ELSE 0 END), 6) AS entropy,
ROUND(SUM(CASE WHEN fp.cooccur_cnt > 0
THEN -1.0 * fp.cooccur_cnt / t.total_cnt * LN(1.0 * fp.cooccur_cnt / t.total_cnt)
ELSE 0 END) / LN(t.pair_num), 6) AS entropy_norm,
SUM(CASE WHEN fp.cooccur_cnt = 0 THEN 1 ELSE 0 END) AS zero_pair_cnt,
MIN(fp.cooccur_cnt) AS min_cnt,
MAX(fp.cooccur_cnt) AS max_cnt,
t.total_cnt, t.pair_num
FROM full_pairs fp, total t
GROUP BY t.total_cnt, t.pair_num;
" 2>&1 | tee output/probe4.log
实测输出:
P05_pair_entropy 5.24501 0.999616 0 192 269 43137 190
| 指标 | 值 | 判读 |
|---|---|---|
| entropy | 5.24501 | 实际熵 |
| entropy_norm | 0.999616 | ≥ 0.99,完全均匀 |
| zero_pair_cnt | 0 | 190 对全部有值,无零共现 |
| min_cnt | 192 | 最低共现次数 |
| max_cnt | 269 | 最高共现次数 |
| total_cnt | 43137 | 190 对共现事件总数 |
| pair_num | 190 | 全部品类对 |
两个关键数字:
zero_pair_cnt = 0 ,190 对品类对全部有共现记录,没有一对是零。这是一个完全稠密的矩阵。
entropy_norm = 0.999616,远高于 0.99 阈值。共现分布高度均匀,甚至比单个品类的分布更均匀(单品类熵 0.9993,共现熵 0.9996)。原因是共现计数是每个品类频率的乘积,乘积会把单个维度上的轻微不均匀平滑掉。
到这里,探测已经把结论钉死了:这是一份完全稠密且高度均匀的共现矩阵。任何关联规则挖掘算法在这种矩阵上都不会挖出强规则。B10 的核心价值不在于发现规则,而在于走通流程并沉淀方法论。
5. 算法选型:从 Apriori 到 FP-Growth
探测的结论直接引出算法选型的问题。原始设计打算用 Apriori,因为它是教科书里最经典的关联规则挖掘算法。但探测四显示共现矩阵完全稠密,这对 Apriori 是一个坏消息。
5.1 Apriori 的核心思路
Apriori 的核心是先验性质,也叫向下封闭性:
一个项集如果是频繁的,那它的所有子集也是频繁的。
反过来,一个项集如果任何子集不频繁,那它一定不频繁。
这个性质让 Apriori 可以做剪枝。生成候选 k-项集时,只要它的任何 (k-1)-子集不频繁,就直接丢弃,不用扫描数据算支持度。
算法流程:
- 扫描所有购物篮,统计每个品类的支持度,保留 ≥ min_support 的品类作为频繁 1-项集。
- 迭代生成频繁 k-项集。每一轮做三件事:
- 连接:把两个频繁 (k-1)-项集连接成候选 k-项集
- 剪枝:检查候选 k-项集的所有 (k-1)-子集是否频繁,不满足就丢弃
- 计数:对保留下来的候选扫描数据,算支持度
- 重复直到没有新的频繁项集。
- 从频繁项集生成关联规则,算置信度和提升度。
三个核心指标:
支持度:一个品类组合在所有购物篮里出现的比例。
Support(X) = 包含 X 的购物篮数 / 总购物篮数
置信度:在包含 A 的购物篮里,同时包含 B 的比例。
Confidence(A → B) = Support(A 和 B 同时出现) / Support(A)
提升度:置信度除以 B 自身的支持度。
Lift(A → B) = Confidence(A → B) / Support(B)
提升度衡量的是 A 出现之后 B 出现的概率,比 B 自然出现的概率高多少倍。提升度等于 1 表示两者独立,大于 1 表示正相关,小于 1 表示负相关。
5.2 Apriori 在稠密矩阵上的问题
Apriori 有一个结构性弱点:它每一轮都要生成并存储候选集,每一轮都要扫一遍原始数据。
扫描次数等于最大项集的长度。挖 3-项集要扫 3 遍,挖 5-项集要扫 5 遍。
候选集的数量更麻烦。20 个品类的候选集规模:
| 项集长度 | 候选数 |
|---|---|
| 2-项集 | 190 |
| 3-项集 | 1140 |
| 4-项集 | 4845 |
| 5-项集 | 15504 |
如果共现矩阵稀疏,剪枝能砍掉大部分候选。但 B10 的矩阵完全稠密,190 对全部有值,剪枝几乎砍不掉任何东西。3-项集、4-项集的候选集会迅速膨胀,2GB 的 worker 节点有 OOM 风险。
这就是原始设计与探测发现之间的冲突。矩阵稠密,Apriori 扛不住。
5.3 FP-Growth 的由来
2000 年,Jiawei Han、Jian Pei、Yiwen Yin 三位研究者在 ACM SIGMOD 会议上发表了一篇论文,标题是 "Mining Frequent Patterns without Candidate Generation"。这篇论文提出了 FP-Growth。
论文标题里的关键词是 without candidate generation,不用生成候选集。这是 FP-Growth 与 Apriori 最本质的区别。
Jiawei Han 是数据挖掘领域的知名学者,他后来还写了经典的《Data Mining: Concepts and Techniques》教材。FP-Growth 从那篇论文开始,逐渐成为关联规则挖掘的主流算法。今天工业界的购物篮分析、推荐系统,用的大多是 FP-Growth 或者它的变体。
5.4 FP-Growth 的核心:FP 树
FP-Growth 不用候选集,靠的是一种叫 FP 树的数据结构。FP 树是一棵前缀树,把所有购物篮压缩成一棵树,共享相同的前缀。
一个例子看懂 FP 树。假设有五个购物篮:
B1: {A, B, C}
B2: {A, C}
B3: {A, B}
B4: {B, C}
B5: {A, B, C}
第一步,数每个品类出现的次数:A=4,B=4,C=4。
第二步,每个购物篮内部按次数降序排列,得到:
B1: A, B, C
B2: A, C
B3: A, B
B4: B, C
B5: A, B, C
第三步,逐个购物篮插入 FP 树,每个节点记一个计数。
插入 B1 {A, B, C}:
root
└── A(1)
└── B(1)
└── C(1)
插入 B2 {A, C}:
root
└── A(2) ← A 计数 +1
├── B(1)
│ └── C(1)
└── C(1) ← 新的分支
插入 B3 {A, B}:
root
└── A(3)
├── B(2) ← B 计数 +1
│ └── C(1)
└── C(1)
插入 B4 {B, C}:
root
├── A(3)
│ ├── B(2)
│ │ └── C(1)
│ └── C(1)
└── B(1) ← 新的根分支
└── C(1)
插入 B5 {A, B, C}:
root
├── A(4)
│ ├── B(3)
│ │ └── C(2) ← C 计数 +1
│ └── C(1)
└── B(1)
└── C(1)
最终这棵树就是 FP 树。整个数据集的 5 个购物篮被压缩成一棵树,只占了几个节点。B1、B5 都是 {A, B, C},它们共享了 A→B→C 这条路径,第二个购物篮只让计数加一,没有产生新的节点。这就是 FP 树的压缩能力。
从 FP 树挖频繁项集。FP 树建好之后,挖频繁项集不用再扫原始数据,直接在这棵树上递归。
以 C 为例。找到 C 在树上的所有出现位置,沿着父指针往上走,得到 C 的所有前缀路径:
- A(4) → B(3) → C(2):计数 2
- A(4) → C(1):计数 1
- B(1) → C(1):计数 1
以 C 为后缀,看它的条件模式基能不能构成新的条件 FP 树。前缀路径里 A 出现 3 次,B 出现 3 次。假设 min_support 是 2,A 和 B 都频繁,可以形成条件 FP 树。C 与 A、B 的组合 {A, C}、{B, C}、{A, B, C} 都是频繁项集。
整个过程扫描原始数据只有两次。一次数频次,一次建树。之后所有挖掘都在树上递归进行,不再扫原始数据。
5.5 FP-Growth 与 Apriori 的对比
| 维度 | Apriori | FP-Growth |
|---|---|---|
| 候选集 | 每轮生成并存储 | 不生成 |
| 扫描数据次数 | 最大项集长度次 | 2 次 |
| 内存占用 | 候选项集膨胀时急剧上升 | 由 FP 树大小决定,稳定 |
| 时间复杂度 | 稀疏矩阵快,稠密矩阵慢 | 稠密矩阵优势明显 |
| 实现复杂度 | 简单 | 复杂(要建树和递归) |
| 输出结果 | 频繁项集 | 完全相同 |
| 适用场景 | 稀疏数据、小项集 | 稠密数据、长项集 |
关键点 :同样的 min_support 下,FP-Growth 和 Apriori 输出的频繁项集完全相同。区别只在执行效率。这不是两种不同的算法,而是两种不同的算法路径,解决同一个问题。
5.6 B10 的算法决策
原始设计用 Apriori,因为它是入门教程的标准算法,支持度、置信度、提升度三个指标的定义都从它引出。
探测发现的工程风险:探测四显示共现矩阵完全稠密,190 对全部有共现,归一化熵 0.9996。Apriori 的候选集在稠密矩阵下会迅速膨胀,2GB 的 worker 节点有 OOM 风险。
最终决策:改用 FP-Growth。输出结果与 Apriori 完全一致,扫描数据次数从"最大项集长度次"降到 2 次。
工程代价 :几乎为零。mlxtend 库的 fpgrowth 函数与 apriori 函数调用方式一致,切换只需改一行代码。
这是一个典型的工程妥协:算法思想从 Apriori 讲,实际执行用 FP-Growth。报告里要写清楚这个决策过程,不能让读者误以为实际用了 Apriori。
6. 口径设计:购物篮、阈值、项集长度
探测和算法都定下来了。在动手建表之前,有三个口径必须先钉死。
- 第一,购物篮的唯一标识用什么。
用 raw_line_id。DWD 表里 raw_line_id 是原始行号,天然唯一。之前考虑过用 user_id + session_id + action_time 三元组,但同一用户同一 session 同一秒可能有多次行为,三元组不唯一,会导致共现计数虚高。raw_line_id 没有这个问题。
- 第二,购物篮内品类要不要去重。
要。order_category_ids 是数组字段,理论上一次下单里同一个品类可能出现多次。如果不去重,explode 后一个品类会展开成多行,共现计数会被放大。SQL 里用 SELECT DISTINCT raw_line_id, cate_id 做去重。
- 第三,支持度阈值和项集长度上限设多少。
B10 的平均购物篮大小是 2.98。品类频率最高的是 1681/10887 = 0.154,最低的是 1532/10887 = 0.141。设 min_support = 0.015,也就是至少 163 个购物篮包含该品类组合,能保住所有 2-项集候选。
max_len = 3。理由:平均购物篮大小 2.98,意味着一半以上的购物篮里只有 2 到 3 个品类。4-项集在实际数据里出现的次数很少,挖出来也是噪声。限制到 3 之后,候选集从 2 万个压缩到 1330 个。
min_confidence = 0.1,先看全部规则。min_lift = 1.05,只保留微弱正相关的规则。
7. 建表
B10 产出两张 ADS 表,ads_category_cooccurrence 记录城市和品类的交叉分布,ads_category_rules 记录关联规则。前者在 Hive 里建,后者在 Python 里产出后写回 Hive。
7.1 ads_category_cooccurrence 建表
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=32;
SET mapreduce.map.memory.mb=512;
SET mapreduce.map.java.opts=-Xmx384m;
SET mapreduce.reduce.memory.mb=768;
SET mapreduce.reduce.java.opts=-Xmx512m;
SET hive.exec.reducers.max=1;
USE b_group;
DROP TABLE IF EXISTS ads_category_cooccurrence;
CREATE TABLE ads_category_cooccurrence (
dimension string,
cate_a bigint,
cate_b bigint,
cooccur_cnt bigint,
support_ab double,
support_a double,
support_b double,
confidence_ab double,
confidence_ba double,
lift double,
is_strong boolean
) STORED AS ORC;
WITH baskets AS (
SELECT DISTINCT t.raw_line_id, CAST(oc.cate_id AS bigint) AS cate_id
FROM dwd_user_behavior t
LATERAL VIEW explode(order_category_ids) oc AS cate_id
WHERE t.dt < '2019-07-27'
AND oc.cate_id IS NOT NULL AND oc.cate_id != ''
),
cate_support AS (
SELECT cate_id, COUNT(DISTINCT raw_line_id) AS single_cnt
FROM baskets GROUP BY cate_id
),
total_basket AS (
SELECT COUNT(DISTINCT raw_line_id) AS total_cnt FROM baskets
),
pairs AS (
SELECT a.cate_id AS cate_a, b.cate_id AS cate_b,
COUNT(DISTINCT a.raw_line_id) AS cooccur_cnt
FROM baskets a JOIN baskets b
ON a.raw_line_id = b.raw_line_id AND a.cate_id < b.cate_id
GROUP BY a.cate_id, b.cate_id
)
INSERT OVERWRITE TABLE ads_category_cooccurrence
SELECT
'order' AS dimension,
p.cate_a, p.cate_b, p.cooccur_cnt,
p.cooccur_cnt * 1.0 / t.total_cnt AS support_ab,
ca.single_cnt * 1.0 / t.total_cnt AS support_a,
cb.single_cnt * 1.0 / t.total_cnt AS support_b,
p.cooccur_cnt * 1.0 / ca.single_cnt AS confidence_ab,
p.cooccur_cnt * 1.0 / cb.single_cnt AS confidence_ba,
p.cooccur_cnt * t.total_cnt * 1.0 / (ca.single_cnt * cb.single_cnt) AS lift,
CASE WHEN p.cooccur_cnt * 1.0 / t.total_cnt >= 0.015
AND p.cooccur_cnt * 1.0 / ca.single_cnt >= 0.1
AND p.cooccur_cnt * t.total_cnt * 1.0 / (ca.single_cnt * cb.single_cnt) >= 1.05
THEN true ELSE false END AS is_strong
FROM pairs p
JOIN cate_support ca ON p.cate_a = ca.cate_id
JOIN cate_support cb ON p.cate_b = cb.cate_id
CROSS JOIN total_basket t;
执行后 ads_category_cooccurrence 有 190 行。跑完看日志末尾有没有 OK 和 Time taken。
7.2 ads_category_rules 建表
这张表的目标结构:
sql
CREATE TABLE ads_category_rules (
dimension string,
antecedent bigint,
consequent bigint,
support_rule double,
confidence double,
lift double,
is_strong boolean
) STORED AS ORC;
这张表的数据不由 Hive 直接产出,而是由 Python 端 FP-Growth 挖掘后写回。写回的过程在本地完成后,分三步走。
第一步 ,把 Python 结果 rules_simple_noheader.csv 传到 master 的 /home/hadoop/b10/output/ 下。文件格式是 tab 分隔的 5 列:antecedent, consequent, support, confidence, lift,无表头,380 行。
第二步,建一个文本中转表,用 LOAD DATA 把 CSV 载入。
sql
USE b_group;
DROP TABLE IF EXISTS tmp_rules_load;
CREATE TABLE tmp_rules_load (
antecedent bigint,
consequent bigint,
support double,
confidence double,
lift double
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
STORED AS TEXTFILE;
LOAD DATA LOCAL INPATH '/home/hadoop/b10/output/rules_simple_noheader.csv'
INTO TABLE tmp_rules_load;
第三步 ,从文本表 INSERT 到 ORC 表,补齐 dimension 和 is_strong 两个字段。
sql
INSERT OVERWRITE TABLE ads_category_rules
SELECT
'order' AS dimension,
antecedent, consequent,
support AS support_rule,
confidence, lift,
CASE WHEN support >= 0.015 AND confidence >= 0.1 AND lift >= 1.05
THEN true ELSE false END AS is_strong
FROM tmp_rules_load;
- 这里踩了一个坑 :
ads_category_rules是 ORC 表,LOAD DATA只能把文本文件载入文本表,直接 LOAD 到 ORC 表会报The file that you are trying to load does not match the file format of the destination table。这是 Hive 的硬约束,ORC 表必须用INSERT写入。所以要先建一张文本中转表,LOAD 到中转表,再从文本表 INSERT 到 ORC 表。
数据写完后 ads_category_rules 是 380 行,每行 7 个字段。
8. 验证:15 项检查
数据交付前跑 15 项验证。分成 7 组。
第一组验证规则表结构。规则总数应该是 380,强规则数应该是 4,去重后的品类对数应该是 190。
第二组验证字段范围。lift 应该在 0.79 到 1.08 之间,support 应该在 0.0176 到 0.0247 之间,confidence 应该在 0.118 到 0.163 之间。
第三组验证对称性。lift(A → B) 和 lift(B → A) 数学上相等,190 对应该全部通过。
第四组列出强规则清单。
第五组验证跨表一致性。ads_category_cooccurrence 190 行,ads_category_rules 380 行,后者是前者的两倍,因为每对品类有两条方向相反的规则。
第六组验证 Top1 和 Bottom1。Top1 应该是 7→13,lift 1.0738。Bottom1 应该是 16→4,lift 0.7946。
第七组验证分布。前件去重 20 个,后件去重 20 个,全部品类都当过前件和后件。
验证过程中踩了两个 SQL 坑:
V03 用 COUNT(DISTINCT CONCAT(antecedent, '-', consequent)) 去重,7-13 和 13-7 被当作不同字符串,结果是 380 而不是 190。修正方式是用 LEAST/GREATEST 把每对归一化:
sql
COUNT(DISTINCT CONCAT(CAST(LEAST(antecedent, consequent) AS STRING), '-',
CAST(GREATEST(antecedent, consequent) AS STRING)))
V07 用 a.lift = b.lift 判断对称性,lift 是 double 类型,ORC 存储后精度有微小差异,逐位严格相等会漏掉一半。修正方式是加浮点容差:
sql
ABS(a.lift - b.lift) < 1e-9
修正后 15 项全部通过。
| 编号 | 验证项 | 实测值 | 判定 |
|---|---|---|---|
| V01 | 规则总数 | 380 | ✅ |
| V02 | 强规则数 | 4 | ✅ |
| V03 | 去重规则对 | 190 | ✅ |
| V04 | lift 范围 | 0.7946, 1.0738,均值 0.9368 | ✅ |
| V05 | support 范围 | 0.0176, 0.0247 | ✅ |
| V06 | confidence 范围 | 0.118, 0.163 | ✅ |
| V07 | lift 对称性 | 190/190 | ✅ |
| V08 | 强规则列表 | 7↔13、2↔6 共 4 条 | ✅ |
| V09 | 跨表行数 | cooc 190 / rules 380 | ✅ |
| V10 | Top1 规则 | 7→13,lift 1.074 | ✅ |
| V11 | Bottom1 规则 | 16→4,lift 0.795 | ✅ |
| V12 | lift ≥ 1.05 数 | 4 | ✅ |
| V13 | lift < 1.0 数 | 338 | ✅ |
| V14 | 前件去重 | 20 | ✅ |
| V15 | 后件去重 | 20 | ✅ |
9. 核心发现
9.1 发现一:购物篮平均 2.98 个品类
探测一显示 10887 个下单购物篮,平均 2.98 个品类,最大 5 个。这个数字决定了共现事件的丰富程度。
平均 2.98 意味着一次下单通常会买 3 个品类。这个数字在真实电商里偏高。真实场景里一次下单平均 1.5 到 2 个品类是常见的。3 个说明这份数据的购物篮比真实数据更"饱满"。
9.2 发现二:共现矩阵完全稠密且均匀
探测四显示 190 对品类对全部有共现,zero_pair_cnt = 0。共现矩阵完全稠密,没有任何一对从未同时出现。
归一化熵 0.999616,远高于 0.99 阈值。共现分布高度均匀。B10 的品类共现结构在数学上就是均匀分布。
这个数字比 B09 的城市维度均匀度还要高(B09 归一化熵约 0.9993)。原因是共现计数是品类频率的乘积,乘积会把单个维度上的轻微不均匀平滑掉。
9.3 发现三:没有任何 3-项集达到阈值
FP-Growth 的输出显示:
频繁项集总数:210
按项集长度分布:
length
1 20
2 190
频繁 3-项集是 0 个。
这说明没有任何三个品类的组合在 10887 个购物篮里出现过 163 次以上。平均购物篮大小 2.98,意味着一半以上的购物篮只有 2 个或 3 个品类。要 3 个品类同时出现,购物篮至少要有 3 个品类。在均匀分布下,这种组合的支持度天然低于 0.015 阈值。
这是 B10 最重要的一个结构性发现:这份数据的购物篮结构天然限制了长项集的挖掘。
9.4 发现四:提升度中位数低于 1.0
380 条规则的 lift 分布:
| 指标 | 值 |
|---|---|
| count | 380 |
| mean | 0.9368 |
| std | 0.0542 |
| min | 0.7946 |
| 25% | 0.8960 |
| 50% | 0.9428 |
| 75% | 0.9781 |
| max | 1.0738 |
中位数 0.9428,低于 1.0。 超过一半的品类对 lift < 1。
这个数字初看会以为"这些品类对互相排斥"。但如果这样解读就错了。lift < 1 是购物篮容量约束下的正常现象,不是负相关。
举一个极端的数学例子。假设 20 个品类完全均匀,每个购物篮固定装 3 个品类,共 1000 个购物篮。每个品类的出现次数是 150,出现概率 0.15。如果两个品类独立,共现概率应该是 0.15 × 0.15 = 0.0225。但实际共现概率是多少?
3 个品类的购物篮总组合数是 C(20,3) = 1140 种。每种组合出现的次数是 1000/1140 ≈ 0.88。任意两品类同时出现的组合数是 C(18,1) = 18 种。所以这两品类同时出现的购物篮数是 18 × 0.88 ≈ 15.8,共现概率 0.0158。
比较:独立假设 0.0225 vs 实际 0.0158。lift = 0.0158 / 0.0225 = 0.70。
所以 lift < 1 是购物篮容量约束下的必然结果。 B10 的 lift 中位数 0.9428 比这个极端的 0.70 高,因为购物篮大小是 2.98 而不是 3.0,且分布不是完美均匀。但方向是一致的。
这个解释必须写进报告。否则读者看到 lift 中位数小于 1,会误以为品类之间存在普遍排斥。
9.5 发现五:4 条强规则的提升度全部低于 1.1
按照 lift ≥ 1.05 的标准,380 条规则里只有 4 条强规则:
| 前件 | 后件 | support | confidence | lift |
|---|---|---|---|---|
| 7 | 13 | 0.024708 | 0.162440 | 1.0738 |
| 13 | 7 | 0.024708 | 0.163327 | 1.0738 |
| 2 | 6 | 0.023606 | 0.157862 | 1.0524 |
| 6 | 2 | 0.023606 | 0.157379 | 1.0524 |
最强的 7↔13 对 lift = 1.0738,只比独立假设高 7.4%。这个强度在真实电商里不能作为推荐依据。真实电商的强关联规则(比如啤酒-尿布、手机-手机壳)lift 通常在 2.0 以上。
2↔6 对 lift = 1.0524,只比独立假设高 5.2%。这个差距在统计上可能只是采样噪声。
如果放宽阈值到 lift ≥ 1.02,规则数增加到 20 条。如果放宽到 lift ≥ 1.00,规则数增加到 42 条。但即使放宽到最宽松的阈值,最强的规则 lift 也只有 1.0738。这份数据里不存在真正有业务价值的强关联规则。
9.6 结论
在一百个用户的样本下,品类之间不存在可用于推荐的强关联规则。B10 的产出价值不在于发现规则,而在于三件事。
-
第一,走通了关联规则挖掘的完整流程,从探测、口径、算法选型到建表、验证。
-
第二,用 FP-Growth 替代 Apriori 解决了稠密矩阵的工程问题,沉淀了一套算法选型的判断准则。
-
第三,用 lift 中位数低于 1 这个现象,讲清楚了一个容易被误读的统计事实:购物篮容量约束下,lift < 1 是正常的,不代表负相关。
10. 对分析方法的意义
10.1 提升度小于 1 不等于负相关
这是 B10 最重要的一个方法论提醒。
lift(A → B) = Confidence(A → B) / Support(B)。
这个公式看起来是在比较"给定 A 时 B 出现的概率"和"B 自然出现的概率"。如果 lift < 1,看起来是 A 出现时 B 反而不容易出现。但在固定容量的购物篮里,这个解读是错的:
- (1)假设每个购物篮恰好装 k 个品类,共有 N 个品类。任意品类出现概率是 k/N。如果两个品类独立,共现概率应该是 (k/N)²。但实际上,任意两个品类同时出现,需要占用购物篮的两个位置,剩下 k-2 个位置由其他 N-2 个品类填充。实际共现概率是 k/N × (k-1)/(N-1)。
- (2)当 k << N 时,这个实际概率明显低于 (k/N)²。
- 代入 B10 的数字:k = 2.98,N = 20。独立假设是 (2.98/20)² = 0.0222。实际是 2.98/20 × 1.98/19 = 0.0155。比值 0.0155 / 0.0222 = 0.70。所以即使两个品类完全独立,lift 也会是 0.70 左右。
所以 lift < 1 在这个场景下是几何约束,不是业务信号。
如果要用 lift 判断品类关联,需要先做容量修正。修正方式是除以"随机组合的期望 lift"。这个修正后的指标叫归一化 lift,B10 没有引入,留作方法论拓展方向。
10.2 关联规则挖掘的工程准则
B10 沉淀了三条工程准则:
-
准则一,先探测再选算法。 稀疏矩阵用 Apriori,稠密矩阵用 FP-Growth。判断矩阵稀疏还是稠密,看
zero_pair_cnt和归一化熵两个指标。zero_pair_cnt = 0且归一化熵 ≥ 0.99,就是完全稠密。 -
准则二,根据购物篮规模限制项集长度。 平均购物篮大小 2.98,max_len 就设 3。购物篮平均只有 3 个品类,4-项集出现的概率天然很低,挖出来也是噪声。用 max_len 限制项集长度,能把候选集缩小一个数量级。
-
准则三,lift 解读要看购物篮容量。 不能简单地把 lift < 1 当作负相关。在固定容量购物篮里,lift 的中位数天然低于 1。要用"归一化 lift"或者"只看 lift 最高的几条规则"来解读。
11. 对后续实验的影响
-
B11 用户 RFM 与聚类分群。 城市维度不可用,品类维度也不携带强关联信号。B11 的 RFM 特征只从用户行为本身提取,包括最近活跃天数、行为频率、支付产品数代理。一百个用户的样本本来就小,规则分层之后每层的用户数会更少,K-means 聚类稳定性需要专门评估。B10 的经验是:在动手分群之前先探测,确认用户行为本身是否有区分度。
-
B12 异常流量与刷单识别。 B10 给 B12 提供了三个正常基线。第一,城市维度上的分布完全均匀。第二,品类共现矩阵完全稠密且均匀。第三,购物篮平均 2.98 个品类,最大 5 个,没有异常大的购物篮。B12 的三种方法(卡方检验、Z 分数、孤立森林)都要在这些基线上做校准,避免误报。
-
B13 用户行为路径挖掘。 B07 已经发现 n-gram 条件概率均匀,路径无预测力。B10 的结论进一步印证:这份数据的多个维度都呈现均匀分布。B13 用马尔可夫链和路径聚类重新挖,目标是找到 B07 没发现的模式。城市和品类维度都不可用,B13 只从 session 内的行为序列本身挖掘。
-
B14 用户画像与标签体系。 城市维度不能作为用户画像的特征维度,品类关联也不携带区分度。B14 的特征工程只从行为维度提取,包括行为频率、活跃时段、session 长度、转化率、搜索偏好、品类分布。
-
B15 品类推荐系统。 B10 的结论直接否定了基于关联规则的推荐路径。协同过滤和内容推荐都要重新评估。一百个用户的用户-品类交互矩阵本身就稀疏,加上品类共现均匀,推荐的区分度会很低。
12. 工程踩坑
B10 踩到四个新坑。
-
第一个坑,
LOAD DATA不能载入 ORC 表。 最初直接用LOAD DATA LOCAL INPATH ... INTO TABLE ads_category_rules,报The file that you are trying to load does not match the file format of the destination table。原因是 ORC 表必须用 INSERT 写入。解决办法是建一张文本中转表,先 LOAD 到中转表,再从文本表 INSERT 到 ORC 表。 -
第二个坑,
cooccurrence.csv导出时首行是空行。wc -l显示 191 行而不是 190。grep -vn '^[0-9]'定位到第 1 行是空行。原因是hive -e输出时首行多输出了一个空行。解决办法是用tail -n +2删掉首行。 -
第三个坑,浮点数严格相等比较会漏项。 V07 验证 lift 对称性时用
a.lift = b.lift,结果只通过了 98/190。原因是 double 类型在 ORC 存储后精度有微小差异。解决办法是加浮点容差ABS(a.lift - b.lift) < 1e-9。 -
第四个坑,去重字符串拼接会区分方向。 V03 验证去重规则对时用
CONCAT(antecedent, '-', consequent),7-13和13-7被当作不同字符串,结果是 380 而不是 190。解决办法是用LEAST/GREATEST把每对归一化。
此外 B10 沿用了一些 B01 到 B09 已经总结过的通用约束。比如每个 HQL 脚本开头都要带一组 SET 参数。比如 Hive 的 WITH 子句必须写在 INSERT 之前。比如 tee 前必须 cd 到工作目录。比如数组字段要用 LATERAL VIEW explode 展开。
踩过的坑速查表:
| 坑 | 表现 | 解法 |
|---|---|---|
LOAD DATA 到 ORC 表 |
file format 不匹配 | 走文本中转表 |
| 导出 CSV 首行空行 | wc -l 多 1 行 | tail -n +2 删首行 |
| 浮点数严格相等 | 对称性验证只过 98/190 | 加 ABS(x - y) < 1e-9 |
| 字符串拼接去重 | 去重数 380 而不是 190 | 用 LEAST/GREATEST 归一化 |
| 数组字段 GROUP BY | 直接 GROUP BY 报错 | 用 LATERAL VIEW explode |
Hive WITH 必须在 INSERT 前 |
INSERT ... WITH 报 ParseException |
改为 WITH ... INSERT |
tee 前必须 cd |
日志落盘失败 | 每次先 cd /home/hadoop/b10 |
13. 写在最后
- 还没搞明白,需要进一步探索的:
-
购物篮平均 2.98 个品类是否合理? 真实电商场景里一次下单平均 1.5 到 2 个品类更常见。3 个说明这份数据的购物篮比真实数据更"饱满"。这可能是数据构造的特征,也可能反映了一个真实的"3C 类目打包购买"场景。B12 异常检测时可以对照。
-
提升度中位数低于 1 是否只是因为容量约束? B10 用数学推导解释了 lift < 1 的几何原因,但没有做严格的实证对照。可以构造一个"随机均匀购物篮"的对照数据集,算它的 lift 分布,看看是否真的和 B10 实测接近。这个对照实验留给 B15 或后续方法论拓展。
-
4 条强规则的 lift 都在 1.05 到 1.08 之间,统计显著性如何? B10 没有做置信区间的估计。理论上可以用 bootstrap 或者卡方检验来判断这 4 条规则是否显著偏离独立假设。这个分析留给 B12 异常检测时参考。
- 小结:B10 交付的不是一份可以用于推荐的品类组合清单,而是一份"证明这份数据里不存在强关联规则"的分析。这份分析的价值不在于发现了什么,而在于用严格的方法否定了原本的假设,并且走通了关联规则挖掘的完整工程流程。
- 如果换一份真实电商数据,同样的流程可以复用:先探测共现矩阵的稠密度,再选 Apriori 或 FP-Growth,最后用 lift 分布判断是否存在强规则。
14. 附录:完整脚本
本文涉及的全部脚本,都在 /home/hadoop/b10/ 下:
| 文件 | 用途 |
|---|---|
sql/probe1.hql |
探测购物篮规模(4.1) |
sql/probe2.hql |
探测品类频率分布(4.2) |
sql/probe3.hql |
探测两两共现矩阵(4.3) |
sql/probe4.hql |
探测共现熵(4.4) |
py/b10_run_fpgrowth.py |
本地 FP-Growth 挖掘(第 5 章) |
sql/01_build_city_category_dist.hql |
建 ads_category_cooccurrence(7.1) |
sql/02_build_city_summary.hql |
建 ads_category_rules(7.2) |
sql/03_verify_all.hql |
15 项验证(8) |
其余探测命令是 hive -e 一行式,已在正文中给出,直接复制运行即可。
表结构速查:
- ads_category_cooccurrence(190 行 = 190 对品类对)
| 字段 | 说明 |
|---|---|
| dimension | 维度,'order' 或 'pay' |
| cate_a | 品类 A |
| cate_b | 品类 B |
| cooccur_cnt | 同时出现的购物篮数 |
| support_ab | 共现支持度 |
| support_a | cate_a 单独支持度 |
| support_b | cate_b 单独支持度 |
| confidence_ab | P(B|A) |
| confidence_ba | P(A|B) |
| lift | 提升度(对称) |
| is_strong | 是否强规则 |
- ads_category_rules(380 行 = 190 对 × 2 方向)
| 字段 | 说明 |
|---|---|
| dimension | 维度,'order' |
| antecedent | 规则前件 |
| consequent | 规则后件 |
| support_rule | 支持度 |
| confidence | 置信度 |
| lift | 提升度 |
| is_strong | 是否强规则(lift ≥ 1.05) |