4、数据清洗与ETL:数据去重、数据校验、字段补齐、时间戳对齐、脏数据处理

埋点数据进了数仓,你以为就万事大吉了?

太天真了。我见过太多团队,辛辛苦苦埋了上百个事件,结果跑出来的报表全是「异常值」——日活比公司总人数还多,用户时长出现负数,事件ID乱码成火星文。说白了,埋点数据天生就是脏的。

这一章,我们就来聊聊怎么把这些「垃圾」洗成能用的「黄金」。我个人习惯把数据清洗和ETL看作一道流水线:进来的是原矿,出去的是精炼品。你想想看,如果这一步没做好,后面所有的分析、建模、策略,全都是在垃圾上盖大楼。

核心原则:数据清洗不是「尽可能多保留」,而是「有策略地丢弃和修复」。宁可数据量少一点,也要保证每一条记录都是可信的。

4.1 数据去重:别让同一条用户行为算两次

数据去重,听起来简单,做起来全是坑。我在项目中遇到过最离谱的一次:某个客户端因为网络重试机制写得不严谨,同一个「点击购买」事件在1秒内上报了7次。结果运营同学看着转化率报表,以为产品突然爆了,差点提前庆祝。

去重一般分两个层面:

  • 精确去重:基于事件ID或唯一标识(如UUID),完全相同的记录只保留一条。
  • 业务去重:在时间窗口内,相同用户+相同事件+相同属性,视为重复。

我建议优先使用事件ID去重。为什么?因为业务去重有风险——万一用户真的在1秒内连续点了两次呢?你把它去掉了,真实行为就丢了。

我的习惯:在埋点规范里就强制要求每条事件携带一个全局唯一的event_id(UUID格式)。这样ETL阶段直接按event_id做distinct,干净利落。

下面是一个简单的去重SQL示例:

-- 基于event_id的精确去重
CREATE TABLE cleaned_events AS
SELECT DISTINCT ON (event_id) 
    event_id,
    user_id,
    event_name,
    event_time,
    properties
FROM raw_events
WHERE event_id IS NOT NULL
ORDER BY event_id, event_time DESC;  -- 保留最新的一条

嗯,这里要注意:如果数据源本身没有event_id,那就得用多字段组合去重。但组合去重一定要加时间窗口,否则容易误杀。

4.2 数据校验:给数据上个「安检门」

数据校验,说白了就是检查数据是否符合预期。我把它分成三类:

校验类型 检查内容 常见问题
格式校验 字段类型、长度、格式是否符合规范 时间字段传了字符串"null"、手机号出现11个9
逻辑校验 数值是否在合理范围内、事件顺序是否合理 用户年龄=999、下单时间早于注册时间
完整性校验 必填字段是否为空、关联字段是否存在 事件缺少user_id、页面浏览事件没有page_url

我曾经接手过一个项目,发现「注册成功」事件里,有30%的记录user_id是空的。后来一查,是前端埋点代码在用户未登录状态下也触发了事件。这种数据如果不拦下来,后面做用户生命周期分析全是错的。

避坑指南:校验规则不要写死在ETL代码里。我建议用配置表管理,比如JSON格式的校验规则。这样业务变了,改配置就行,不用重新部署。

4.3 字段补齐:别让空值毁了你的分析

字段补齐,不是让你「瞎编数据」。而是基于已有信息,合理推断缺失值。

常见的补齐策略:

  • 默认值补齐:比如设备类型为空,填"unknown";国家/地区为空,填"未知"。
  • 衍生补齐:比如有IP地址,通过IP库反查出城市、运营商。
  • 上下文补齐:比如用户在一次会话中,前几个事件有设备信息,中间某个事件丢了,可以用会话内最近一条记录的设备信息来填充。

我个人最常用的是上下文补齐。举个例子:

-- 用会话内最近的非空设备信息补齐缺失值
WITH ranked_events AS (
    SELECT *,
           LAST_VALUE(device_type IGNORE NULLS) OVER (
               PARTITION BY session_id 
               ORDER BY event_time 
               ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
           ) AS filled_device_type
    FROM raw_events
)
SELECT * FROM ranked_events;

你想想看,如果直接丢弃那些设备类型为空的事件,可能就丢了20%的数据。但用上下文补齐,能救回大部分。

注意:补齐一定要记录日志。我习惯在每条数据上加一个字段叫fill_status,标记哪些字段是被补齐的。这样分析时可以选择「只看原始数据」或「包含补齐数据」。

