DD 数据开发学习站DATA ENGINEERING
模块 08 · 平台与数据服务

用户画像

标签体系、长表与宽表、位图圈人、ID-Mapping 与隐私边界。

难度 · 进阶 约 110 分钟 选学 练习 8 项 计划内 刷题 0/3
建议先学:dbt 与 DataOps
本页目录 · 27 节

本章目标:能设计一套用户标签体系,用 SQL 算出统计型标签和生命周期标签,用位图做人群圈选,并说清 ID-Mapping 和隐私边界。 前置知识:进阶维度建模、实时数仓与 Flink 预计用时:110 分钟

运营常说“把近 30 天买过两次以上、客单价高于 200 的北京用户拉出来做活动”。这句话背后是一套用户画像系统:把散落在订单、行为、资料里的信息加工成标签,再按条件组合成人群。没有画像,每次活动都要数据开发临时写 SQL。

1. 为什么需要用户画像

用户画像解决三个问题:

  • 统一口径:什么叫“活跃用户”、什么叫“高价值用户”,全站只有一套定义。
  • 快速圈人:运营自助选择标签组合,分钟级得到人群包,不用排队等开发。
  • 可追踪效果:人群包有版本和快照,活动结束后能对比投放前后指标。

画像不是一张大宽表就完事。标签会频繁新增和修改,存储和计算方式要能支撑这种变化。

2. 核心概念

2.1 标签的三种来源

类型 怎么产生 例子
统计型 对行为或交易做聚合 近 30 天订单数、累计 GMV、最近购买距今天数
规则型 按业务规则打标 首单用户、退款用户、北京用户
模型型 模型预测打分 流失概率、购买倾向、生命周期阶段

统计型最基础,规则型最容易解释,模型型需要算法团队配合。先把统计型和规则型做扎实,不要一上来就上模型。

2.2 标签的存储形态

明细标签表(长表)
user_id | tag_code | tag_value | update_time
U01     | lifecycle | 沉睡复购   | 2024-03-10
U01     | city      | 北京       | 2024-03-10

画像宽表(宽表)
user_id | lifecycle | city | order_cnt_30d | gmv_180d
U01     | 沉睡复购   | 北京 | 2             | 200.00

长表便于新增标签和追溯,宽表便于查询和圈人。常见做法是长表做加工,宽表做服务,定期从长表刷新。

2.3 人群圈选与位图

人群是标签条件的组合,例如“北京 + 高价值活跃 + 近 30 天有下单”。如果每个条件都去扫宽表,数据量大时很慢。

位图把每个用户映射到一个 bit 位,一个标签值对应一个整数:

北京用户     01011010
高价值活跃   00110000
近 30 天下单 01111000
交集         00010000  → 第 5 位用户

按位与做交集、按位或做并集、按位取反做排除,圈人变成整数运算。ClickHouse 的 bitmap、Doris 的 BITMAP、Redis 的 BITCOUNT 都支持这种思路。

2.4 ID-Mapping

同一个人可能有三套 ID:设备号、手机号、业务账号。下单用账号,浏览用设备,活动页用手机号。不打通就会把一个人算成三个用户。

映射表结构:

id_type | id_value        | one_id
device  | dev-a1          | U100
phone   | phone_hash_001  | U100
account | acc-778         | U100

one_id 是全站统一用户 ID。映射要处理一对多冲突(一个手机号绑定多个账号)、历史合并和拆分,规则必须由业务确认。

2.5 隐私红线

画像系统大量使用个人信息,必须遵守最小必要原则:

  • 手机号、邮箱、身份证不存明文,存加盐哈希或加密值。
  • 可识别信息和标签数据分表存放,按权限申请访问。
  • 支持用户撤回授权,撤回后停止加工和投放。
  • 人群包只给统计结果或匿名 ID,不直接导出明文联系方式。

3. 动手示例

用 DuckDB 建一张小型订单表,完整走一遍“加工标签、生成宽表、位图圈人”。

