简介:这份《用户画像基础》PDF聚焦互联网行业用户画像的完整知识框架,适合数据产品、数据分析与运营人员系统入门。内容从画像定义与标签体系讲起,覆盖统计类、规则类、机器学习挖掘类三类标签,并延伸到数仓分层、Spark/Hive/HBase/MySQL/Redis/Elasticsearch等基础设施,以及用户画像八大模块与项目开发上线流程。资源共1个文件,类型为PDF,压缩包大小约795KB,便于随时翻阅。目前已有1015人学习下载,可作为搭建用户画像体系、规划标签开发任务和开展精准运营的参考手册,尤其适合需要理解数据如何从仓库走向业务应用的学习者。
1. 用户画像基础是什么:先解决“标签打架”问题
做过三年以上的数仓或推荐工程师,多半见过这种场景:运营要“高价值用户”,数据团队从订单侧给出高消费用户,从访问侧又给出高频活跃用户,两批标签词面相近,人群重合率却不到六成。问题不出在算法,而出在“用户画像基础”没打牢。用户画像基础不是拉一张宽表、堆几十个特征,而是把用户标识、标签口径、计算时效、访问性能串起来的一套规范。本文沿着这个标题,从数据底座、标签体系、批量计算到线上存储,讲清一套可复现的画像基础方案。读者如果正在搭画像平台,或者被业务疑问追问到怀疑人生,本文能给你一张完整的地图。
2. 用户画像基础的数据底座:ID打通与数据分层
在建立任何标签之前,先要回答一个问题:一个用户到底是谁。移动端场景下,同一个用户可能用手机号登录过一次,老版本App里一直以device_id匿名访问,后来微信授权又生成一个新的业务ID。如果只按单一ID聚合,画像宽表里会有一模一样的物理用户出现多行,标签也随之分裂。
2.1 先梳理用户标识:u_id、device_id、cookie
常见的平台内标识有登录用户ID、设备ID、Cookie ID、手机号、第三方OpenID。它们产生的场景不同,可信度也不同。建议先把这些标识列成一张枚举表,再决定映射关系。
| 标识 | 产生场景 | 更新频率 | 可信度 | 典型问题 |
|---|---|---|---|---|
| userId | 注册/登录 | 低频 | 高 | 未登录用户无此标识 |
| device_id | App首次启动 | 固定不变 | 中高 | 可被清缓存或刷机识别为新人 |
| cookie_id | Web/H5访问 | 会话级 | 低 | 浏览器清Cookie即丢失 |
| 手机号 | 下单/绑定 | 极低 | 高 | 换号后影响连续归因 |
| openid | 微信/支付宝授权 | 低频 | 中 | 同一人在不同开放平台id不同 |
我的习惯是:在ODS层保留原始标识,不直接修改;在DWD层建一张dim_user_id_mapping映射表,以userId为主键,把device_id、cookie_id都指向这个主键。未登录用户先以device_id作为占位主键,等业务侧触发登录后再合并身份。这样既能上报匿名数据,又能保证登录后画像连续。
2.2 数仓分层与标签宽表设计
画像基础表通常放在DWS层,因为它属于轻度汇总的公共维度数据。数据链路建议是:ODS统一收集业务日志 → DWD清洗成行为明细 → DWS加工用户维汇总层 → ADS面向应用提取标签。宽表不是越宽越好,字段过多会降低查询效率,也会让下游依赖变得脆弱。
下面是一张最小可用的日级画像宽表DDL,覆盖了事实指标和简单标签:
CREATE TABLE dws.user_profile_daily ( user_id STRING COMMENT '统一用户ID', first_seen_date STRING COMMENT '首次出现日期', last_active_date STRING COMMENT '最近活跃日期', order_cnt_30d BIGINT COMMENT '近30天下单次数', order_amt_30d DECIMAL(12,2) COMMENT '近30天下单金额', tag_value_level STRING COMMENT '价值分层标签', tag_channel_prefer STRING COMMENT '渠道偏好标签' ) PARTITIONED BY (dt STRING COMMENT '日期分区') STORED AS PARQUET;之所以用PARTITIONED BY (dt),是为了按天刷新全量快照,便于下游按分区读取。存储使用Parquet,一方面压缩比高,另一方面按列裁剪能减少查询IO。宽表里的字段分三类:用户身份字段、事实聚合字段、派生标签字段。事实字段尽量保留原始聚合值,派生标签则可以随时由事实字段重算。
2.3 最小可跑的ID-Mapping SQL
下面这个SQL例子,可以把登录日志和设备注册表合并成统一映射。它只处理两类来源,但思路可以扩展:
INSERT OVERWRITE TABLE dwd.dim_user_id_mapping SELECT COALESCE(a.user_id, b.reg_user_id) AS user_id, COALESCE(a.device_id, b.device_id) AS device_id, CURRENT_TIMESTAMP() AS update_time FROM ( SELECT device_id, MAX(login_user_id) AS user_id FROM dwd.app_login_log WHERE dt = '${bizdate}' GROUP BY device_id ) a FULL OUTER JOIN ( SELECT device_id, user_id AS reg_user_id FROM dwd.app_device_register WHERE dt = '${bizdate}' ) b ON a.device_id = b.device_id;逻辑说明:子查询a从登录日志中取每个设备最近登录过的一个用户ID,子查询b从设备注册表取注册用户ID。FULL OUTER JOIN保证只出现过一次访问行为的设备也能得到一行映射。COALESCE两边都适用,因为JOIN的两侧都保留device_id。实际生产环境比这复杂得多,还要处理一台设备多人使用、多个设备对应一个用户等情况,但核心思想就是先把已知登录行为作为可信证据,再逐步用规则合并。
3. 用户画像基础的标签体系:从维度到权重的设计
ID打通之后,画像基础的核心就是标签体系。很多团队的标签表里几百个字段,但业务方不知道每个字段的准确含义,数据团队自己也不敢改口径。要避免这种局面,得从标签分类、命名、生命周期三个维度先定规矩。
3.1 标签的三种类型:事实、规则、模型
用户画像标签可以从加工程度上分成三类。事实标签直接从行为明细聚合而来,规则标签基于逻辑判断,模型标签则需要训练或打分。三者各有取舍:
| 类型 | 典型示例 | 更新频率 | 优点 | 缺点 |
|---|---|---|---|---|
| 事实标签 | 最近购买时间、累计消费金额 | 日级/小时级 | 准确、可解释 | 相对用户意图滞后 |
| 规则标签 | 高价值用户、沉睡用户 | 日级 | 口径灵活、易调整 | 阈值依赖运营经验 |
| 模型标签 | 价格敏感度、流失概率 | 周级/月级 | 覆盖广、预测性强 | 需要训练评估和模型维护 |
我在项目里偏向把事实标签和规则标签优先做,因为它们在早期就能快速产生业务价值。模型标签要等数据积累到一定量级再上,否则样本太少,模型输出的分数会震荡。
3.2 标签命名、口径与生命周期
标签命名是团队协作的基础。推荐格式为:{业务域}_{标签名}_{周期}_{类型}。例如trade_order_amt_30d_fact表示交易域近30天下单金额事实标签,trade_value_level_rule表示交易域价值分层规则标签。这种命名让数据字典可以自动关联,也方便下游按前缀权限管控。
标签口径必须沉淀成文档。我的习惯是维持一张“标签口径表”,字段包含标签名、中文名、定义描述、口径SQL、负责人、更新状态。这样当业务方质疑“为什么用户A是高价值,用户B不是”时,能直接打开文档看到判断阈值和SQL,而不是听口头解释。
生命周期也要区分对待:日更新标签跑T+1任务;月更新标签只做月末调度;一次性标签必须带版本号,避免下次跑数把历史人群刷掉。无论哪种,都要保证万一任务失败时,上一份数据不被立刻覆盖。
3.3 用Python生成标签权重衰减
模型类标签经常需要对用户行为做时间衰减,否则三个月前一次大额购买会永远把用户顶到“高价值”。一个常用的衰减函数是半衰期指数衰减:
import math def decay_weight(days_ago: int, half_life: int = 30) -> float: """按距离今天的天数计算事件权重。 half_life=30 表示30天前的事件权重为0.5,60天前为0.25。""" return 0.5 ** (days_ago / half_life)使用示例:计算某用户对“3C数码”的加权兴趣分,浏览记0.3分、加购记0.6分、下单记1.0分,再乘以时间衰减系数:
action_score = { "view": 0.3, "cart": 0.6, "order": 1.0, } user_actions = [ (3, "order"), # 3天前下单 (12, "cart"), # 12天前加购 (45, "view"), # 45天前浏览 ] score = sum( decay_weight(days_ago, half_life=30) * action_score[action] for days_ago, action in user_actions ) print(f"全部行为加权后的兴趣分: {score:.3f}")逻辑说明:decay_weight的底数取0.5,意味着每个半衰期周期权重减半。half_life是核心参数,调小会让衰减过快,适合短期促销活动;调大则保留较长历史记忆,适合用户生命周期价值预测。要注意,这种连续衰减对周期性行为(比如每周固定购物)不友好,那种场景建议先做“周期内归一化”,再叠加周期间衰减。
4. 用户画像基础的计算与存储:批量跑数与线上读取
标签定义完之后,就要考虑怎么算、怎么存。画像基础数据的消费者一般分两类:离线报表、数据开发,他们能接受T+1延迟;线上推荐和实时风控,他们要求毫秒级拿到标签。所以计算和存储要分层设计。
4.1 用Spark SQL从明细加工标签宽表
假设DWD已有订单明细表dwd.fact_order_detail,可以用Spark SQL生成前面那张宽表。下面是一个典型模板:
INSERT OVERWRITE TABLE dws.user_profile_daily PARTITION (dt = '${bizdate}') SELECT t.user_id, MIN(t.order_date) AS first_seen_date, MAX(t.order_date) AS last_active_date, COUNT(IF(t.order_date >= date_sub('${bizdate}', 30), t.order_id, NULL)) AS order_cnt_30d, SUM(IF(t.order_date >= date_sub('${bizdate}', 30), t.amount, 0)) AS order_amt_30d, IF(SUM(IF(t.order_date >= date_sub('${bizdate}', 30), t.amount, 0)) > 5000, 'high', 'normal') AS tag_value_level FROM ( SELECT user_id, order_id, amount, order_date FROM dwd.fact_order_detail WHERE dt >= date_sub('${bizdate}', 90) ) t GROUP BY t.user_id;逻辑说明:COUNT和SUM里的IF不是过滤,而是在用户维度内做条件聚合。放在WHERE里会把没有近30天订单的用户整行滤掉,导致宽表缺失“沉睡用户”类标签。外层查询拿到90天明细,指标只统计近30天,这样既能保留所有有历史行为的用户,又不会让30天外的数据污染指标。
4.2 画像结果落到HBase和Redis
离线宽表适合数据分析师跑SQL,不适合线上服务做高并发读取。常见做法是把画像标签灌入HBase,rowkey采用反转的user_id。
HBase表:profile_daily rowkey:reverse(user_id) -- 示例:10091220 -> 02219001 column family:attr qualifier:order_cnt_30d, tag_value_level, tag_channel_prefer TTL:按实际业务保留30~60天rowkey反转是HBase常用技巧,让前缀散列在Region上,避免连续userId写爆同一个Region。对毫秒级读取要求更高的标签,可以再同步一份到Redis:
redis-cli SET profile:10091220 '{"order_cnt_30d":3,"tag_value_level":"high"}' EX 86400这里的EX 86400表示缓存一天,配合离线任务凌晨刷新,白天服务读取Redis时不会命中过期数据。如果标签多且频繁访问,建议改成Redis Hash,一次性HMGET多个字段,减少网络往返。
4.3 调度与参数调优
批处理任务通常要挂在调度平台上。如果用crontab加Spark提交,一个最小化的任务形如:
30 2 * * * spark-submit \ --master yarn \ --deploy-mode client \ --executor-memory 8G \ --executor-cores 4 \ --num-executors 20 \ --conf spark.sql.shuffle.partitions=400 \ profile_daily_job.py --bizdate $(date -d 'yesterday' +%F)几个关键参数说明:--executor-memory 8G和--executor-cores 4决定每个Executor的计算资源,--num-executors 20控制并行度;spark.sql.shuffle.partitions直接影响JOIN和GROUP BY的reduce数量,设置过小会OOM,设置过大会产生大量小文件。小表数据可以先广播,例如在Spark代码里加spark.sql.autoBroadcastJoinThreshold配置,把维表大小阈值调高些。
此外,建议每天对画像任务做“新数据量和空分区”巡检。空分区常常意味着上游日志停送,如果忽略,画像会静默变差。
5. 用户画像基础的验证:覆盖率和准确率怎么算
标签表和指标表不同,指标有业务原始定义,标签则由多层加工产生,一旦口径错了,下游很难察觉。因此在输出给应用方之前,至少要看两个指标:覆盖率和准确率。
5.1 覆盖率:画像覆盖了多少用户
覆盖率的定义不能简单写成COUNT(*) / 总用户数。分母不同会得到完全相反的结论。我一般按业务用途区分分母:用于站内推荐的分母取近30天有访问行为的活跃用户;用于短信营销的分母取近30天有真实手机号的用户;用于全量统计的分母取注册表全部用户。下面这个SQL按价值标签计算覆盖率:
SELECT tag_value_level, COUNT(*) AS tagged_user_cnt, COUNT(*) / MAX(total_ucnt) AS coverage_rate FROM ( SELECT p.user_id, p.tag_value_level, (SELECT COUNT(DISTINCT user_id) FROM dws.user_profile_daily WHERE dt = '${bizdate}') AS total_ucnt FROM dws.user_profile_daily p WHERE dt = '${bizdate}' ) t GROUP BY tag_value_level;逻辑说明:内层子查询把总用户数和每个用户行绑定在一起,MAX(total_ucnt)在GROUP BY后只取到同一个总数,不会因分组被放大。coverage_rate可以比较不同标签之间的填充度,如果某标签覆盖率低于预期,要回查是数据源缺失,还是加工逻辑里的过滤条件太严格。
5.2 准确率:抽检回来了
覆盖率说明标签有没有,准确率说明标签对不对。实际操作中不可能校验全量,通常按用户ID分层随机抽取100~200个样本,由业务方对每个标签做“是/否/存疑”的三分类判断。判断结果回来后,用Python快速算一个准确率:
import random check_result = [] # 模拟抽检结果,实际应来自人工标注 # 每条记录格式: {"user_id": "1001", "pred": "high", "label": "high"} correct = sum(1 for r in check_result if r["pred"] == r["label"]) accuracy = correct / len(check_result) print(f"抽检准确率: {accuracy:.2%} 抽检量: {len(check_result)}")这里最容易被忽略的是“存疑”样本。如果一个标签在业务方眼里经常“存疑”,说明标签定义对业务无感,要么改定义,要么拆成多个更细的标签。准确率抽检不能替代口径文档,它是口径文档的日常校验手段。
5.3 用A/B测试评估画像效果
标签好不好,最硬的标准是它能否驱动业务指标。常见做法是做一个面向策略的小流量A/B测试:实验组使用画像标签分流,控制组保持原有策略。假设要验证“高价值用户标签”的优惠券转化效果,最后得出四格表,可以用卡方检验判断差异显著性:
from scipy.stats import chi2_contingency # 每行是 [消费人数, 未消费人数] table = [ [experiment_convert, experiment_non_convert], [control_convert, control_non_convert], ] chi2, p_value, _, _ = chi2_contingency(table) print(f"p_value = {p_value:.4f}")解释:p_value < 0.05说明实验组和对照组差异不是随机波动,画像标签对业务有可度量影响;反之则说明该标签目前没有实际价值,需要调标签侧策略,而不是急着推广。让画像标签直接背业务指标很危险,建议观察至少一个完整的业务周期,避免新功能带来的短期波动。
6. 把用户画像基础沉淀成PDF:从标签文档到可评审的交付物
数据文档比代码更容易过期,但用户画像基础恰恰需要一份跨团队能评审、能归档的静态文档。我习惯把所有标签口径表、ID映射说明、宽表DDL和验证结果放在同一个Markdown文件里,再转换成PDF版本,文件名带上日期和版本号。
生成PDF最省事的方式是用pandoc加xelatex引擎。在装有TeX发行版的Linux或macOS环境中,运行下面命令即可:
pandoc user_profile.md \ --pdf-engine=xelatex \ -V mainfont="Noto Sans CJK SC" \ -V monofont="Noto Sans Mono CJK SC" \ -V geometry:margin=2.5cm \ -o 用户画像基础_v1.0_20250608.pdf参数说明:--pdf-engine=xelatex负责把Markdown里的中文内容编译成PDF;mainfont和monofont指定中文字体;geometry:margin=2.5cm控制页边距,文档内容多时可以把边距调小,内容少时调大。如果不想安装TeX,也可以先用pandoc生成HTML,再用wkhtmltopdf:
wkhtmltopdf --enable-local-file-access --footer-center '[page]' user_profile.html 用户画像基础.pdf生成PDF后,建议把每一张标签口径表截图放进去,并在文档末尾附上最近一次覆盖率验证结果。这样团队评审时不需要打开数仓,单靠一份PDF就能统一对“用户画像基础”的理解,后续人事变动时也能保证知识和业务口径不流失。
本文还有配套的精品资源,点击获取