小肥柴的Hadoop之旅 快速实验篇(B03) 用户行为漏斗与转化率
电商用户行为分析实战 · B 组 · 第三阶段
数据源:
b_group.dwd_user_behavior(B02 产出)技术栈:Hive on MR
目录
- 前言
- [0. 实验全景](#0. 实验全景)
- [1. Why:为什么做 B03](#1. Why:为什么做 B03)
- [2. 前置:B02 产出的统一入口](#2. 前置:B02 产出的统一入口)
- [3. 脚本 00:环境检查与基线](#3. 脚本 00:环境检查与基线)
- [4. 脚本 01:整体漏斗](#4. 脚本 01:整体漏斗)
- [5. 脚本 02a:Session 行为标志](#5. 脚本 02a:Session 行为标志)
- [6. 诊断插曲:session 跨天与跨用户](#6. 诊断插曲:session 跨天与跨用户)
- [7. 脚本 02b:Session 有序漏斗(修订版)](#7. 脚本 02b:Session 有序漏斗(修订版))
- [8. 脚本 03:品类漏斗](#8. 脚本 03:品类漏斗)
- [9. 脚本 04:统一验证](#9. 脚本 04:统一验证)
- [10. 三个核心发现(专题)](#10. 三个核心发现(专题))
- [11. 对 B04 的影响](#11. 对 B04 的影响)
- [12. 工程收获](#12. 工程收获)
- [13. 方法收获](#13. 方法收获)
- [14. 遗留问题](#14. 遗留问题)
- [15. 结语](#15. 结语)
- [附录 A:质量报告](#附录 A:质量报告)
- [附录 B:执行环境](#附录 B:执行环境)
前言
B03 是 B 组第三个项目,也是第一个业务分析项目。
B01 完成了"数据能不能用"的审计,B02 完成了"把 DWD 升级成生产级资产"的工程化。到了 B03,问题第一次变成:这些数据能回答什么业务问题?
原设计里,B03 的问题是"搜索→点击→下单→支付的漏斗流失在哪?"。但执行下来发现,这个问题本身预设了一个错误的漏斗模型 :实际数据里,搜索和点击没有稳定的顺序关系,支付和下单也不总在同一会话内配对。B03 的最有价值产出,不是"漏斗转化率是多少",而是"原漏斗模型不成立,正确的漏斗模型是什么"。
这是 B 组实验第三次"推翻假设":B01 推翻了四条数据假设,B02 修正了 DWD 结构,B03 推翻了业务漏斗模型假设。
0. 实验全景
text
b_group.dwd_user_behavior(B02 产出)
│ 180570 行,11 个 dt 分区
│ 排除 07-27:166770 行
│
│ ① 整体漏斗(脚本 01)
▼
ads_funnel_overall 4 行(search/click/order/pay)
│
│ ② Session 行为标志(脚本 02a)
▼
ads_funnel_session 8744 行(每 session 一行,15 种 combo)
│
│ ③ Session 有序漏斗(脚本 02b)
▼
ads_funnel_session_ordered 8744 行(每 session 一行,含四个 *_seq)
│
│ ④ 品类漏斗(脚本 03)
▼
ads_funnel_category 20 行(每品类一行)
↓
脚本 04 统一验证:16 项全部通过
- 小结 :B02 说"这是你的资产",B03 说"用这份资产能算出什么业务结论,以及哪些结论不成立"。
1. Why:为什么做 B03
1.1 如果不做这一步,后面哪些项目会受到影响?
B04(热门品类 Top10)会受影响:它只能按点击量排序,可能推荐"高点击低转化"的品类。没有 B03 的品类转化率,B04 的排序就是瞎猜;而且 B03 还发现品类分布极端均匀,这一发现直接影响 B04 的结论是否成立。
B06(页面单跳转化率)会受影响:它需要知道"点击→下单"是不是一个可用的漏斗环节,才能设计页面路径。如果 B03 没跑,B06 可能按"搜索→点击→下单"设计,结果大部分路径为空。
B08(搜索词分析)会受影响 :它需要知道搜索在漏斗中的位置。原以为搜索是入口,B03 实测发现不是,B08 的分析框架要调整。
B13(ADS 主题宽表)会受影响:宽表缺核心业务字段------整体转化率、环节流失率、品类转化率。
B15(总结与工程复盘)会受影响:复盘时缺一条"业务漏斗模型被推翻"的关键认知,整个 B 组的故事线不完整。
1.2 这一步具体做什么?
用 Hive 计算四张表:
| 表 | 粒度 | 核心问题 | 行数 |
|---|---|---|---|
ads_funnel_overall |
全站环节 | 各环节 PV/UV/Session 量级 | 4 |
ads_funnel_session |
Session | 行为组合分布 | 8744 |
ads_funnel_session_ordered |
Session | 有序漏斗 | 8744 |
ads_funnel_category |
品类 | 品类点击→下单→支付 | 20 |
1.3 做完怎么验证?
| 验证项 | 方法 | 期望 |
|---|---|---|
| PV 守恒 | 整体漏斗 PV = 基线 | 37115/111310/10887/7458 |
| Session 守恒 | 各环节 session 数与 combos 一致 | 7426/8513/4977/4111 |
| 品类守恒 | 漏斗汇总 = 子表行数 | order 32487, pay 22436 |
| 跨表一致 | 02a 与 02b 行数相同 | 8744 |
| 组合守恒 | combo 之和 = session 总数 | 8744 |
1.4 与下游什么关系?
| 下游 | 本步给了什么 | 不用会怎样 |
|---|---|---|
| B04 | 品类转化率 + "品类无区分度"结论 | 会出无意义的 Top10 |
| B05 | Session 转化标签 | 不知道哪些 session 高价值 |
| B06 | "点击→下单"是稳定环节 | 可能设计错误的页面路径 |
| B08 | 搜索不是漏斗入口 | 分析框架错位 |
| B13 | 漏斗指标字段 | ADS 宽表缺核心指标 |
| B14 | 漏斗图数据 | 无法画转化漏斗 |
| B15 | 统一漏斗口径 + 业务认知 | 复盘无统一口径、缺故事线 |
2. 前置:B02 产出的统一入口
2.1 表结构
B03 使用的核心表:
sql
b_group.dwd_user_behavior (
user_id, session_id, page_id, action_time, ts, city_id,
behavior_type, behavior_combo,
search_keyword, click_category_id, click_product_id,
order_category_ids, order_product_ids,
pay_category_ids, pay_product_ids,
is_*_field_used, is_valid_behavior, is_multi_behavior,
is_unclassified, is_true_missing,
quality_flags, raw_line_id, seq_in_session,
dt -- 分区
)
关键字段:
behavior_type:search / click / order / pay,四类互斥ts:timestamp,用于时间差seq_in_session:session 内按ts, raw_line_id双键排序的序号
四张子表:dwd_order_category、dwd_order_product、dwd_pay_category、dwd_pay_product。
2.2 基线记录
执行前记录(写入 output/b03_baseline.txt):
text
main_total_all,180570
main_total_excl_0727,166770
behavior_search,37115
behavior_click,111310
behavior_order,10887
behavior_pay,7458
2.3 07-27 处理策略
B01 已确认 07-27 数据偏低 15~20%,疑截断。B03 采用:
- 主分析排除 07-27 :
dt BETWEEN '2019-07-17' AND '2019-07-26' - 报告中所有数字均基于 10 天数据
2.4 全局配置的两条警告
执行前发现 hive-site.xml 里两条全局配置:
xml
<property>
<name>hive.exec.mode.local.auto</name>
<value>true</value> <!-- B02 OOM 元凶 -->
</property>
<property>
<name>hive.auto.convert.join</name>
<value>false</value> <!-- MAPJOIN 不自动触发 -->
</property>
影响一 :local.auto=true 会让输入文件数 ≤ 4 的查询走本地模式,用 Hive CLI 256MB 堆跑 MR,一碰多值 explode 或窗口就 OOM。
决策:B03 所有写表脚本在开头显式覆盖:
sql
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
不改全局配置,避免影响其他项目。
2.5 Metastore 全局固定
执行前确认 hive-site.xml:
xml
<property>
<name>javax.jdo.option.ConnectionURL</name>
<value>jdbc:derby:;databaseName=/home/hadoop/hive_metastore/metastore_db;create=true</value>
</property>
Metastore 被固定在 /home/hadoop/hive_metastore/metastore_db,与当前工作目录无关。B02 手册里反复强调的 Derby 目录漂移问题在本环境不存在 。后续 B03--B15 只需 USE b_group;,不需要每项目独立 metastore。
工作目录 /home/hadoop/bXX 仍然保留,用于脚本、日志、输出的组织。
3. 脚本 00:环境检查与基线
3.1 目的
确认 B02 表可用,记录基线,确认 07-27 排除后的行数。
3.2 脚本
sql
-- B03-00:环境检查与基线
USE b_group;
SELECT 'main_total_all' AS metric, CAST(COUNT(*) AS STRING) AS value
FROM dwd_user_behavior;
SELECT 'main_total_excl_0727' AS metric, CAST(COUNT(*) AS STRING) AS value
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26';
SELECT CONCAT('behavior_', behavior_type) AS metric, CAST(COUNT(*) AS STRING) AS value
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY behavior_type;
3.3 执行结果
| 指标 | 期望 | 实测 | |
|---|---|---|---|
| main_total_all | 180570 | 180570 | ✅ |
| main_total_excl_0727 | 166770 | 166770 | ✅ |
| behavior_search | 37115 | 37115 | ✅ |
| behavior_click | 111310 | 111310 | ✅ |
| behavior_order | 10887 | 10887 | ✅ |
| behavior_pay | 7458 | 7458 | ✅ |
3.4 观察
三次查询都被 Hive 自动拒绝本地模式 (日志原话:Cannot run job locally: Number of Input Files (= 10/11) is larger than hive.exec.mode.local.auto.input.files.max(= 4))。说明当前分区粒度下,每个 dt 分区一个文件,10~11 个文件刚好超过阈值。
但如果某查询只命中 3 个分区(≤4 文件),它就会偷偷切进本地模式 ------所以写表脚本仍要显式 SET hive.exec.mode.local.auto=false; 防御。
4. 脚本 01:整体漏斗
4.1 设计
- 4 行,每行一个环节(search / click / order / pay)
- 三个口径:PV、UV、Session 数
- 转化率不写表,查询时用窗口函数算(避免 CTAS 嵌窗口的兼容问题)
- 排除 07-27
4.2 脚本
sql
USE b_group;
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
SET hive.exec.compress.intermediate=true;
SET hive.exec.compress.output=true;
SET hive.auto.convert.join=true;
DROP TABLE IF EXISTS ads_funnel_overall;
CREATE TABLE ads_funnel_overall (
stage string,
stage_order int,
pv bigint,
uv bigint,
session_cnt bigint
)
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY");
INSERT OVERWRITE TABLE ads_funnel_overall
SELECT 'search' AS stage, 1 AS stage_order,
COUNT(*) AS pv, COUNT(DISTINCT user_id) AS uv,
COUNT(DISTINCT session_id) AS session_cnt
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26' AND behavior_type = 'search'
UNION ALL
SELECT 'click', 2, COUNT(*), COUNT(DISTINCT user_id), COUNT(DISTINCT session_id)
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26' AND behavior_type = 'click'
UNION ALL
SELECT 'order', 3, COUNT(*), COUNT(DISTINCT user_id), COUNT(DISTINCT session_id)
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26' AND behavior_type = 'order'
UNION ALL
SELECT 'pay', 4, COUNT(*), COUNT(DISTINCT user_id), COUNT(DISTINCT session_id)
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26' AND behavior_type = 'pay';
4.3 执行结果
执行耗时 203 秒(5 个 MR Stage)。
| stage | stage_order | pv | uv | session_cnt |
|---|---|---|---|---|
| search | 1 | 37115 | 100 | 7426 |
| click | 2 | 111310 | 100 | 8513 |
| order | 3 | 10887 | 100 | 4977 |
| pay | 4 | 7458 | 100 | 4111 |
4.4 初步观察
观察 1:UV 口径完全退化 。100 用户全样本,每个人都触达过每一类行为。后续所有分析必须用 PV 或 Session 口径,不能依赖 UV。
观察 2:click session (8513) > search session (7426)。很多点击不是从搜索来的。搜索→点击不是严格父子关系。
观察 3:真正的漏斗是"点击→下单→支付":
- Session 层点击→下单:4977 / 8513 = 58.5%
- Session 层下单→支付:4111 / 4977 = 82.6%
- PV 层点击→下单:10887 / 111310 = 9.78%
PV 和 Session 口径差异巨大,因为 PV 口径下点击基数远大于 order。两套口径都要在报告中说明。
5. 脚本 02a:Session 行为标志
5.1 设计
每个 session 一行,含四个布尔标志(has_search / has_click / has_order / has_pay),加上 total_events 和 day_cnt。
5.2 脚本
sql
USE b_group;
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
DROP TABLE IF EXISTS ads_funnel_session;
CREATE TABLE ads_funnel_session
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT
session_id,
MAX(CASE WHEN behavior_type = 'search' THEN 1 ELSE 0 END) AS has_search,
MAX(CASE WHEN behavior_type = 'click' THEN 1 ELSE 0 END) AS has_click,
MAX(CASE WHEN behavior_type = 'order' THEN 1 ELSE 0 END) AS has_order,
MAX(CASE WHEN behavior_type = 'pay' THEN 1 ELSE 0 END) AS has_pay,
COUNT(*) AS total_events,
COUNT(DISTINCT dt) AS day_cnt
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY session_id;
5.3 组合分布结果
Session 总数 8744。15 种行为组合:
| combo | 含义 | 数量 | 占比 |
|---|---|---|---|
| 1111 | 搜索+点击+下单+支付 | 2882 | 33.0% |
| 1100 | 搜索+点击 | 1711 | 19.6% |
| 1110 | 搜索+点击+下单 | 1709 | 19.5% |
| 1101 | 搜索+点击+支付(无下单) | 955 | 10.9% |
| 0100 | 仅点击 | 783 | 9.0% |
| 0110 | 点击+下单 | 241 | 2.8% |
| 0101 | 点击+支付(无下单) | 152 | 1.7% |
| 1000 | 仅搜索 | 132 | 1.5% |
| 0111 | 点击+下单+支付 | 80 | 0.9% |
| 0010 | 仅下单 | 38 | 0.4% |
| 0001 | 仅支付 | 21 | 0.2% |
| 1010 | 搜索+下单 | 19 | 0.2% |
| 1001 | 搜索+支付 | 13 | 0.1% |
| 1011 | 搜索+下单+支付 | 5 | 0.06% |
| 0011 | 下单+支付 | 3 | 0.03% |
守恒核对:
- 含 search:2882+1711+1709+955+132+19+13+5 = 7426 ✅
- 含 click:2882+1711+1709+955+783+241+152+80 = 8513 ✅
- 含 order:2882+1709+241+80+38+19+5+3 = 4977 ✅
- 含 pay:2882+955+152+80+21+13+5+3 = 4111 ✅
5.4 三个反直觉观察
观察 1 :全链路 session(1111)占比 33%,是最大类。原以为主流是"仅点击"或"仅搜索"。这说明 session 很长(平均 19 事件),或 session_id 在多天间被复用。
观察 2 :1101 + 0101 + 0001 = 1128 session 出现"支付但无下单"。逻辑上支付必须紧跟下单,这暗示 session 跨天或 session_id 复用。
观察 3 :纯搜索 session(1000)只有 132 个,纯点击 session(0100)只有 783 个。绝大多数 session 都包含"搜索 + 点击"组合。
6. 诊断插曲:session 跨天与跨用户
组合分布里的三个反直觉观察,必须查清楚才能继续。
6.1 诊断 1:session 跨天分布
sql
SELECT day_cnt, COUNT(*) AS session_cnt
FROM ads_funnel_session GROUP BY day_cnt ORDER BY day_cnt;
结果:
text
1 8736
2 8
结论:99.9% 的 session 单天,8 个跨天 session 属异常值但占比极小。
6.2 诊断 2:抽查 1101 的 session 明细
sql
SELECT session_id, dt, behavior_type, action_time, seq_in_session
FROM dwd_user_behavior
WHERE session_id IN (
SELECT session_id FROM ads_funnel_session
WHERE has_search=1 AND has_click=1 AND has_order=0 AND has_pay=1 LIMIT 3
)
ORDER BY session_id, seq_in_session LIMIT 60;
样本 1(16 事件,04:36~04:38):
text
seq=1 pay 04:36:28 ← 支付在 session 最开头
seq=2 search 04:36:37
seq=3 click 04:36:45
...
样本 2(3 事件):
text
seq=1 click 20:48:47
seq=2 search 20:48:51
seq=3 pay 20:48:59 ← 点完就支付,没有 order
样本 3(13 事件,含 2 次 pay):
text
seq=7 pay 19:06:24
seq=9 pay 19:06:36 ← 两次支付,都没 order
业务解释 :pay 事件独立出现,最合理的解释是支付历史订单------用户前一天/上一个 session 下过单,今天进来直接支付;或订单行为未在同一 session 记录。
6.3 诊断 3:session 是否跨用户
sql
SELECT COUNT(*) AS sessions_multi_user
FROM (
SELECT session_id FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY session_id HAVING COUNT(DISTINCT user_id) > 1
) t;
结果:0。
结论:session_id 不跨用户,数据健康。
6.4 对 02b 的决定性影响
原设计 02b 想算"严格有序全链路 session 数"。现在看:
- 33% 的 session 是
1111,但其中大部分 pay 不在 order 之后 - 严格有序漏斗会严重低估,甚至可能接近 0
决策:02b 改为"两段有序 + 全链路宽松",分三个两段指标 + 两个全链路指标:
| 指标 | 定义 |
|---|---|
search_before_click |
同 session 内 search_seq < click_seq |
click_before_order |
同 session 内 click_seq < order_seq |
order_before_pay |
同 session 内 order_seq < pay_seq |
full_strict |
上述三个全部满足 |
full_loose |
同 session 内含全部四类(无论顺序) |
7. 脚本 02b:Session 有序漏斗(修订版)
7.1 脚本
sql
USE b_group;
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
DROP TABLE IF EXISTS ads_funnel_session_ordered;
CREATE TABLE ads_funnel_session_ordered
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT
session_id,
MIN(CASE WHEN behavior_type = 'search' THEN seq_in_session END) AS search_seq,
MIN(CASE WHEN behavior_type = 'click' THEN seq_in_session END) AS click_seq,
MIN(CASE WHEN behavior_type = 'order' THEN seq_in_session END) AS order_seq,
MIN(CASE WHEN behavior_type = 'pay' THEN seq_in_session END) AS pay_seq
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY session_id;
7.2 五指标汇总
| 指标 | 值 | 分母 | 比率 |
|---|---|---|---|
| search_sessions | 7426 | --- | --- |
| search_before_click | 1922 | search∩click = 7257 | 26.5% |
| click_before_order | 4242 | click∩order = 4939 | 85.9% |
| order_before_pay | 1582 | order∩pay = 2970 | 53.3% |
| full_strict | 260 | full_loose = 2882 | 9.0% |
| full_loose | 2882 | --- | --- |
7.3 三个重磅发现
发现 A:搜索→点击的顺序在大多数 session 里是反的。
- 同时有 search 和 click 的 session:7257
- 其中先搜后点(
search_seq < click_seq):仅 1922(26.5%) - 剩下 5335(73.5%)是"先点击,后搜索"
这不是数据错误,是埋点事实:用户可能在浏览页/推荐流直接点击,再回到搜索框搜索。搜索不是可用的漏斗起点,真正的漏斗起点是点击。
发现 B:点击→下单是唯一稳健的有序漏斗。
- 85.9% 的 click∩order session 里
click_seq < order_seq - 这与业务直觉一致,也是唯一"经得起顺序检验"的环节
发现 C:下单→支付只有 53.3% 顺序正确。
- 近一半的 session 里支付先于下单
- 印证了"支付历史订单"的真实业务模式
7.4 修订后的漏斗主结论
原来的"搜索→点击→下单→支付"漏斗模型,实际数据上应改为:
text
入口 点击 PV 111310 / click session 8513
↓ 顺序可验证:85.9%
下单 order PV 10887 / order session 4977
↓ 顺序可验证:53.3%
支付 pay PV 7458 / pay session 4111
搜索 独立分析:search PV 37115 / 与点击顺序无关
搜索不是漏斗入口,而是独立的"用户主动意图"信号,后续 B08(搜索词分析)要用这个定位。
8. 脚本 03:品类漏斗
8.1 设计
- 点击品类来自
dwd_user_behavior.click_category_id(单值) - 下单品类来自
dwd_order_category(explode 后) - 支付品类来自
dwd_pay_category(explode 后) - 三表
FULL OUTER JOIN,保证没有点击但有下单的品类也出现 - 不含搜索------B03 已确认搜索不是漏斗环节
8.2 脚本
sql
USE b_group;
SET hive.exec.mode.local.auto=false;
SET mapreduce.task.io.sort.mb=64;
DROP TABLE IF EXISTS ads_funnel_category;
CREATE TABLE ads_funnel_category (
category_id bigint,
click_cnt bigint,
order_cnt bigint,
pay_cnt bigint
)
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY");
INSERT OVERWRITE TABLE ads_funnel_category
SELECT
COALESCE(c.category_id, o.category_id, p.category_id) AS category_id,
COALESCE(c.click_cnt, 0) AS click_cnt,
COALESCE(o.order_cnt, 0) AS order_cnt,
COALESCE(p.pay_cnt, 0) AS pay_cnt
FROM (
SELECT click_category_id AS category_id, COUNT(*) AS click_cnt
FROM dwd_user_behavior
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
AND behavior_type = 'click' AND click_category_id != -1
GROUP BY click_category_id
) c
FULL OUTER JOIN (
SELECT category_id, COUNT(*) AS order_cnt
FROM dwd_order_category
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY category_id
) o ON c.category_id = o.category_id
FULL OUTER JOIN (
SELECT category_id, COUNT(*) AS pay_cnt
FROM dwd_pay_category
WHERE dt BETWEEN '2019-07-17' AND '2019-07-26'
GROUP BY category_id
) p ON COALESCE(c.category_id, o.category_id) = p.category_id;
8.3 结果
20 个品类,按 click 降序:
| 品类 | click | order | pay | click→order | order→pay |
|---|---|---|---|---|---|
| 2 | 5687 | 1628 | 1120 | 28.6% | 68.8% |
| 11 | 5652 | 1634 | 1115 | 28.9% | 68.2% |
| 15 | 5652 | 1532 | 1165 | 27.1% | 76.0% |
| 17 | 5640 | 1621 | 1132 | 28.7% | 69.8% |
| 20 | 5628 | 1632 | 1141 | 29.0% | 69.9% |
| 12 | 5609 | 1593 | 1131 | 28.4% | 71.0% |
| 7 | 5600 | 1656 | 1162 | 29.6% | 70.2% |
| 9 | 5595 | 1600 | 1153 | 28.6% | 72.1% |
| 19 | 5577 | 1598 | 1072 | 28.7% | 67.1% |
| 5 | 5572 | 1681 | 1053 | 30.2% | 62.6% |
| 13 | 5564 | 1647 | 1087 | 29.6% | 66.0% |
| 14 | 5558 | 1657 | 1087 | 29.8% | 65.6% |
| 18 | 5540 | 1626 | 1119 | 29.3% | 68.8% |
| 10 | 5531 | 1619 | 1089 | 29.3% | 67.3% |
| 4 | 5522 | 1630 | 1170 | 29.5% | 71.8% |
| 8 | 5510 | 1610 | 1144 | 29.2% | 71.1% |
| 16 | 5493 | 1656 | 1143 | 30.1% | 69.0% |
| 3 | 5482 | 1606 | 1118 | 29.3% | 69.6% |
| 1 | 5460 | 1628 | 1120 | 29.8% | 68.8% |
| 6 | 5438 | 1633 | 1115 | 30.0% | 68.3% |
8.4 守恒验证
| 指标 | 值 | 对照 |
|---|---|---|
| category_click_sum | 111310 | ✅ 与基线一致 |
| order_cat_rows | 32487 | --- |
| funnel_order_sum | 32487 | ✅ 相等 |
| pay_cat_rows | 22436 | --- |
| funnel_pay_sum | 22436 | ✅ 相等 |
8.5 关键观察:品类数据太均匀了
20 个品类的 click 全部在 5438~5687 之间(波动 ±2.3%),转化率全部在 27~30% / 68~76% 之间。
真实电商数据应是长尾分布 ,头部品类 click 可能是尾部品类的 10~100 倍,转化率也有 3~5 倍差异。这里的均匀度极端反常。
这不是清洗问题,是数据集生成方式的问题 。B01 只审计了行级质量,没审计维度分布,B03 补上了这一课。
9. 脚本 04:统一验证
9.1 脚本
sql
-- 16 项验证
USE b_group;
SELECT 'V1_overall_rows' AS metric, CAST(COUNT(*) AS STRING) AS value FROM ads_funnel_overall;
SELECT 'V2_overall_pv_sum', CAST(SUM(pv) AS STRING) FROM ads_funnel_overall;
SELECT 'V3_search_sessions', CAST(session_cnt AS STRING) FROM ads_funnel_overall WHERE stage = 'search';
SELECT 'V4_click_sessions', CAST(session_cnt AS STRING) FROM ads_funnel_overall WHERE stage = 'click';
SELECT 'V5_order_sessions', CAST(session_cnt AS STRING) FROM ads_funnel_overall WHERE stage = 'order';
SELECT 'V6_pay_sessions', CAST(session_cnt AS STRING) FROM ads_funnel_overall WHERE stage = 'pay';
SELECT 'V7_session_rows', CAST(COUNT(*) AS STRING) FROM ads_funnel_session;
SELECT 'V8_session_no_behavior', CAST(COUNT(*) AS STRING) FROM ads_funnel_session
WHERE has_search + has_click + has_order + has_pay = 0;
SELECT 'V9_session_multi_day', CAST(COUNT(*) AS STRING) FROM ads_funnel_session WHERE day_cnt > 1;
SELECT 'V10_ordered_rows', CAST(COUNT(*) AS STRING) FROM ads_funnel_session_ordered;
SELECT 'V11_full_loose', CAST(COUNT(*) AS STRING) FROM ads_funnel_session_ordered
WHERE search_seq IS NOT NULL AND click_seq IS NOT NULL
AND order_seq IS NOT NULL AND pay_seq IS NOT NULL;
SELECT 'V12_full_strict', CAST(COUNT(*) AS STRING) FROM ads_funnel_session_ordered
WHERE search_seq IS NOT NULL AND click_seq IS NOT NULL
AND order_seq IS NOT NULL AND pay_seq IS NOT NULL
AND search_seq < click_seq AND click_seq < order_seq AND order_seq < pay_seq;
SELECT 'V13_category_rows', CAST(COUNT(*) AS STRING) FROM ads_funnel_category;
SELECT 'V14_category_click_sum', CAST(SUM(click_cnt) AS STRING) FROM ads_funnel_category;
SELECT 'V15_category_order_sum', CAST(SUM(order_cnt) AS STRING) FROM ads_funnel_category;
SELECT 'V16_category_pay_sum', CAST(SUM(pay_cnt) AS STRING) FROM ads_funnel_category;
9.2 结果(16 项全部通过)
| 指标 | 期望 | 实测 | |
|---|---|---|---|
| V1_overall_rows | 4 | 4 | ✅ |
| V2_overall_pv_sum | 166770 | 166770 | ✅ |
| V3_search_sessions | 7426 | 7426 | ✅ |
| V4_click_sessions | 8513 | 8513 | ✅ |
| V5_order_sessions | 4977 | 4977 | ✅ |
| V6_pay_sessions | 4111 | 4111 | ✅ |
| V7_session_rows | 8744 | 8744 | ✅ |
| V8_session_no_behavior | 0 | 0 | ✅ |
| V9_session_multi_day | 8 | 8 | ✅ |
| V10_ordered_rows | 8744 | 8744 | ✅ |
| V11_full_loose | 2882 | 2882 | ✅ |
| V12_full_strict | 260 | 260 | ✅ |
| V13_category_rows | 20 | 20 | ✅ |
| V14_category_click_sum | 111310 | 111310 | ✅ |
| V15_category_order_sum | 32487 | 32487 | ✅ |
| V16_category_pay_sum | 22436 | 22436 | ✅ |
10. 三个核心发现(专题)
这一章把 B03 最有价值的三条发现单独拎出来,它们都是推翻了预设的发现。
10.1 发现 1:搜索不是漏斗入口
预设:搜索 → 点击 → 下单 → 支付,搜索是漏斗第一层。
实测:
- 同时有 search 和 click 的 session:7257
- 先搜后点(
search_seq < click_seq):1922(26.5%) - 先点后搜:5335(73.5%)
结论 :搜索与点击没有稳定的顺序关系。搜索不是"漏斗起点",而是独立的"用户主动意图"信号。
影响:
- B04:品类排序不能假设"搜索→点击"
- B06:页面路径不含搜索
- B08:搜索分析要独立定位,不挂在漏斗下
10.2 发现 2:支付无下单是真实业务
预设:支付必须紧跟下单。
实测:
1101(搜索+点击+支付,无下单):955 session0101(点击+支付,无下单):152 session0001(仅支付):21 session- 合计 1128 session 里"支付时没有下单"
明细显示 pay 事件独立出现,最合理解释是支付历史订单------用户前一天/上一个 session 下过单,今天进来直接支付。
结论 :order_before_pay 只有 53.3% ,严格全链路(full_strict)只有 260 ,远小于宽松全链路 2882。
影响:
- B07:会话分析要区分"新订单支付"和"历史订单支付"
- B12:异常流量识别要考虑"纯支付 session"是正常业务
- B14:漏斗图要标注两种支付模式
10.3 发现 3:品类维度没有业务区分度
预设:品类应该有明显头部和长尾。
实测:
- 20 个品类,click 全部在 5438~5687 之间(波动 ±2.3%)
- 转化率全部在 27~30% / 68~76% 之间
真实电商应呈长尾分布,这里是极端均匀。
结论 :这不是清洗问题,是数据集生成方式的问题 。B01 只审计了行级质量,没审计维度分布。B03 补上了这一课。
影响:
- B04:Top10 与 Bottom10 只差 4%,热门品类 Top10 的分析价值有限
- B09:城市×品类偏好分析也会受均匀度影响
- B10:品类共现分析要重新评估
11. 对 B04 的影响
B04 原设计是"热门品类 Top10 与加权排名"。B03 发现品类分布均匀后,B04 需要调整。
11.1 原设计的问题
- 若按点击量排 Top10:Top1(品类 2,5687)和 Bottom1(品类 6,5438)只差 4.6%
- 若按加权(点击×20% + 下单×30% + 支付×50%):由于三个维度的品类排序都不一致,加权结果会在 20 个品类间轻微抖动
- 结论:排名差异不具业务意义
11.2 三个方案
| 方案 | 描述 | 优点 | 缺点 |
|---|---|---|---|
| A | 保留 Top10,报告中明确标注"品类分布均匀" | 简单 | 结论无意义 |
| B | 改为品类转化率排行(click→order / order→pay) | 至少区分高/低转化品类 | 转化率差异也仅 3% |
| C | 改为品类共现(把 B10 部分提前) | 至少看品类间关系 | 与原 B04 主题偏离 |
11.3 推荐方案
推荐 A + B 组合:
- 保留 Top10(原设计),但报告中标注"品类分布均匀,排名差异不具业务意义"
- 增加"品类转化率排行"作为补充
- 在 B04 报告里明确写出这条数据约束,与 B01 的"07-27 疑截断"、"100 用户"并列
决策权留给 B04 执行时。B03 的任务是把发现记录下来。
12. 工程收获
12.1 全局配置的两条坑
hive.exec.mode.local.auto=true会让小输入查询走本地模式,用 256MB 堆跑 MR,容易 OOM- 修正:每个写表脚本开头
SET hive.exec.mode.local.auto=false; SET mapreduce.task.io.sort.mb=64; hive.auto.convert.join=false意味着 MAPJOIN 不自动触发,需要显式SET或/*+ MAPJOIN */
12.2 交互式 SQL 与脚本文件的取舍
- 单行
SELECT:交互式可靠 - 多行 SQL:必须写
.hql文件 +hive -f - 短查询:
hive -e "..."单行可靠 - 长查询:写成
.hql文件,避免 PS2 续行卡死
12.3 输出日志的双刃剑
2>/dev/null屏蔽噪声,但连错误信息也屏蔽- 推荐:
2>&1 | grep -vE 'SLF4J|Logging initialized|Hive Session|^$' - 或保留全部日志,从日志尾部看结果
12.4 先 SHOW TABLES 再验证
建表脚本执行后,先 SHOW TABLES LIKE 'xxx' 确认表存在,再跑验证 SELECT 。这一步别省------否则 SELECT 报错被 2>/dev/null 吞掉,看到的是"空输出"而不是"错误"。
12.5 ORC + Snappy + 分区表
- 主表
dwd_user_behavior:分区 ORC,11 个 dt 分区 - B03 的四张表:非分区 ORC,Snappy
- 决策原因:B03 的结果表很小(4 行 / 8744 行 / 20 行),分区收益有限
12.6 UNION ALL 多分支的 Stage 数
ads_funnel_overall 的 4 个 UNION ALL 分支,Hive 编译成 5 个 MR Stage(4 个分支各 1 个 + 1 个汇总)。耗时 203 秒。如果拆成 4 个 INSERT INTO 单跑,总耗时相近但更可控。
13. 方法收获
13.1 假设驱动:B03 推翻了"漏斗模型"假设
- B01 推翻 4 条数据假设
- B02 修正 DWD 结构
- B03 推翻"搜索→点击→下单→支付"漏斗模型
每一条推翻,都对应一次设计调整。这些"推翻"本身是最有价值的产出。
13.2 组合分布是 Session 分析的第一手材料
MAX(CASE WHEN ...) 生成四个布尔标志,再 GROUP BY 四个标志,得到 15 种组合分布。这一步比计算任何转化率都重要------它直接暴露了数据的行为模式。
13.3 顺序分析必须显式验证
- 原设计假设"搜索→点击→下单→支付"顺序天然成立
- 实测发现:search→click 只有 26.5% 顺序正确,order→pay 只有 53.3%
- "顺序"不是数据的天然属性,是需要验证的假设
13.4 维度分布审计不能漏
B01 审计了行级质量(null、空串、格式),没审计维度分布。B03 发现品类分布极端均匀,这才补齐了"维度分布"这一课。
后续项目:B04(品类)、B09(城市)、B08(搜索词)都要先看分布,再下结论。
13.5 口径说明必须显式写出
- 整体漏斗有 PV / UV / Session 三种口径
- UV 口径在本数据完全退化(都是 100)
- 品类漏斗的点击是"行为次数",下单/支付是"品类出现次数"
- 不写清口径的数字,都是误导
14. 遗留问题
14.1 8 个跨天 session
- day_cnt=2 的 session 有 8 个
- 占比 0.09%,暂不处理
- 未验证:这 8 个 session 是否与"支付无下单"相关
14.2 3 组完全重复(沿用 B01)
- 同一用户、同一秒、同一页面、同一搜索词
- 占比 0.0017%,暂不处理
14.3 07-27 疑截断(沿用 B01)
- 数据量偏低 15~20%
- B03 全部排除 07-27
- 未做含 07-27 的对照版本
14.4 100 用户样本(沿用 B01)
- 100 用户全样本,UV 口径完全退化
- 所有用户级分析(B11 RFM)都要谨慎
14.5 品类均匀(B03 新发现)
- 20 个品类分布极端均匀
- 未验证:是数据集生成方式问题,还是清洗过程引入
- 影响 B04、B09、B10 的分析价值
14.6 支付无下单(B03 新发现)
- 1128 session 出现"支付无下单"
- 最合理解释是"支付历史订单"
- 未验证:能否通过用户跨 session 行为佐证
15. 结语
B03 是 B 组第一个业务分析项目。它完成了"用 B02 的资产回答业务问题"的任务,但更重要的是:它推翻了预设的漏斗模型。
原来的漏斗模型是"搜索→点击→下单→支付"。实测发现:
- 搜索不是漏斗入口(73.5% 的 session 先点后搜)
- 下单→支付不总在同一 session 内(只有 53.3% 顺序正确)
- 品类维度没有业务区分度(波动 ±2.3%)
这三条发现,才是 B03 对后续 B04--B15 的真正贡献 :不是"一份漏斗报告",而是"一套已被验证的业务认知"。
B01 建立了"数据认知 "(11 天数据、四类互斥、文件顺序≠时间顺序),B02 建立了"结构认知 "(分区、ts、seq_in_session),B03 建立了"业务认知"(搜索不是漏斗入口、支付无下单是真实业务、品类均匀)。
这三层认知,是 B04--B15 的共同底座。
附录 A:质量报告
output/b03_quality_report.csv:
text
metric,value
overall_rows,4
overall_pv_sum,166770
search_pv,37115
click_pv,111310
order_pv,10887
pay_pv,7458
search_sessions,7426
click_sessions,8513
order_sessions,4977
pay_sessions,4111
session_rows,8744
session_no_behavior,0
session_multi_day,8
session_combo_1111,2882
session_combo_1100,1711
session_combo_1110,1709
session_combo_1101,955
session_combo_0100,783
session_combo_0110,241
session_combo_0101,152
session_combo_1000,132
search_before_click,1922
click_before_order,4242
order_before_pay,1582
full_loose,2882
full_strict,260
category_rows,20
category_click_sum,111310
category_order_sum,32487
category_pay_sum,22436
dt_range_excl_0727,2019-07-17_to_2019-07-26
dt_0727_anomaly,suspected_truncation
data_quality_finding,category_distribution_suspiciously_uniform
data_quality_finding,search_not_funnel_entry
data_quality_finding,pay_without_order_is_real_business
附录 B:执行环境
| 项 | 版本 |
|---|---|
| Hadoop | 3.3.6 |
| Hive | 3.1.3 (on MR) |
| 集群 | 1 master + 3 worker,每节点 2GB |
| Java | 8 |
| 工作目录 | /home/hadoop/b03 |
| Metastore | 全局固定 /home/hadoop/hive_metastore/metastore_db |
| Warehouse | /user/hive/warehouse |
本文是 B03 阶段完整复盘,也是 B04--B15 的业务底座说明。