用户画像平台设计

不管在任何公司,我们都需要努力了解我们的用户,以便为他们提供更优质的服务。APP内容推荐需要根据用户特征来决定推送内容;促销活动需要针对不同的用户群体设计不同的活动方案;线上产品售卖也需要了解用户喜好,才能更好地把产品卖给用户。

为了实现这些目标,我建立了一个用户画像平台。本文将首先探讨平台的功能需求和标签体系定位,然后介绍平台的架构和具体功能实现。

功能

用户画像平台主要关注分析场景,主要的使用者是公司各个业务线的运营人员和数据分析师。在平台的第一阶段,主要支持以下几个功能:

  1. 标签定义:标签是描述用户的一个属性,比如「使用的设备类型」、「居住地」或「年龄范围」等。
  2. 用户群体选择:选择一组用户标签及其相应的标签值,找出符合条件的用户群体。比如,寻找「居住在上海,且使用安卓设备」的用户。
  3. 用户画像:对于选定的用户群体,查看其标签分布。例如,查看「居住在上海,且使用安卓设备」的用户的年龄范围分布。

标签体系

在明确用户画像平台的使用场景和主要功能后,我们进一步回溯并确定用户标签体系。用户标签可以从两个方面进行分类:标签的实时性和标签的值类型。

首先考虑标签的实时性。由于用户画像平台的主要功能是「用户群体筛选」和「用户画像查看」,这两个功能并不需要极高的实时性,因此实时标签的优势并不明显。T+1的非实时标签完全能够满足数据分析师和运营人员的需求。

接着考虑标签的值类型,也就是标签是枚举的还是非枚举的。枚举标签,顾名思义,指的是标签值可以列举的标签,例如设备类型、网络类型、国家、城市等,这类标签在用户群体筛选中发挥了重要作用。非枚举标签则指标签值可以无限增长的标签,比如活跃天数、注册日期等,这类标签主要用于展示用户信息。考虑到「用户群体筛选」是各业务线最急迫的需求,我们在第一阶段决定不支持非枚举标签功能。

因此,我们确定了用户画像平台的第一阶段标签体系为非实时的枚举标签,主要为「用户群体筛选」和「用户画像查看」这两个查询功能服务。

架构与实现

在架构上,用户画像平台分为两个模块:数据写入,分析查询。

Roaringbitmap简介

接下来,我将简要介绍一种高效的位图压缩方法——Roaringbitmap。首先,我们来看一个问题:

假设我们有一个包含40亿个不重复且位于[0,2^32-1]范围内的整数集合,如何快速判断一个数是否在这个集合中?

如果我们直接存储这40亿个数,需要消耗约14.9GB的内存,这显然是不可接受的。因此,我们可以使用位图(bitmap)进行存储,即第0位表示数字0,第1位表示数字1,以此类推。如果一个数在原集合中,我们就将其对应的位图位设置为1,否则保持为0。这样,我们可以方便地查询结果,只需要占用512MB的内存,仅为原来的3.4%。但这种方法也存在缺点:例如,如果我们需要存储从1到5000万的5000万个连续整数,使用普通的位图仍需要消耗512MB的存储,显然,对于这种情况,我们有很大的优化空间。2016年,S. Chambi、D. Lemire、O. Kaser等人在论文《Better bitmap performance with Roaring bitmaps》和《Consistently faster and smaller compressed bitmaps with Roaring》中提出了Roaringbitmap,其主要特性是可以大幅度节省存储并提供快速的位图计算,因此,我们可以考虑使用它进行优化。对于前面提到的存储连续的5000万个整数,只需要几十KB

Roaringbitmap的主要思想是:将32位无符号整数按照高16位进行分桶,即最多可能有2^16=65536个桶,论文中称之为container。

img

在存储数据时,根据数据的高16位找到对应的container(如果找不到,就会新建一个),然后将低16位放入container中。换句话说,一个Roaringbitmap就是许多container的集合,具体的细节可以参阅 https://roaringbitmap.org/

数据写入