3.1 准备用户行为数据

CREATE OR REPLACE TABLE user_orders (
    user_id VARCHAR,
    order_id VARCHAR,
    order_ts TIMESTAMP,
    amount DECIMAL(12, 2),
    status VARCHAR
);

INSERT INTO user_orders VALUES
    ('U01', '1001', TIMESTAMP '2024-01-01 10:00:00', 120.00, 'paid'),
    ('U01', '1002', TIMESTAMP '2024-01-15 12:00:00',  80.00, 'paid'),
    ('U02', '1003', TIMESTAMP '2024-01-02 09:00:00', 260.00, 'paid'),
    ('U03', '1004', TIMESTAMP '2024-02-03 20:00:00',  99.00, 'paid'),
    ('U02', '1005', TIMESTAMP '2024-02-10 11:00:00',  45.00, 'canceled'),
    ('U04', '1006', TIMESTAMP '2024-03-05 08:30:00', 310.00, 'paid'),
    ('U03', '1007', TIMESTAMP '2024-03-06 19:00:00',  75.00, 'paid'),
    ('U05', '1008', TIMESTAMP '2024-03-07 15:00:00', 150.00, 'paid'),
    ('U04', '1009', TIMESTAMP '2024-03-08 16:00:00', 220.00, 'paid'),
    ('U01', '1010', TIMESTAMP '2024-03-09 21:00:00',  60.00, 'canceled');

把统计日期固定为 2024-03-10,保证每次运行结果一致。

3.2 计算 RFM 指标

CREATE OR REPLACE TABLE user_rfm AS
SELECT
    user_id,
    DATE '2024-03-10' - MAX(CAST(order_ts AS DATE)) AS recency_days,
    COUNT(*) AS frequency,
    SUM(amount) AS monetary
FROM user_orders
WHERE status = 'paid'
GROUP BY user_id;

SELECT * FROM user_rfm ORDER BY user_id;

结果:

U01  55  2  200.00
U02  68  1  260.00
U03   4  2  174.00
U04   2  2  530.00
U05   3  1  150.00

只有 paid 订单参与计算,取消订单不计入频次和金额。

3.3 打生命周期标签

CREATE OR REPLACE TABLE user_profile AS
SELECT
    r.*,
    CASE
        WHEN recency_days <= 7 AND frequency >= 2 THEN '高价值活跃'
        WHEN recency_days <= 30 THEN '活跃'
        WHEN frequency >= 2 THEN '沉睡复购'
        ELSE '流失风险'
    END AS lifecycle_tag
FROM user_rfm r;

SELECT user_id, recency_days, frequency, monetary, lifecycle_tag
FROM user_profile
ORDER BY user_id;

按规则,U03、U04 是“高价值活跃”,U05 是“活跃”,U01 是“沉睡复购”,U02 是“流失风险”。阈值来自业务,不要照抄,要和运营确认。

3.4 生成位图并圈人

CREATE OR REPLACE TABLE user_bitmap AS
SELECT
    user_id,
    lifecycle_tag,
    1 << CAST(SUBSTR(user_id, 2) AS INTEGER) AS bit
FROM user_profile;

CREATE OR REPLACE TABLE crowd_high_value AS
SELECT
    bit_or(bit) AS crowd_bitmap,
    bit_count(bit_or(bit)) AS user_cnt
FROM user_bitmap
WHERE lifecycle_tag = '高价值活跃';

SELECT * FROM crowd_high_value;

结果是一个整数 24(第 3、4 位为 1)和人数 2。真实系统中用户 ID 会有几十亿,位图分桶存储,圈人时对各分桶做位运算再汇总。

3.5 ID-Mapping 查询

CREATE OR REPLACE TABLE id_mapping (
    id_type VARCHAR,
    id_value VARCHAR,
    one_id VARCHAR
);

