快速实验篇(B10)品类共现与关联规则实战

小肥柴的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)-子集不频繁,就直接丢弃,不用扫描数据算支持度。

算法流程:

  1. 扫描所有购物篮,统计每个品类的支持度,保留 ≥ min_support 的品类作为频繁 1-项集。
  2. 迭代生成频繁 k-项集。每一轮做三件事:
    • 连接:把两个频繁 (k-1)-项集连接成候选 k-项集
    • 剪枝:检查候选 k-项集的所有 (k-1)-子集是否频繁,不满足就丢弃
    • 计数:对保留下来的候选扫描数据,算支持度
  3. 重复直到没有新的频繁项集。
  4. 从频繁项集生成关联规则,算置信度和提升度。

三个核心指标:

支持度:一个品类组合在所有购物篮里出现的比例。

复制代码
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. 写在最后

  • 还没搞明白,需要进一步探索的:
  1. 购物篮平均 2.98 个品类是否合理? 真实电商场景里一次下单平均 1.5 到 2 个品类更常见。3 个说明这份数据的购物篮比真实数据更"饱满"。这可能是数据构造的特征,也可能反映了一个真实的"3C 类目打包购买"场景。B12 异常检测时可以对照。

  2. 提升度中位数低于 1 是否只是因为容量约束? B10 用数学推导解释了 lift < 1 的几何原因,但没有做严格的实证对照。可以构造一个"随机均匀购物篮"的对照数据集,算它的 lift 分布,看看是否真的和 B10 实测接近。这个对照实验留给 B15 或后续方法论拓展。

  3. 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)
相关推荐
卷毛迷你猪1 小时前
快速实验篇(B09)城市维度分析,均匀随机字段的识别与证明
大数据·hive·hadoop
samFuB5 小时前
【数据集】上市公司数字化与绿色化耦合协调度数据(2010-2025年)
大数据
198******1263411 小时前
2026企业AI办公工具选型指南:从评估框架到场景适配
大数据·运维·人工智能
2601_9672127211 小时前
专利型电源轨道系统技术路线对比与选型框架
大数据·运维·网络·经验分享
数字化顾问12 小时前
(138页PPT)流程体系的建设优化与运营(附下载方式)
大数据·运维·人工智能
织码weavecodes13 小时前
培训平台部署备份与容灾:从三服务交付到可恢复运行
大数据·学习·开源软件
天涯明月199314 小时前
世界模型:原理、范式与工程实践
大数据·人工智能·大模型·具身智能·世界模型
梦想画家15 小时前
SQLMesh Python 模型入门(三):前后置语句、蓝图建模与避坑指南
大数据·python·sqlmesh
和裕15 小时前
年度框架直供 vs 零散按需采购:定制纸箱采购成本、交付与服务核心区别全对比
大数据·运维·网络·人工智能·算法