我们的原始数据主要分为:

  • 用户操作行为数据table_oper_raw
    这包括操作时间(oper_datetime)、用户标识ID(user_id)以及用户操作行为名称(oper_name)。例如,”2024-02-24 14:31:09|o4fdG4-Tm6-SO4fz9p3fiIQoK6a0|点击首页banner” 表示在2024-02-24 14:31:09,用户o4fdG4-Tm6-SO4fz9p3fiIQoK6a0点击了首页的banner
user_id oper_name oper_datetime
o4fdG4-Tm6-SO4fz9p3fiIQoK6a0 click_banner 2024-02-24 14:31:09
o4fdG4-Tm6-SO4fz9p3fiIQoK6a0 click_banner 2024-02-24 14:32:20
o4fdG4-Tm6-SO4fz9p3fiIQoK6a0 close_game 2024-02-24 14:44:12
  • 用户属性数据table_attribute_raw
    这表示用户在产品或用户画像中的属性,包含时间(datetime)、用户标识(user_id)以及各类用户属性字段(可能包括用户的新进渠道、所在地区等),例如“2024-02-24 14:31:09|o4fdG4-Tm6-SO4fz9p3fiIQoK6a0|小米商店|广东省”
user_id country city device_type age
o4fdG4-Tm6-SO4fz9p3fiIQoK6a0 China Beijing IOS 10
o4fdG4-cZVBIUGtbOYvOPDKTxXLU China Shanghai Android 5
o4fdG479Ln6OURYtGdZIYfQHMBHg United States New York Android 12

然后,将大宽表的数据进行”转置”,然后批量导入到 ClickHouse,如下表所示。表中的每一行代表一个标签实例(也就是标签和标签值的组合),例如”city = Beijing”。此外,这一行还需要保存用户 id,这个 id 需要进行编码,将每个用户映射成唯一的id(32位的无符号整型),因为 Roaringbitmap 只能接受整型

tag tag_item users
country China 1
country United States 10
city Beijing 2
city Shanghai 1002

建表语句为:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE table_user_tag
(
`tag` String comment '标签类别',
`tag_item` String comment '标签项',
`p_day` Date comment '日期',
`origin_user` String comment '用户 ID'
)
ENGINE = MergeTree
PARTITION BY toYYYYMMDD(p_day)
ORDER BY (tag, tag_item)
SETTINGS index_granularity = 8192
COMMENT '用户标签表';
Roaringbitmap压缩

对于用户的属性/操作数据,先建一个可以存放 bitmap 的表 table_user_tag_bitmap,根据使用场景,我设计 ClickHouse 表结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
CREATE TABLE table_user_tag_bitmap
(
`tag` String comment '标签类别',
`tag_item` String comment '标签项',
`p_day` Date comment '日期',
`users` AggregateFunction(groupBitmap, UInt64) comment '内部用户bitmap'
)
ENGINE = AggregatingMergeTree
PARTITION BY toYYYYMMDD(p_day)
ORDER BY (p_day, tag, tag_item)
SETTINGS index_granularity = 8192
COMMENT '用户标签bitmap表';

当然这里面还可以增加其他字段以区分更细的粒度,比如 app_id 等用来区分不同应用,这里简化

然后,看表包含的字段

  • tag 代表标签, tag_item 代表标签值。因为在标签的圈选查询中,经常有 tag = "city" AND tag_item = "beijing" 的语句,我们将 (tag, tag_item) 作为主键,以提高查询效率。
  • p_day 代表数据写入的日期,也作为 ClickHouse 的分片键。因为每天的标签数据都是全量导入,p_day 不仅可以用来区分标签版本,也方便我们批量删除历史数据。
  • users 用来存放根据 (tag, tag_item) 聚合得到用户人群

接着用聚合函数 groupBitmapState 对用户id进行压缩写入到 table_user_tag_bitmap

1
2
3
4
5
INSERT INTO table_user_tag_bitmap
SELECT tag, tag_item, toDate('2024-02-28'), groupBitmapState(origin_user) as users
FROM table_user_tag
WHERE p_day = toDate('2024-02-28')
GROUP BY tag, tag_item;