4.4 时间戳对齐:跨时区、跨系统的「时间统一战」

时间戳对齐,是埋点数据清洗里最容易被忽视、但后果最严重的环节。

为什么会这样?因为用户可能在东八区打开App,事件经过CDN、负载均衡、后端服务,最终落库的时间可能已经变成了UTC。更麻烦的是,有些客户端上报的是本地时间,有些上报的是服务器时间,混在一起根本没法做时序分析。

我建议统一遵循以下规则:

  1. 所有时间戳统一转为UTC+0存储。展示时再根据用户时区转换。
  2. 区分事件时间和接收时间。事件时间是用户行为发生的时刻,接收时间是服务器收到数据的时刻。两个都要保留。
  3. 做时间戳合理性校验。比如事件时间不能晚于接收时间(除非有时区问题),不能早于当前时间24小时以上。

下面是一个时间对齐的示例流程:

-- 时间戳标准化
UPDATE cleaned_events
SET 
    event_time_utc = CASE
        WHEN timezone = 'Asia/Shanghai' THEN event_time_local - INTERVAL '8 hours'
        WHEN timezone = 'America/New_York' THEN event_time_local + INTERVAL '5 hours'
        ELSE event_time_local  -- 默认视为UTC
    END,
    receive_time_utc = receive_time_raw AT TIME ZONE 'UTC';

避坑指南:我曾经遇到过一个坑——客户端上报的时间戳是毫秒级,但服务端解析时当成了秒级,结果所有事件时间都变成了1970年。所以,一定要在ETL入口处做时间戳单位校验:如果时间戳小于1e12,大概率是秒级,需要乘以1000。

4.5 脏数据处理:该扔就扔,该修就修

脏数据,就是那些「一眼假」的记录。比如:

  • 用户ID为null或"undefined"
  • 事件名称为空字符串或乱码
  • 数值字段出现负数(比如页面停留时间=-100秒)
  • 枚举字段出现未定义的值(比如性别字段出现"火星")

我的处理原则是「三层过滤」:

层级 处理方式 适用场景
第一层:硬过滤 直接丢弃,记录到异常日志 关键字段为空、格式完全错误
第二层:软修复 尝试修复,修复失败再丢弃 时间戳单位错误、编码问题
第三层:标记保留 保留数据,但打上脏数据标签 数值轻微异常、枚举值可映射

举个例子:

-- 脏数据处理示例
WITH filtered AS (
    SELECT *,
           CASE
               WHEN user_id IS NULL OR user_id = '' THEN 'DROP'
               WHEN event_name ~ '[^\x20-\x7E]' THEN 'REPAIR'  -- 含非ASCII字符
               WHEN stay_duration < 0 THEN 'TAG_DIRTY'
               ELSE 'CLEAN'
           END AS data_quality
    FROM raw_events
)
SELECT * FROM filtered
WHERE data_quality != 'DROP';  -- 只保留可修复和干净的

嗯,这里要强调一点:脏数据不能直接删了事。我习惯把丢弃的数据写入一张rejected_events表,定期review。有时候你会发现,某些「脏数据」其实是业务新需求导致的,比如新增了一个埋点字段但校验规则没更新。

我的经验:数据清洗不是一次性工作。每次上线新埋点、改版旧埋点,都要重新审视清洗规则。我建议每两周review一次清洗日志,看看有没有新的脏数据模式出现。

4.6 本章知识体系总览

下面这张图,把数据清洗与ETL的核心流程串起来了。你可以把它当作操作手册:

数据清洗与ETL核心流程 数据清洗与ETL核心流程 原始埋点数据 ① 数据去重 ② 数据校验 ✅ 校验通过 ❌ 校验失败 ③ 字段补齐 ④ 脏数据处理 ⑤ 时间戳对齐 丢弃 / 修复 / 标记 记录到异常日志 ✅ 清洗后数据

这张图把整个流程串成了流水线:原始数据进来,先过去重,再过校验。校验通过的走字段补齐和时间戳对齐;校验失败的走脏数据处理,该扔扔、该修修、该标记标记。最终,两条支流汇聚成清洗后的可信数据。

数据清洗这件事,说白了就是「信任但验证」。你不能假设埋点团队上报的数据都是完美的,也不能假设所有脏数据都能修好。保持敬畏心,做好每一层过滤,你的分析模型才能站得稳。