INSERT INTO id_mapping VALUES
    ('device',  'dev-a1',        'U100'),
    ('phone',   'phone_hash_001', 'U100'),
    ('account', 'acc-778',        'U100'),
    ('device',  'dev-b2',        'U200');

SELECT one_id
FROM id_mapping
WHERE id_value = 'phone_hash_001';

查询返回 U100。上游所有行为表在写入画像前,都要先把各自的 ID 映射成 one_id,否则标签会分散在多个“假用户”上。

4. 常见坑

4.1 标签口径没有文档

“活跃用户”有人按登录算,有人按下单算,圈出来的人数差几倍。每个标签必须有名称、定义、计算逻辑、更新频率和负责人。

4.2 宽表无限加列

每来一个需求就加一列,宽表几百列,更新一次要重写全表。长表存标签、宽表只保留高频标签,冷门标签按需查询。

4.3 用最新标签回看历史

用户现在是“高价值”,不代表三个月前也是。做历史分析要用标签快照或按事件时间重算,不能用当前值倒推。

4.4 ID 映射冲突不处理

一个手机号对应多个账号,直接取第一条会把行为拼到错误的人身上。冲突要有优先级和人工确认机制,合并和拆分都要留审计记录。

4.5 人群包不可复现

运营今天圈了 10 万人,明天用同样条件变成 9.8 万,无法解释。人群包要保存标签版本、计算时间和完整条件。

4.6 明文导出个人信息

把手机号、地址直接导出给外部渠道,既违规也容易泄露。只输出匿名 ID 或加密标识,由投放系统在受控环境内完成匹配。

5. 在线练习

题目 考点
1141. 查询近 30 天活跃用户数 时间窗口与活跃口径
1454. 活跃用户 🔒 连续活跃与去重
1158. 市场分析 I 多表关联与空值处理

自测:

  • 能写出“最近一次购买距今天数”的聚合 SQL。
  • 能解释同一个用户为什么会被算成多个用户。

6. 面试延伸

面试题:用户画像系统怎么分层?

是什么:源数据层(订单、行为、资料)、标签加工层(统计/规则/模型)、标签存储层(长表、宽表、位图)、应用层(圈人、推荐、触达)。

解决什么问题:标签生产和标签使用解耦,新增标签不影响上层应用,圈人不用每次扫明细。

我的实操边界:本章用 DuckDB 加工了小型标签表和位图,未做过亿级用户的画像平台。

面试题:标签宽表和长表怎么选?

是什么:长表一行一个标签值,便于扩展和追溯;宽表一行一个用户,便于查询和圈人。

解决什么问题:长表解决“标签多、变化快”,宽表解决“查询快、组合方便”,通常两者配合。

我的实操边界:本章两种形态都建过,未做过宽表的自动刷新调度。

面试题:位图为什么适合人群圈选?

是什么:每个用户占一个 bit,标签值对应一个位图,交集、并集、排除都能用位运算完成。

解决什么问题:传统 JOIN 圈人要扫多张表,位图把集合运算压缩成整数运算,速度快、存储省。

我的实操边界:本章在 DuckDB 里用 bit_or 模拟,未使用 ClickHouse/Doris 的生产位图。

面试题:ID-Mapping 的难点是什么?

是什么:多渠道 ID 统一、一对多冲突、历史合并拆分、映射关系随时间变化。

解决什么问题:不统一 ID,同一个人会被重复计算,标签和行为链路断裂。

我的实操边界:本章设计了映射表结构,未实现概率匹配和实时合并。

7. 本章验收

  • 能说出统计型、规则型、模型型标签的区别,并各举一例。
  • 能用 SQL 算出用户最近购买天数、频次和累计金额。
  • 能按业务规则打生命周期标签,并解释每个阈值的含义。
  • 能用位图表示一个人群,并算出交集人数。
  • 能设计 ID-Mapping 表,并说明一对多冲突怎么处理。
  • 能列出至少三条画像系统的隐私保护措施。
本章任务0 / 8
本页目录