这样原本很庞大的数据就被压缩成了几十行数据,每行包括标签和对应的用户id形成的bitmap:

image-20240228190040062

数据查询

首先,简要地介绍下方案中常用的bitmap函数:

  • bitmapCardinality
    返回一个UInt64类型的数值,表示bitmap对象的基数。用来计算不同条件下的用户数,可以粗略理解为count(distinct)

  • bitmapAnd
    为两个bitmap对象进行与操作,返回一个新的bitmap对象。可以理解为用来满足两个条件之间的and,但是参数只能是两个bitmap

  • bitmapOr
    为两个bitmap对象进行或操作,返回一个新的bitmap对象。可以理解为用来满足两个条件之间的or,但是参数也同样只能是两个bitmap。

如果是多个的情况,可以尝试使用groupBitmapMergeState,组合不同标签,圈选出用户人群。例如,我们想找出城市为北京、性别为女的用户。

我们只需首先找到城市为北京的用户人群(用 bitmap 表示),然后找到性别为女的用户人群,然后对它们进行 AND 操作即可。具体查询如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
WITH
(
SELECT groupBitmapAndState(users)
FROM table_user_tag_bitmap
WHERE
AND tag = 'city'
AND tag_item = 'beijing'
AND p_day = toDate('2024-02-28')
) AS user_group_1,
(
SELECT groupBitmapAndState(users)
FROM table_user_tag_bitmap
WHERE
AND tag = 'gender'
AND tag_item = 'female'
AND p_day = toDate('2024-02-28')
) AS user_group_2
SELECT bitmapToArray(bitmapOr(user_group_1, user_group_2));

结果:

image-20240228192441076

其中,groupBitmapMergeState 函数对通过 WHERE 筛除得到的任意个数的 bitmap (users) 进行 AND 操作,而 bitmapAnd/bitmapOr 只能对两个 bitmap 进行操作。

查看用户画像

假如需要选取”北京的女性用户”这个人群,我们想要了解这个人群中使用设备的 benchmarkLevel。这种标签分布信息就是我们所说的用户画像。

这个查询的实现方式也是直观的。

  1. 使用和上一节相同的步骤,得到”北京的女性用户”这个bitmap。
  2. 对这个人群进行分组,分别得到各个benchmarkLevel的bitmap。我们在这一步会得到更多的bitmap。
  3. 将步骤2中的每一个bitmap与步骤1中的bitmap进行AND操作,就可以得到”北京的女性用户”在”设备benchmarkLevel”上的分布情况。

具体的实现方式如下查询所示。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
WITH
(
SELECT groupBitmapMergeState(users)
FROM table_user_tag_bitmap
WHERE p_day = toDate('2024-02-28') AND tag = 'city' AND tag_item = 'beijing'
) AS user_group_1,
(
SELECT groupBitmapMergeState(users)
FROM table_user_tag_bitmap
WHERE p_day = toDate('2024-02-28') AND tag = 'gender' AND tag_item = 'female'
) AS user_group_2,
(
-- 北京的女性用户
SELECT bitmapAnd(user_group_1, user_group_2)
) AS filter_users
SELECT
bitmapCardinality(bitmapAnd(filter_users, group_by_users)) AS count,
tag,
tag_item
FROM
(SELECT
groupBitmapMergeState(users) AS group_by_users,
tag,
tag_item
FROM table_user_tag_bitmap
WHERE tag = "device_type"
GROUP BY (tag, tag_item));

image-20240229110703638

总结和反思

总的来看,这个方案的优势包括:

  1. 存储体积小,极大地节省了存储空间;
  2. 查询速度快,通过使用bitmapCardinality、bitmapAnd、bitmapOr等位图函数,可以快速计算用户数量和满足某些条件的查询,将耗时的join操作转变为位图之间的计算;
  3. 适合进行灵活天数的标签查询;
  4. 更新方便,用户行为数据和用户属性数据分开存储,便于后续属性的添加和数据的回滚。

用户画像平台设计
https://sugayoiya.github.io/posts/53612.html
作者
Sugayoiya
发布于
2024年1月1日
许可协议