快速实验篇(B01)用户行为日志画像与 DWD 标准化

小肥柴的Hadoop之旅 快速实验篇(B01)用户行为日志画像与 DWD 标准化

电商用户行为分析实战 · B 组 · 第一阶段

数据源:user_visit_action.txt(18.6 MB,180570 行)

技术栈:Hadoop MR + Hive on MR


目录

  • 前言
  • [0. 实验全景:从原始日志到 DWD 分层](#0. 实验全景:从原始日志到 DWD 分层)
  • [1. 数据源与业务语义](#1. 数据源与业务语义)
    • [1.1 数据源结构](#1.1 数据源结构)
    • [1.2 三类字段:共性、行为、维度](#1.2 三类字段:共性、行为、维度)
    • [1.3 三种"没有值"的语义区分](#1.3 三种"没有值"的语义区分)
    • [1.4 行为判定规则与互斥声明](#1.4 行为判定规则与互斥声明)
  • [2. 实验架构:本地编译 + 集群运行](#2. 实验架构:本地编译 + 集群运行)
    • [2.1 环境分层](#2.1 环境分层)
    • [2.2 数据流全景](#2.2 数据流全景)
    • [2.3 五张 Hive 表的依赖链](#2.3 五张 Hive 表的依赖链)
  • [3. 六条假设:从声明到实测](#3. 六条假设:从声明到实测)
    • [3.1 H1 结构完整性](#3.1 H1 结构完整性)
    • [3.2 H2 行为互斥](#3.2 H2 行为互斥)
    • [3.3 H3 日期分布:推翻"单日"假设](#3.3 H3 日期分布:推翻"单日"假设)
    • [3.4 H4 品类-产品配对:推翻"配对"假设](#3.4 H4 品类-产品配对:推翻"配对"假设)
    • [3.5 H5 文件顺序 vs 时间顺序](#3.5 H5 文件顺序 vs 时间顺序)
    • [3.6 H6 脏数据密度:远比预期干净](#3.6 H6 脏数据密度:远比预期干净)
  • [4. MR 清洗程序](#4. MR 清洗程序)
    • [4.1 Mapper:字段级语义显式化](#4.1 Mapper:字段级语义显式化)
    • [4.2 Driver:Tool + ToolRunner 写法](#4.2 Driver:Tool + ToolRunner 写法)
    • [4.3 Counter:MR 里的免费画像工具](#4.3 Counter:MR 里的免费画像工具)
    • [4.4 执行命令与 -D 参数位置](#4.4 执行命令与 -D 参数位置)
  • [5. Hive 建表与依赖链](#5. Hive 建表与依赖链)
    • [5.1 五张 HQL 脚本的作用与依赖](#5.1 五张 HQL 脚本的作用与依赖)
    • [5.2 外部表 + ORC 两级设计](#5.2 外部表 + ORC 两级设计)
    • [5.3 Derby 元数据的目录漂移](#5.3 Derby 元数据的目录漂移)
  • [6. 数据画像结果](#6. 数据画像结果)
    • [6.1 结构完整性](#6.1 结构完整性)
    • [6.2 行为互斥验证](#6.2 行为互斥验证)
    • [6.3 日期分布](#6.3 日期分布)
    • [6.4 日 × 行为交叉](#6.4 日 × 行为交叉)
    • [6.5 维度画像](#6.5 维度画像)
  • [7. 数据处理的反复拉扯(专题)](#7. 数据处理的反复拉扯(专题))
    • [7.1 编码:GBK 不可映射字符](#7.1 编码:GBK 不可映射字符)
    • [7.2 日期:从硬编码到分布画像](#7.2 日期:从硬编码到分布画像)
    • [7.3 Hive 保留字:date → dt](#7.3 Hive 保留字:date → dt)
    • [7.4 CTAS 限制:array<bigint> → array<string>](#7.4 CTAS 限制:array → array)
    • [7.5 多值炸裂:从配对到独立](#7.5 多值炸裂:从配对到独立)
    • [7.6 压缩:Snappy 与 Hive SerDe 的错位](#7.6 压缩:Snappy 与 Hive SerDe 的错位)
  • [8. 验证与守恒](#8. 验证与守恒)
    • [8.1 行数守恒](#8.1 行数守恒)
    • [8.2 MR vs Hive 交叉验证](#8.2 MR vs Hive 交叉验证)
    • [8.3 质量报告](#8.3 质量报告)
  • [9. 工程收获](#9. 工程收获)
    • [9.1 Counter 的价值](#9.1 Counter 的价值)
    • [9.2 Tool + ToolRunner 的意义](#9.2 Tool + ToolRunner 的意义)
    • [9.3 Hive 工作目录一致性](#9.3 Hive 工作目录一致性)
    • [9.4 幂等性:输出目录先删](#9.4 幂等性:输出目录先删)
  • [10. 方法收获](#10. 方法收获)
    • [10.1 审计不是"发现问题",而是"检验假设"](#10.1 审计不是"发现问题",而是"检验假设")
    • [10.2 多值字段:先画像,再炸裂](#10.2 多值字段:先画像,再炸裂)
    • [10.3 文件顺序 ≠ 时间顺序](#10.3 文件顺序 ≠ 时间顺序)
    • [10.4 外部表 + ORC 的两级设计](#10.4 外部表 + ORC 的两级设计)
  • [11. 遗留问题与已知未验证方向](#11. 遗留问题与已知未验证方向)
    • [11.1 07-27 数据疑截断](#11.1 07-27 数据疑截断)
    • [11.2 3 组完全重复](#11.2 3 组完全重复)
    • [11.3 100 用户样本量](#11.3 100 用户样本量)
    • [11.4 无维表](#11.4 无维表)
    • [11.5 B02 的改进方向](#11.5 B02 的改进方向)
  • [12. 结语](#12. 结语)
  • [附录 A:Counter 完整存档](#附录 A:Counter 完整存档)
  • [附录 B:执行环境](#附录 B:执行环境)

前言

B01 是 B 组电商用户行为分析的第一阶段,任务是:把一份 18.6 MB、180570 行的原始行为日志,经过画像、清洗、语义显式化,落地成可复用的 DWD 明细层。

这份报告和一般"步骤说明"不同。它不只写"做了什么",还写"原本以为是什么、实测发现是什么、为什么会这样、修正后得到什么"。因为真实的数据工程里,最耗时的不是敲代码,而是发现自己的假设错了。

全文围绕六个假设的检验展开:2 条成立,4 条被推翻。每一条推翻,都对应一次设计调整。这些"推翻"才是本阶段最有价值的部分。


0. 实验全景:从原始日志到 DWD 分层

先给一张全景图。

text 复制代码
原始日志文件 user_visit_action.txt
   │  18.6 MB,180570 行,下划线分隔,13 字段
   │
   │  ① hdfs dfs -put
   ▼
HDFS ODS 层
   /b_group/ods/user_action/user_visit_action.txt
   │
   │  ② Hadoop MR(CleanDriver + CleanMapper)
   │     · 解析 13 字段
   │     · 行为判定(search/click/order/pay)
   │     · 语义显式化(-1/null/空串 → 显式标记)
   │     · 多值规范化
   │     · 16 个审计维度 → Counter
   ▼
HDFS DWD TSV 层
   /b_group/dwd/user_behavior_tsv/part-m-00000
   │  25 列,tab 分隔
   │
   │  ③ Hive 建表(01--05)
   ▼
Hive 表(b_group 库)
   ├── ods_user_action            外部表  180570 行
   ├── dwd_user_behavior_tsv      外部表  180570 行
   ├── dwd_user_behavior_orc      ORC    180570 行(主 DWD)
   ├── dwd_user_behavior_dirty    ORC         0 行(真缺失隔离)
   ├── dwd_order_category         ORC     35188 行
   ├── dwd_order_product          ORC     23597 行
   ├── dwd_pay_category           ORC     24147 行
   └── dwd_pay_product            ORC     16096 行

一句话:一份日志,经过 MR 清洗、Hive 分层,产出 1 张主 DWD + 1 张脏数据隔离 + 4 张多值子表,供 B02--B15 使用。


1. 数据源与业务语义

1.1 数据源结构

项 值
文件名 user_visit_action.txt
大小 18.6 MB
行数 180570
格式 下划线 _ 分隔,13 字段
语义 一行 = 一次用户行为
HDFS 路径 /b_group/ods/user_action/user_visit_action.txt

字段定义:

# 字段 类型 说明
1 date string 行为日期
2 user_id long 用户 ID
3 session_id string 会话 UUID
4 page_id long 页面 ID
5 action_time string 动作时间(含空格)
6 search_keyword string 搜索关键词
7 click_category_id long 点击品类
8 click_product_id long 点击产品
9 order_category_ids string 下单品类,逗号分隔
10 order_product_ids string 下单产品
11 pay_category_ids string 支付品类
12 pay_product_ids string 支付产品
13 city_id long 城市 ID

1.2 三类字段:共性、行为、维度

13 个字段按语义可分为三类:

共性字段(5 个):每行都有,与行为类型无关。

text 复制代码
date, user_id, session_id, page_id, action_time

行为字段(6 个):按行为类型分列,非当前行为的字段填"不适用"值。

行为 承载字段 不适用时填什么
搜索 search_keyword 字符串 null
点击 click_category_id、click_product_id 数值 -1
下单 order_category_ids、order_product_ids 字符串 null
支付 pay_category_ids、pay_product_ids 字符串 null

维度字段(2 个):可用于分组的维度。

text 复制代码
city_id, page_id   (page_id 已在共性字段中,也承担维度角色)

两个设计要点:

  1. 点击用 -1 表示"不适用",因为它是数值类型字段。
  2. 搜索/下单/支付用字符串 null 表示"不适用",因为原始日志是文本文件,字段必须有字面值。

1.3 三种"没有值"的语义区分

清洗过程中必须区分三种"看起来像没有值"的情况:

形式 例子 语义 处理
SQL NULL Hive 里的 NULL 真的没有 需判断
空字符串 "" 有字段但无内容 需判断
占位符 -1、字符串 null 不适用于本行 转 NULL 或保留

关键 :非点击行的 click_category_id = -1,语义是"这行不是点击",不是数据缺失。这两种语义在 DWD 里要分开标记。

1.4 行为判定规则与互斥声明

数据集的设计约定是:一行恰好命中 4 类行为中的 1 类,这就是"四类行为互斥"。

但这是设计上的声明,不是逻辑必然。可能存在:

  • 一行同时满足搜索和点击(埋点出错);
  • 一行 4 类都不命中(纯页面浏览)。

因此互斥必须在实际数据上验证,不能默认成立。

判定规则直接由字段语义推导:

text 复制代码
is_search = search_keyword != null && search_keyword != 'null' && search_keyword != ''
is_click  = click_category_id != -1 && click_product_id != -1
is_order  = order_category_ids != null && order_category_ids != 'null' && order_category_ids != ''
is_pay    = pay_category_ids != null && pay_category_ids != 'null' && pay_category_ids != ''

行为组合分类:

命中数 behavior_type 说明
1 search / click / order / pay 正常,符合设计
≥2 multi 互斥违规,保留但标记
0 unclassified 未分类,保留但标记

四位组合编码 (search, click, order, pay),如 0100 = 仅点击。


2. 实验架构:本地编译 + 集群运行

2.1 环境分层

本实验涉及两个环境:

本地开发环境(Windows)

text 复制代码
E:\Java_proj\MyHadoopB1\
├── pom.xml
├── src/main/java/com/bgroup/b01/
│   ├── CleanMapper.java
│   └── CleanDriver.java
└── target/
    └── b01-1.0.jar         ← Maven 构建产物

本地只负责编写并编译两个 Java 文件,生成 b01-1.0.jar。

集群运行环境(hadoop@master)

text 复制代码
/home/hadoop/
├── user_visit_action.txt    ← 原始数据(上传前)
├── b01-1.0.jar              ← 从本地上传的编译产物
└── b01/
    ├── sql/
    │   ├── 01_create_ods.hql
    │   ├── 02_create_dwd_tsv.hql
    │   ├── 03_create_dwd_orc.hql
    │   ├── 04_create_dirty.hql
    │   └── 05_create_subtables.hql
    └── output/
        └── b01_quality_report.csv

所有 HQL 脚本都写在集群上,通过 hive -f sql/xxx.hql 执行。

2.2 数据流全景

text 复制代码
原始日志文件
    ↓ hdfs dfs -put
HDFS ODS (/b_group/ods/user_action/)
    ↓ Hadoop MR (CleanDriver + CleanMapper)
HDFS DWD TSV (/b_group/dwd/user_behavior_tsv/)
    ↓ Hive 建外部表 + ORC 转换
Hive 表(8 张)

2.3 五张 Hive 表的依赖链

五张 HQL 表不是平铺的,而是有明确的依赖链:

text 复制代码
[b01] ODS 原始日志 (/b_group/ods/user_action/)
    │
    └── [01] 建 ods_user_action 外部表
             作用:直读原始 txt,用于行数守恒和原始查询

[b01] MR 输出 (/b_group/dwd/user_behavior_tsv/)
    │
    └── [02] 建 dwd_user_behavior_tsv 外部表
             作用:把 MR 输出的 TSV 映射成 Hive 表(25 列)
             │
             ├── [03] 建 dwd_user_behavior_orc
             │        作用:从 02 读,转 ORC + 多值字段 split 成数组
             │        产出:主 DWD 表,供 B03--B12 使用
             │        │
             │        └── [05] 建 4 张多值子表
             │                 作用:从 03 读,把数组 explode 成行
             │
             └── [04] 建 dwd_user_behavior_dirty
                      作用:从 02 读,过滤 is_true_missing = true 的行

执行顺序约束:

  • 01 独立,可先可后
  • 02 必须在 MR 完成后执行(MR 输出目录必须先存在)
  • 03、04 依赖 02
  • 05 依赖 03

3. 六条假设:从声明到实测

实验开始前,我们对这份数据有六个默认假设。有的来自数据集设计文档,有的来自经验。这一章逐条检验它们的命运。

3.1 H1 结构完整性

假设:13 字段、下划线分隔、结构规整,无空行、无字段数异常、无真缺失。

验证方式 :MR Counter 中 EMPTY_LINE、FIELD_COUNT_ERROR、TRUE_MISSING 三个计数器。

结果 :✅ 成立。

三个 Counter 全部为 0。结构层面零缺陷。

3.2 H2 行为互斥

假设:四类行为互斥,每行恰好命中 1 类。

验证方式 :Mapper 对每行计算 hits 和 combo,通过 COMBO_* 和 BEHAVIOR_MULTI / BEHAVIOR_UNCLASSIFIED Counter 汇总。

结果 :✅ 成立,且零例外。

COMBO_* 只出现 4 种(1000/0100/0010/0001),multi = 0,unclassified = 0。四类合计 180570 = TOTAL。这是用 Counter 严格证明的,不是抽样估计。

3.3 H3 日期分布:推翻"单日"假设

假设:数据只覆盖 2019-07-17 一天。

验证方式:第一次 MR 运行时,Mapper 中硬编码:

java 复制代码
if (!"2019-07-17".equals(f[0])) {
    context.getCounter("B01", "DATE_OUT_OF_RANGE").increment(1);
}

结果 :❌ 假设被推翻。

DATE_OUT_OF_RANGE = 163943,只有 6627 行是 7-17。进一步用 shell 查分布,发现实际覆盖 11 天(2019-07-17 ~ 2019-07-27)。

修正:把硬编码判断改为分布画像:

java 复制代码
context.getCounter("B01", "DATE_" + f[0]).increment(1);

重跑后,Counter 里直接得到 11 个 DATE_*,无需再单独查 Hive。

3.4 H4 品类-产品配对:推翻"配对"假设

假设 :order_category_ids 和 order_product_ids 一一对应,可以用 zip 配对成 (category_id, product_id)。

验证方式 :在 Hive 里按 size() 对比两个数组的长度。

结果 :❌ 假设被推翻。

11768 条 order 行中:

text 复制代码
size_equal =  2399   ← 只有 20% 两数组长度相等
size_diff  =  9369   ← 80% 长度不等

抽样看原始数据:

text 复制代码
品类 ["15","13","5","11","8"]   产品 ["99","2"]       ← 5 vs 2
品类 ["15","9","3"]             产品 ["30"]           ← 3 vs 1
品类 ["6"]                      产品 ["100","42","47"] ← 1 vs 3

结论 :这不是脏数据,而是语义理解错误。一次下单涉及"一组品类"和"一组产品",两者独立,不是配对关系。

修正 :DWD 不建 dwd_order_item 配对表,改拆四张独立子表。

3.5 H5 文件顺序 vs 时间顺序

假设:文件顺序 = 时间顺序,可以靠文件行号推断用户动作序列。

验证方式 :在 session 内,按 action_time 排序,看 raw_line_id 是否也单调:

sql 复制代码
WITH s AS (
  SELECT session_id, action_time,
         CAST(raw_line_id AS bigint) AS rid,
         LAG(CAST(raw_line_id AS bigint)) OVER (
           PARTITION BY session_id ORDER BY action_time
         ) AS prev_rid
  FROM dwd_user_behavior_orc
)
SELECT COUNT(*), SUM(CASE WHEN rid < prev_rid THEN 1 ELSE 0 END) FROM s;

结果 :❌ 假设被推翻。

text 复制代码
total_rows       = 180570
rid_disorder_cnt =   3908   (2.16%)

3908 行在 session 内时间倒流。这是分布式日志采集的天然特征。

修正 :B06 页面单跳转化率、B07 Session 序列分析必须先 ORDER BY action_time, raw_line_id,不能靠文件顺序推断用户动作序列。

3.6 H6 脏数据密度:远比预期干净

假设 :原始日志里 null、-1、空串混用,脏数据可能大量存在。

验证方式:MR 的字段级 Counter(13 字段 × 3 类异常值)+ 格式校验 Counter(时间、UUID、多值)。

结果 :❌ 假设被推翻。

所有格式校验 Counter 全部为 0:

text 复制代码
TIME_PARSE_ERROR      = 0
SESSION_ID_BAD_FORMAT = 0
MULTI_BAD_order_cat   = 0
MULTI_BAD_order_prod  = 0
MULTI_BAD_pay_cat     = 0
MULTI_BAD_pay_prod    = 0

真缺失 = 0,时间格式全部合法,session_id 全部是标准 UUID,多值格式规整。

这条推翻的意义:原本以为要花大力气清洗脏数据,实测发现数据质量远好于预期,可以把精力放在"语义澄清"和"画像"上。


4. MR 清洗程序

4.1 Mapper:字段级语义显式化

CleanMapper 是清洗的核心。它对每行做 5 件事:

  1. 结构校验:字段数是否 13,是否空行
  2. 真缺失检查:5 个关键字段是否有真缺失
  3. 字段值审计 :13 个字段各自的 null/-1/空串计数
  4. 行为判定 :根据字段语义判定 behavior_type 和 behavior_combo
  5. 输出语义显式化 :把 -1、字符串 null 转成 \N(Hive 侧变 SQL NULL),同时保留 is_*_field_used 区分"字段不适用"和"字段适用但值为空"

完整代码:

java 复制代码
package com.bgroup.b01;

import org.apache.hadoop.io.LongWritable;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Mapper;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

public class CleanMapper extends Mapper<LongWritable, Text, NullWritable, Text> {

    private final Text outValue = new Text();

    @Override
    protected void map(LongWritable key, Text value, Context context)
            throws IOException, InterruptedException {

        String line = value.toString();
        context.getCounter("B01", "TOTAL").increment(1);

        if (line == null || line.trim().isEmpty()) {
            context.getCounter("B01", "EMPTY_LINE").increment(1);
            return;
        }

        String[] f = line.split("_", -1);
        if (f.length != 13) {
            context.getCounter("B01", "FIELD_COUNT_ERROR").increment(1);
            return;
        }

        // ===== 1. 关键字段真缺失 =====
        boolean trueMissing = isBlank(f[0]) || isBlank(f[1]) || isBlank(f[2])
                || isBlank(f[3]) || isBlank(f[4]);
        if (trueMissing) context.getCounter("B01", "TRUE_MISSING").increment(1);

        // ===== 2. 字段值分布审计 =====
        for (int i = 0; i < 13; i++) {
            String v = f[i];
            if ("null".equals(v)) {
                context.getCounter("B01", "FIELD_" + (i + 1) + "_NULL").increment(1);
            } else if ("-1".equals(v)) {
                context.getCounter("B01", "FIELD_" + (i + 1) + "_NEG1").increment(1);
            } else if (v.isEmpty()) {
                context.getCounter("B01", "FIELD_" + (i + 1) + "_EMPTY").increment(1);
            }
        }

        // ===== 3. 行为判定 =====
        boolean isSearch = !isBlankOrNull(f[5]);
        boolean isClick  = !"-1".equals(f[6]) && !"-1".equals(f[7]);
        boolean isOrder  = !isBlankOrNull(f[8]);
        boolean isPay    = !isBlankOrNull(f[10]);

        int hits = 0;
        if (isSearch) hits++;
        if (isClick)  hits++;
        if (isOrder)  hits++;
        if (isPay)    hits++;

        String behaviorType;
        if (hits == 1) {
            behaviorType = isSearch ? "search" : isClick ? "click" : isOrder ? "order" : "pay";
        } else if (hits >= 2) {
            behaviorType = "multi";
            context.getCounter("B01", "BEHAVIOR_MULTI").increment(1);
        } else {
            behaviorType = "unclassified";
            context.getCounter("B01", "BEHAVIOR_UNCLASSIFIED").increment(1);
        }
        context.getCounter("B01", "BEHAVIOR_" + behaviorType.toUpperCase()).increment(1);

        String combo = (isSearch ? "1" : "0") + (isClick ? "1" : "0")
                     + (isOrder ? "1" : "0") + (isPay ? "1" : "0");
        context.getCounter("B01", "COMBO_" + combo).increment(1);

        // ===== 4. 时间审计 =====
        if (!isValidDateTime(f[4])) {
            context.getCounter("B01", "TIME_PARSE_ERROR").increment(1);
        }
        context.getCounter("B01", "DATE_" + f[0]).increment(1);

        // ===== 5. session_id 格式 =====
        if (!isUuid(f[2])) {
            context.getCounter("B01", "SESSION_ID_BAD_FORMAT").increment(1);
        }

        // ===== 6. 多值处理 =====
        String orderCat  = isOrder ? normalizeMulti(f[8],  context, "order_cat")  : "\\N";
        String orderProd = isOrder ? normalizeMulti(f[9],  context, "order_prod") : "\\N";
        String payCat    = isPay   ? normalizeMulti(f[10], context, "pay_cat")    : "\\N";
        String payProd   = isPay   ? normalizeMulti(f[11], context, "pay_prod")   : "\\N";

        // ===== 7. 质量标记 =====
        List<String> flags = new ArrayList<>();
        if (trueMissing) flags.add("TRUE_MISSING");
        if (hits >= 2)   flags.add("MULTI_BEHAVIOR");
        if (hits == 0)   flags.add("UNCLASSIFIED");

        // ===== 8. 输出 =====
        String out = String.join("\t",
                f[0], f[1], f[2], f[3], f[4].trim(), f[12],
                behaviorType, combo,
                isSearch ? f[5] : "\\N",
                isClick  ? f[6] : "\\N",
                isClick  ? f[7] : "\\N",
                orderCat, orderProd, payCat, payProd,
                isSearch ? "true" : "false",
                isClick  ? "true" : "false",
                isOrder  ? "true" : "false",
                isPay    ? "true" : "false",
                hits == 1 ? "true" : "false",
                hits >= 2 ? "true" : "false",
                hits == 0 ? "true" : "false",
                trueMissing ? "true" : "false",
                String.join(",", flags),
                String.valueOf(key.get())
        );

        outValue.set(out);
        context.write(NullWritable.get(), outValue);
    }

    // ===== 工具方法 =====
    private static boolean isBlank(String s) {
        return s == null || s.trim().isEmpty();
    }

    private static boolean isNullLiteral(String s) {
        return "null".equalsIgnoreCase(s == null ? "" : s.trim());
    }

    private static boolean isBlankOrNull(String s) {
        return isBlank(s) || isNullLiteral(s);
    }

    private static boolean isValidDateTime(String s) {
        if (isBlank(s)) return false;
        return s.matches("\\d{4}-\\d{2}-\\d{2} \\d{2}:\\d{2}:\\d{2}");
    }

    private static boolean isUuid(String s) {
        if (isBlank(s)) return false;
        return s.matches("[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}"
                       + "-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}");
    }

    private static String normalizeMulti(String s, Context ctx, String tag) {
        if (isBlankOrNull(s)) return "\\N";
        String[] parts = s.split(",", -1);
        StringBuilder sb = new StringBuilder();
        boolean hasBad = false;
        for (String p : parts) {
            String t = p.trim();
            if (t.isEmpty()) { hasBad = true; continue; }
            if (!t.matches("\\d+")) { hasBad = true; continue; }
            if (sb.length() > 0) sb.append(",");
            sb.append(t);
        }
        if (hasBad) ctx.getCounter("B01", "MULTI_BAD_" + tag).increment(1);
        return sb.length() == 0 ? "\\N" : sb.toString();
    }
}

Mapper 的输出是 25 列 tab 分隔文本,对应 DWD 表结构。

4.2 Driver:Tool + ToolRunner 写法

CleanDriver 负责作业配置。用 Tool + ToolRunner 而不是直接硬编码:

java 复制代码
package com.bgroup.b01;

import org.apache.hadoop.conf.Configuration;
import org.apache.hadoop.conf.Configured;
import org.apache.hadoop.fs.Path;
import org.apache.hadoop.io.NullWritable;
import org.apache.hadoop.io.Text;
import org.apache.hadoop.mapreduce.Job;
import org.apache.hadoop.mapreduce.lib.input.FileInputFormat;
import org.apache.hadoop.mapreduce.lib.output.FileOutputFormat;
import org.apache.hadoop.util.Tool;
import org.apache.hadoop.util.ToolRunner;

public class CleanDriver extends Configured implements Tool {

    @Override
    public int run(String[] args) throws Exception {
        if (args.length != 2) {
            System.err.println("Usage: CleanDriver <input> <output>");
            return 1;
        }

        Configuration conf = getConf();

        setIfAbsent(conf, "mapreduce.map.memory.mb", "512");
        setIfAbsent(conf, "mapreduce.map.java.opts", "-Xmx400m");
        setIfAbsent(conf, "mapreduce.task.io.sort.mb", "128");
        setIfAbsent(conf, "mapreduce.map.sort.spill.percent", "0.80");

        Job job = Job.getInstance(conf, "B01 Clean");
        job.setJarByClass(CleanDriver.class);
        job.setMapperClass(CleanMapper.class);
        job.setNumReduceTasks(0);
        job.setOutputKeyClass(NullWritable.class);
        job.setOutputValueClass(Text.class);

        FileInputFormat.addInputPath(job, new Path(args[0]));
        FileOutputFormat.setOutputPath(job, new Path(args[1]));

        return job.waitForCompletion(true) ? 0 : 1;
    }

    private static void setIfAbsent(Configuration conf, String key, String value) {
        if (conf.get(key) == null) conf.set(key, value);
    }

    public static void main(String[] args) throws Exception {
        int exitCode = ToolRunner.run(new Configuration(), new CleanDriver(), args);
        System.exit(exitCode);
    }
}

两个设计要点:

  • Tool + ToolRunner :命令行 -D key=value 会被 ToolRunner 自动解析进 Configuration,改参数不用重编译。
  • setNumReduceTasks(0):纯 map 作业,没有 reduce 阶段。这也是 2GB 小集群上能跑通的关键。

4.3 Counter:MR 里的免费画像工具

Counter 是 Hadoop 提供的分布式计数器:

  • 每个 task 里 increment,框架自动汇总
  • 作业结束后用 hadoop job -counters <jobId> 读取
  • 不占输出文件,不参与 shuffle,不影响主逻辑

这次 MR 一共用了 40+ 个 Counter 覆盖 16 个审计维度。这是 B01 数据画像的核心手段。

4.4 执行命令与 -D 参数位置

bash 复制代码
# 1 上传原始数据到 HDFS
hdfs dfs -mkdir -p /b_group/ods/user_action
hdfs dfs -put -f user_visit_action.txt /b_group/ods/user_action/

# 2 清空旧输出目录(幂等)
hdfs dfs -rm -r -f /b_group/dwd/user_behavior_tsv

# 3 执行 MR 清洗
hadoop jar b01-1.0.jar com.bgroup.b01.CleanDriver \
  -D mapreduce.map.memory.mb=512 \
  -D mapreduce.map.java.opts=-Xmx400m \
  -D mapreduce.task.io.sort.mb=128 \
  -D mapreduce.map.sort.spill.percent=0.80 \
  /b_group/ods/user_action \
  /b_group/dwd/user_behavior_tsv

# 4 验证输出行数
hdfs dfs -text /b_group/dwd/user_behavior_tsv/part-m-* | wc -l

注意:-D 参数必须放在 jar 名 + 主类名之后、两个路径参数之前。这是 ToolRunner / GenericOptionsParser 的硬性要求,放错位置参数会当路径解析。


5. Hive 建表与依赖链

5.1 五张 HQL 脚本的作用与依赖

01_create_ods.hql
sql 复制代码
CREATE DATABASE IF NOT EXISTS b_group;
USE b_group;

CREATE EXTERNAL TABLE IF NOT EXISTS ods_user_action (
  raw_line string
)
STORED AS TEXTFILE
LOCATION '/b_group/ods/user_action/';

作用 :把原始日志目录映射成 Hive 外部表。每行一个 raw_line 字符串,不做解析。用于行数守恒和原始查询。

02_create_dwd_tsv.hql
sql 复制代码
USE b_group;

CREATE EXTERNAL TABLE IF NOT EXISTS dwd_user_behavior_tsv (
  dt                string,
  user_id           bigint,
  session_id        string,
  page_id           bigint,
  action_time       string,
  city_id           bigint,
  behavior_type     string,
  behavior_combo    string,
  search_keyword    string,
  click_category_id bigint,
  click_product_id  bigint,
  order_category_ids string,
  order_product_ids  string,
  pay_category_ids   string,
  pay_product_ids    string,
  is_search_field_used boolean,
  is_click_field_used  boolean,
  is_order_field_used  boolean,
  is_pay_field_used    boolean,
  is_valid_behavior    boolean,
  is_multi_behavior    boolean,
  is_unclassified      boolean,
  is_true_missing      boolean,
  quality_flags        string,
  raw_line_id          string
)
ROW FORMAT DELIMITED
FIELDS TERMINATED BY '\t'
NULL DEFINED AS '\\N'
STORED AS TEXTFILE
LOCATION '/b_group/dwd/user_behavior_tsv/';

作用 :把 MR 输出的 TSV 目录映射成 Hive 外部表,25 列。LOCATION 指向 MR 输出,NULL DEFINED AS '\\N' 把 MR 输出的 \N 解析成 SQL NULL。第一列 dt 而非 date(date 是 Hive 保留字)。

03_create_dwd_orc.hql
sql 复制代码
USE b_group;

DROP TABLE IF EXISTS dwd_user_behavior_orc;

CREATE TABLE dwd_user_behavior_orc
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT
  dt, user_id, session_id, page_id, action_time, city_id,
  behavior_type, behavior_combo,
  search_keyword, click_category_id, click_product_id,
  split(order_category_ids, ',') AS order_category_ids,
  split(order_product_ids, ',')  AS order_product_ids,
  split(pay_category_ids, ',')   AS pay_category_ids,
  split(pay_product_ids, ',')    AS pay_product_ids,
  is_search_field_used, is_click_field_used,
  is_order_field_used, is_pay_field_used,
  is_valid_behavior, is_multi_behavior, is_unclassified, is_true_missing,
  quality_flags, raw_line_id
FROM dwd_user_behavior_tsv
WHERE is_true_missing = false;

作用 :把 TSV 外部表转成 ORC 列式存储,同时把 4 个多值字符串字段 split 成 array<string>。这是 B03--B12 使用的主 DWD 表。

04_create_dirty.hql
sql 复制代码
USE b_group;

DROP TABLE IF EXISTS dwd_user_behavior_dirty;

CREATE TABLE dwd_user_behavior_dirty
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT * FROM dwd_user_behavior_tsv WHERE is_true_missing = true;

作用 :把关键字段真缺失的行单独存到脏数据表,与主 DWD 表隔离。本例 is_true_missing = 0,所以脏表为空。保留作为"脏数据隔离"机制存在。

05_create_subtables.hql
sql 复制代码
USE b_group;

-- 订单-品类子表
DROP TABLE IF EXISTS dwd_order_category;
CREATE TABLE dwd_order_category
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT user_id, session_id, action_time, city_id,
       CAST(cat AS bigint) AS category_id
FROM dwd_user_behavior_orc
LATERAL VIEW explode(order_category_ids) t AS cat
WHERE behavior_type = 'order' AND order_category_ids IS NOT NULL;

-- 订单-产品子表
DROP TABLE IF EXISTS dwd_order_product;
CREATE TABLE dwd_order_product
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT user_id, session_id, action_time, city_id,
       CAST(prod AS bigint) AS product_id
FROM dwd_user_behavior_orc
LATERAL VIEW explode(order_product_ids) t AS prod
WHERE behavior_type = 'order' AND order_product_ids IS NOT NULL;

-- 支付-品类子表
DROP TABLE IF EXISTS dwd_pay_category;
CREATE TABLE dwd_pay_category
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT user_id, session_id, action_time, city_id,
       CAST(cat AS bigint) AS category_id
FROM dwd_user_behavior_orc
LATERAL VIEW explode(pay_category_ids) t AS cat
WHERE behavior_type = 'pay' AND pay_category_ids IS NOT NULL;

-- 支付-产品子表
DROP TABLE IF EXISTS dwd_pay_product;
CREATE TABLE dwd_pay_product
STORED AS ORC
TBLPROPERTIES ("orc.compress"="SNAPPY")
AS
SELECT user_id, session_id, action_time, city_id,
       CAST(prod AS bigint) AS product_id
FROM dwd_user_behavior_orc
LATERAL VIEW explode(pay_product_ids) t AS prod
WHERE behavior_type = 'pay' AND pay_product_ids IS NOT NULL;

作用:把主 DWD 表里的 4 个多值数组炸裂成子表,一行 = 一个"订单/支付中一个品类或产品"。供 B04、B10 等按品类聚合的分析使用。

关键点 :不建 dwd_order_item 配对表。原设计假设品类和产品一一配对,实测推翻(见 3.4 节),改为拆四张独立子表。

执行顺序
bash 复制代码
cd /home/hadoop/b01

hive -f sql/01_create_ods.hql
hive -f sql/02_create_dwd_tsv.hql
hive -f sql/03_create_dwd_orc.hql
hive -f sql/04_create_dirty.hql
hive -f sql/05_create_subtables.hql

5.2 外部表 + ORC 两级设计

为什么用"外部表 + ORC 表"两级:

  • 外部表 dwd_user_behavior_tsv 指向 MR 输出的原始目录,不拷贝数据,节省空间
  • ORC 表 dwd_user_behavior_orc 是真正查询用的表,压缩比约 9:1(29.8 MB → 3.3 MB)
  • 保留两层,是为了让 MR 输出可以被独立审计,不受 Hive 表结构变更影响

5.3 Derby 元数据的目录漂移

Hive 默认用内嵌 Derby 做元数据存储。Derby 的行为是:在当前工作目录下创建 metastore_db/。

也就是说:

  • 在 /home/hadoop/b01 跑 Hive,元数据在 /home/hadoop/b01/metastore_db
  • 在 /home/hadoop/b01/sql 跑 Hive,元数据在 /home/hadoop/b01/sql/metastore_db
  • 在 /home/hadoop 跑 Hive,元数据在 /home/hadoop/metastore_db

不同目录跑 Hive,等于不同的元数据库,之前建的表会"消失"。

这是初学者最容易踩的坑之一。本实验统一在 /home/hadoop/b01 下执行所有 Hive 命令。


6. 数据画像结果

6.1 结构完整性

MR Counter 第一次运行:

text 复制代码
TOTAL                 = 180570
EMPTY_LINE            =      0
FIELD_COUNT_ERROR     =      0
TRUE_MISSING          =      0
TIME_PARSE_ERROR      =      0
SESSION_ID_BAD_FORMAT =      0

结论:结构层面零缺陷。无空行,无字段数异常,无真缺失,时间格式全部合法,session_id 全部是标准 UUID。

6.2 行为互斥验证

MR Counter 结果:

text 复制代码
COMBO_1000 = 40232   ← 仅搜索
COMBO_0100 = 120519  ← 仅点击
COMBO_0010 = 11768   ← 仅下单
COMBO_0001 =  8051   ← 仅支付
                -----
                180570 = TOTAL

BEHAVIOR_MULTI        = 0
BEHAVIOR_UNCLASSIFIED = 0

用"字段 null 计数"反推行为数,与 Counter 完全一致:

行为 反推公式 结果 Counter
search 180570 − 140338 40232 40232 ✅
click 180570 − 60051 120519 120519 ✅
order 180570 − 168802 11768 11768 ✅
pay 180570 − 172519 8051 8051 ✅

行为分布的业务解读:

  • 点击占比 66.7%,最多
  • 搜索占比 22.3%
  • 下单占比 6.5%
  • 支付占比 4.5%

典型的电商漏斗形态,与后续 B03 用户行为漏斗分析的预期一致。

6.3 日期分布

11 天,180570 行:

text 复制代码
16820 2019-07-25
16773 2019-07-21
16772 2019-07-24
16705 2019-07-22
16694 2019-07-18
16636 2019-07-19
16627 2019-07-17
16615 2019-07-26
16610 2019-07-20
16518 2019-07-23
13800 2019-07-27   ← 偏低约 15~20%

这条分布有两个意义:

  1. 推翻"单日"假设(见 3.3 节),后续 B03--B12 可以做跨天趋势。
  2. 暴露 07-27 异常:比前 10 天低 15~20%,疑为数据截断。

6.4 日 × 行为交叉

text 复制代码
日期          search  click   order  pay
2019-07-17     3683   11105   1109   730
2019-07-18     3684   11239   1044   727
2019-07-19     3817   10945   1148   726
2019-07-20     3653   11114   1125   718
2019-07-21     3801   11221    978   773
2019-07-22     3743   11117   1101   744
2019-07-23     3641   10984   1122   771
2019-07-24     3701   11215   1072   784
2019-07-25     3786   11164   1110   760
2019-07-26     3606   11206   1078   725
2019-07-27     3117    9209    881   593

观察:

  • 前 10 天非常稳定,日间波动 < 5%
  • 07-27 所有行为类型同比偏低,疑为数据截断
  • 跨天趋势分析时,需标注或排除 07-27

6.5 维度画像

text 复制代码
distinct_user    =   100
distinct_session =  9457
distinct_city    =    26
distinct_page    =    50

关键洞察:

  • 只有 100 个用户。这不是"大数据",是"小样本长周期"。
  • 每用户平均 1806 条行为,平均 95 个 session
  • 后续 RFM、用户分层分析时,100 个用户的 KMeans 分层意义有限,需要报告这一约束

7. 数据处理的反复拉扯(专题)

这一章把整个实验里所有"试错---修正"的过程集中起来。每一次修正,都对应一个假设被推翻。

7.1 编码:GBK 不可映射字符

现象:Windows 下 Maven 编译 Java 文件时,报"编码 GBK 的不可映射字符"。

根因:源码是 UTF-8,Maven 默认编译编码在中文 Windows 下落到 GBK。

修正 :pom.xml 中加入:

xml 复制代码
<properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

并在 maven-compiler-plugin 里显式声明 <encoding>UTF-8</encoding>。

验证:用 Python 按 UTF-8 解码读源码,能正常显示中文注释。

7.2 日期:从硬编码到分布画像

现象 :第一次 MR 运行,DATE_OUT_OF_RANGE = 163943,看起来像大量脏数据。

根因 :Mapper 里硬编码了 if (!"2019-07-17".equals(f[0])),默认数据只有一天。

修正:改成分布画像:

java 复制代码
context.getCounter("B01", "DATE_" + f[0]).increment(1);

意义:

  • "越界"是价值判断,默认了"只有一天"的假设
  • "分布画像"是事实陈述,不预设
  • 在数据探索阶段,多画事实,少下判断

7.3 Hive 保留字:date → dt

现象:

sql 复制代码
SELECT split(raw_line,'_')[0] AS date, COUNT(*) FROM ods_user_action GROUP BY ...;
-- 报错:cannot recognize input near 'AS' 'date' ',' in selection target

根因 :date 是 Hive 保留字,不能作为列别名或列名。

修正:

  • DWD 表第一列改名为 dt
  • 所有查询统一用 dt
  • 用 sed 批量替换 HQL 脚本里的 date → dt

7.4 CTAS 限制:array<bigint> → array<string>

现象:

sql 复制代码
CAST(split(order_category_ids, ',') AS array<bigint>) AS order_category_ids

在 Hive 3.1.3 的 CREATE TABLE AS SELECT 中报:

text 复制代码
FAILED: ParseException line 11:41 cannot recognize input near 'array' '<' 'bigint'

根因 :Hive 3.1.3 CTAS parser 对嵌套 CAST(... AS array<bigint>) 的处理有限制。

修正 :改用 array<string>,在子表里再 cast 为 bigint。多值字段本以字符串存储,array<string> 天然匹配。

7.5 多值炸裂:从配对到独立

现象 :原 05 用 zip + posexplode 配对炸裂,得到 dwd_order_item = 4780,远少于 order 行数 11768。

根因 :zip 会在两个数组长度不一致时截断,隐藏了"品类与产品不配对"的语义错误。

修正:改拆四张独立子表:

text 复制代码
dwd_order_category = 35188
dwd_order_product  = 23597
dwd_pay_category   = 24147
dwd_pay_product    = 16096

意义:一次对数据语义的澄清,避免了后续所有分析(如 B10 品类共现)建立在错误前提上。

7.6 压缩:Snappy 与 Hive SerDe 的错位

现象 :第一次 MR 输出加了 Snappy 压缩,hdfs dfs -cat part-m-* | wc -l 得到 26875,与 180570 完全不符。

根因 :hdfs dfs -cat 是原样输出字节流,不是解压。wc -l 数的是压缩字节里的换行符。

修正:两个方案------

  • 用 hdfs dfs -text 自动解压
  • 或者干脆不用输出压缩(18MB 数据压缩收益极小),重跑 MR

最终选择:重跑 MR 时去掉压缩参数,简化后续 Hive 建表。


8. 验证与守恒

8.1 行数守恒

text 复制代码
ODS        = 180570
DWD_TSV    = 180570
DWD_ORC    = 180570
DIRTY      =      0

守恒式 :ODS = DWD_ORC + DIRTY + FIELD_COUNT_ERROR + EMPTY_LINE,即 180570 = 180570 + 0 + 0 + 0。

8.2 MR vs Hive 交叉验证

行为 MR Counter Hive 独立 SQL 一致
search 40232 40232 ✅
click 120519 120519 ✅
order 11768 11768 ✅
pay 8051 8051 ✅
multi 0 0 ✅
unclassified 0 0 ✅

MR 与 Hive 两条独立路径得到完全一致的结果,清洗逻辑可信度高。

8.3 质量报告

output/b01_quality_report.csv:

text 复制代码
metric            value
total_rows        180570
behavior_search   40232
behavior_click    120519
behavior_order    11768
behavior_pay      8051
multi_behavior    0
unclassified      0
true_missing      0
distinct_user     100
distinct_session  9457
distinct_city     26
distinct_page     50
dt_min            2019-07-17
dt_max            2019-07-27

9. 工程收获

这一章讲"下次做工程能更顺"。

9.1 Counter 的价值

Counter 是 MR 的免费画像工具:不占输出文件、不参与 shuffle、不影响主逻辑。

本实验用 40+ Counter 覆盖了 16 个审计维度,是所有画像结论的原始来源。

最佳实践:写 MR 时同步设计 Counter,把审计作为一等公民。

9.2 Tool + ToolRunner 的意义

用 Tool + ToolRunner 代替硬编码 Configuration:

  • 命令行 -D key=value 自动解析进 Configuration
  • 改参数不用重编译
  • 符合工程规范,便于运维传参

约束 :-D 必须放在 jar 名 + 主类名之后、应用参数之前。

9.3 Hive 工作目录一致性

内嵌 Derby 会在当前工作目录 创建 metastore_db/。不同目录跑 Hive,等于不同的元数据库。

最佳实践 :固定一个工作目录(如 /home/hadoop/b01)跑所有 Hive 命令。

长期方案:改用 MySQL 存储元数据,集中管理、不随目录漂移。

9.4 幂等性:输出目录先删

MR / Hive 输出目录已存在会直接报错。每次跑作业前必须:

bash 复制代码
hdfs dfs -rm -r -f <output_dir>

Hive 建表时用 DROP TABLE IF EXISTS 保证幂等。


10. 方法收获

这一章讲"下次想问题能更对"。

10.1 审计不是"发现问题",而是"检验假设"

本实验最核心的一条:审计不是被动地找问题,而是主动地检验假设。

原设计有 6 条假设,被推翻了 4 条:

  • 数据不是单日(11 天)
  • 品类与产品不配对
  • 文件顺序 ≠ 时间顺序
  • 脏数据密度远低于预期

每一条推翻,都对应一次设计调整。 这些"推翻"本身是最有价值的产出。

10.2 多值字段:先画像,再炸裂

不要一拿到多值字段就直接 explode。先画像,看形态。

原设计用 zip 配对,得到 4780 行,看起来"跑通了"。但实际是 80% 的数据被 zip 的长度截断悄悄丢掉了。跑通 ≠ 正确。

原则:

  1. 先跑 size() 分布,看两个数组形态
  2. 如果是配对关系,用 zip
  3. 如果是独立集合,用 explode,拆多张子表

10.3 文件顺序 ≠ 时间顺序

分布式日志采集,文件顺序由采集进程写入时间决定,不是事件发生时间。

推论 :任何依赖事件序列的分析(页面跳转、会话序列),必须显式 ORDER BY action_time,不能靠文件行号。

B01 发现 2.16% 的乱序比例。这个结论必须在 B06、B07 的序列分析中处理。

10.4 外部表 + ORC 的两级设计

MR 输出目录 → Hive 外部表 → ORC 表,是数据治理的常见模式:

  • 外部表指向原始目录,不改原始数据
  • ORC 表是查询用副本,结构可演进
  • 保留两层,MR 输出可独立审计,不受 Hive 表结构变更影响

收益:一次投入,长期可维护。


11. 遗留问题与已知未验证方向

11.1 07-27 数据疑截断

07-27 数据量比前 10 天低 15~20%。最可能是采集未完成。跨天趋势分析时需标注或排除。

未验证:能否通过当日 session 分布异常(如大量 session 只有一个事件)佐证"截断"。

11.2 3 组完全重复

3 组完全重复,都是"同一用户、同一秒、同一页面、同一搜索词"(如"吃鸡"、"内存")。占比 0.0017%。

未验证:是否是埋点重复上报,还是真实的双击行为。暂不处理。

11.3 100 用户样本量

只有 100 个用户。后续 B11 RFM 分层分析时,KMeans 在 100 个样本上分 5~8 群,统计意义有限。

建议:B11 用规则分层(如 R/F/M 打分法),不用 KMeans;或在报告中标注这一约束。

11.4 无维表

城市、品类、产品只有 ID,没有对应的名称/省份/品类树。

影响:

  • B09 城市/区域品类偏好分析需要自建小维表(26 个城市手工映射省份)
  • 品类共现分析(B10)只能按 ID 呈现,业务解读受限

未验证:能否从公开数据源补充城市映射。

11.5 B02 的改进方向

  1. 主表按 dt 分区:11 天数据,分区存储提升查询效率
  2. 新增 ts 字段 :action_time 转 timestamp 类型
  3. 新增 seq_in_session:session 内按时间排序的序号,一劳永逸解决乱序问题
  4. 子表补充 dt 列:便于按天聚合

12. 结语

B01 是 B 组实验的第一阶段。它完成了从原始日志到 DWD 明细层的完整落地,但更重要的是:它把"我以为"和"实际是"之间的差距记录了下来。

六条假设,两条成立,四条被推翻。每一条推翻,都对应一次设计调整------日期 Counter 从硬编码改为分布画像、多值子表从 1 张拆为 4 张、序列分析必须显式排序。

这些修正,才是 B01 对后续 B02--B15 的真正贡献:不是 "一份干净的 DWD 表",而是"一套已被验证的数据认知"。


附录 A:Counter 完整存档

text 复制代码
B01
    BEHAVIOR_CLICK=120519
    BEHAVIOR_ORDER=11768
    BEHAVIOR_PAY=8051
    BEHAVIOR_SEARCH=40232
    COMBO_0001=8051
    COMBO_0010=11768
    COMBO_0100=120519
    COMBO_1000=40232
    DATE_2019-07-17=16627
    DATE_2019-07-18=16694
    DATE_2019-07-19=16636
    DATE_2019-07-20=16610
    DATE_2019-07-21=16773
    DATE_2019-07-22=16705
    DATE_2019-07-23=16518
    DATE_2019-07-24=16772
    DATE_2019-07-25=16820
    DATE_2019-07-26=16615
    DATE_2019-07-27=13800
    FIELD_10_NULL=168802
    FIELD_11_NULL=172519
    FIELD_12_NULL=172519
    FIELD_6_NULL=140338
    FIELD_7_NEG1=60051
    FIELD_8_NEG1=60051
    FIELD_9_NULL=168802
    TOTAL=180570

附录 B:执行环境

项 版本
Hadoop 3.3.6
Hive 3.1.3
集群 1 master + 3 worker,每节点 2GB
Java 8

本文是 B01 阶段完整复盘,也是 B02--B15 的数据底座说明。

相关推荐
四季豆332 小时前
中翰软件调整战略定位:以数据与知识“双治理”搭建企业AI落地桥梁
大数据
T06205142 小时前
【2026年更新】全国自然保护区矢量数据,列表+边界+功能区
大数据
ha_lydms2 小时前
MaxCompute中JSON函数
大数据·数据库·阿里云·json·dataworks·maxcompute·odps
H0311169852 小时前
移动应用数据分析平台信息整理:月狐数据、七麦数据、蝉大师
大数据·人工智能
小宋10212 小时前
Jev 不是另一个聊天模型:Choice、Score、Boolean 与概率决策完整实战
大数据·人工智能
数字化顾问3 小时前
(136页PPT)BCGIDG资本中国某省市场Geneva项目商业尽职调查项目报告(附下载方式)
大数据·人工智能
故七月3 小时前
GEO 工程实践:企业官网结构化改造,提升大模型实体采信度
大数据·人工智能·geo
MiYi124063 小时前
2026 企业 AI 办公工具选型指南:从需求分析到任务交付的完整评估框架
大数据·人工智能
IT研究室4 小时前
最新大数据毕业设计选题推荐-基于大数据的全球地震数据分析与可视化-大数据-Spark-Hadoop-Bigdata
大数据·信息可视化·数据分析·spark·课程设计