• 好好住画像全攻略


    1.1.1.前言

    好好住致力于成为可信赖的家装家具消费平台。在这里汇聚了来自世界各地热爱生活的“住友”、专业的家装设计师以及品牌家具好物。在好好住“住友”可以通过分享家的故事,碰撞出灵感的火花;设计师可以快速寻找到潜在的客户,帮助更多的人实现家的梦想;品牌商家可以尽情展示自己,让优质好物走进千家万户。而怎样让“住友”,“设计师”和“品牌”更好的串联起来,一直是好好住努力的方向。

    随着移动互联网蓬勃发展,我们进入了一个数据大爆炸的时代。怎样让用户快速、准确获取所需,是每一个互联网公司都必须面对的巨大挑战。那在好好住我们都做了什么呢?那就不得不提好好住的用户画像平台。好好住的画像平台为算法推荐提供了精准的模型训练数据,为内容分发提供了数据支持,为运营同学提供了高效、精细化运营工具,用户画像已经融入到了好好住的方方面面。那我们是怎么搭建好好住画像平台?这里将从两方面介绍好好住用户画像:画像平台和基于StarRocks用户画像应用实践。

    1.1.2.画像平台

    用户画像平台这部分我们介绍两个最重要的模块:标签管理和标签应用(用户分群)。前者主要功能是维护画像标签的元数据,后者主要是标签的应用。

    1.1.2.1.标签管理

    1.1.2.1.1.标签元数据

    为了让标签更好的管理,我们将标签分成2级目录、5个大类。
    在这里插入图片描述

    标签的维护由画像产品经理负责。标签录入系统时,需要依次录入标签中文、英文名,以及选择标签的一级、二级分类、数据类型。这里需要注意的是,选择字符串类型的同时需要指定标签的枚举值。
    在这里插入图片描述
    在这里插入图片描述
    如果选择日期类型,需要指定日期格式(时间戳,yyyyMMdd等),如果选择的是整形或者浮点数类型,需要指定标签取值的起始位置、划分间隔以及区间个数。例如:int类型标签选择区间间隔‘10’,区间个数为5个,起始位置为‘0’,那么这个int标签会划分成[0,10),[10,20),[20,30),[30,40),[40,+∞)5个区间。整形和浮点型的这种处理方式,主要是为了提升画像圈选以及用户群体分析响应速度。最后是设置标签的实时性:T+1或者实时。标签录入完成后,数据开发会根据标签定义进行标签开发。

    1.1.2.1.2.标签价值分析

    随着用户画像的广泛运用,画像标签的规模逐步达到400+。随之也带来了一个问题:部分标签在上线之后的几周之内,会有一段密集使用期。但随着业务的调整,这个标签使用率大幅降低,直至被遗弃。也就是说,有些标签并不是长久有效,标签是有“有效期”的。理想情况是,一个标签失去使用价值了,我们应该对其进行下线,释放与之对应的存储计算资源。现实情况是,我们很难评估标签的价值,也就很难推动标签下线,每天都需要耗费大量的计算、存储资源构建画像。基于此背景,我们规划了标签价值分析模块,让标签的价值有更好的数据依据可循。
    如何去衡量标签的价值?最直观的方式就是:观测标签在指定时间内是否有被使用。目前标签的使用有两种方式:点查,用户圈选(分析)。
    对于点查这种方式,我们封装了一层点查http服务,业务方在接入画像点查服务时,需要在画像平台申请一个请求token。画像平台根据token管理标签的访问权限,同时每次画像请求,点查服务都会记录token的使用情况。
    在这里插入图片描述
    标签”用户生产等级”过去7天使用情况
    在这里插入图片描述
    标签”用户生产等级”昨日各token使用情况
    在这里插入图片描述
    同样用户在使用画像圈选工具时,也会记录用户标签使用情况。这样我们就知道标签被哪些用户调用,被哪些圈选场景引用,哪些人群包用到了此标签。同样如果一个标签数据异常了,我们也能够快速评估影响面。
    除此之外,我们还对标签的取值分布,覆盖度进行统计,通过这个模块,我们可以快速了解标签本身的数据质量。通过标签质量,我们可以采取相对应的措施(推动下线或者调整)。例如:一个标签的取值过度集中(95%的用户取值都一样),或者标签只覆盖了1%的用户。像这种情况,画像产品经理可以启动标签的下线流程,或者调整标签的计算逻辑。
    在这里插入图片描述

    1.1.2.1.3.用户分群

    标签落地到画像平台后,接着就是如何将标签应用到具体的业务中。在好好住,用户画像标签主要有两种应用场景:点查和用户圈选(分析),这里我们主要介绍下后者。关于“用户圈选(分析)”,具体包含两个场景:“用户圈选”和“人群包对比分析”。前者主要是使用画像平台筛选工具,圈选出一批符合条件的用户,然后对这批用户进行触达操作(push,弹框等)。后者就是对不同的人群进行对比分析,找到不同人群画像特征上的明显差异,然后根据特征差异,进行相应的业务动作。

    1.1.2.1.3.1.用户圈选

    业务产品或者运营通过此功能筛选用户,并生成人群包。人群包生成后,会提供一个http接口,用户可以通过这个接口获得人群报数据,实现个性化业务需求。 目前人群包生成方式有三种:画像平台工具筛选,sql圈选,用户手动上传。其中使用“画像平台工具筛选”,“sql圈选”的人群包,每天在固定的时间进行更新。
    在这里插入图片描述

    1.1.2.1.3.2.人群包对比分析

    通过此功能可以对不同业务动作的用户群体,进行用户画像的对比分析,然后根据不同用户特征调整运营策略。这里用一个例子来介绍下具体的应用场景。例如:运营同学做了一个某品牌抽屉柜的促销活动,给所有具备抽屉柜购买意愿的人推送促销优惠券,优惠的力度特别诱人。活动进行了一半,发现转换率并没有达到预期。为了挖掘深层次原因,运营同学通过画像平台把这次活动转化成功的用户和转换不成功的用户圈选出两个人群包。通过人群包的画像对比分析,发现转换不成功的用户大部分都是家里有孩子的,用户可能考虑此款抽屉柜对小孩存在安全隐患。为了挽回这批用户,给这批转换不成功的用户推送同品牌的儿童抽屉柜优惠券,最终达到此次活动预期。
    在这里插入图片描述

    1.1.3.基于StarRocks用户画像应用实践

    好好住在采用StarRocks搭建用户画像平台之前,历经ES,ClickHouse两个版本迭代。接下来我们来介绍一下,前两个版本我们遇见的问题与挑战,以及我们如何用StarRocks解决这些问题。

    ES:作为好好住画像最初的解决方案,当时的目标只是为了支持”用户精准触达“,但随着画像应用普及,问题也逐步暴露出来,归纳为以下两点:

    • 无法与画像之外的数据进行关联
      在用户圈选场景中,我们经常碰到一些需求,需要将”用户画像数据”与用户的一些”业务动作“进行关联。例如:给过去七天点击过设计师咨询按钮,并且家住北京,正在装修,预算10-30万的用户推送宜家满减优惠券领取的消息。这个场景中,”过去七天点击过设计师咨询按钮“这个业务动作的数据保存在数仓里,基于现有的ES的架构,我们只能把”过去点击设计师咨询按钮“当成一个画像标签处理。这种处理方式虽然能解决问题,但在一定程度上把本不属于”用户画像“范畴的数据存入ES,导致用户画像的标签个数在很短的时间内膨胀到了400+,对用户画像数据造成了污染。
    • 无法支持后续画像分析场景
      为了挖掘用户价值,业务方希望画像平台除了能够圈选用户外,还能够支持对画像数据进行深入分析。但ES作为一个全文检索引擎,统计分析并不擅长。

    ClickHouse:为了解决ES所面临的问题,我们引入了Clickhouse,并基于ClickHouse对画像底层数据进行了重构。同时我们将用户的行为数据也导入到ClickHouse,使用Clickhouse的聚合模型,将”用户圈选“、”人群分析“转换成了Bitmap的”交“,”并“、”补“操作,大大提升了画像平台响应速度。但同样在使用过程中,我们也面临了不少问题。

    • join性能不尽如人意。
    • 画像平台引入的用户行为数据后,事实表和维度表的关联操作不可避免,要获得最佳性能clickhouse只能把事实表与维度表打平成宽表。宽表模型是以牺牲灵活性为代价,将 join 的操作前置,来加速业务的查询,很难适应灵活多变、新增的业务场景。
    • 并发性能差。和大多数OLAP查询引擎一样,clickhouse不支持高并发业务场景,大量的查询任务很容易把CPU打满。

    StarRocks:StarRocks是我们一直关注的OLAP查询引擎,而吸引我们将用户画像从ClickHouse迁移到StarRocks,总结大致有如下几点:

    • 极致性能:不亚于ClickHouse甚至更好的查询性能,同时在多张亿级别数据表join的复杂查询场景,也能秒级响应。
    • 兼容Mysql协议:兼容MySQL协议,使用mysql客户端就能无缝对接StarRocks,大大降低了用户使用门槛。
    • 运维简单:在线扩容且不影响线上业务,后台数据能够自动进行rebalance。
    • 支持高并发:支持高并发查询场景。
    • 支持多种数据模型:目前支持”明细模型”,”聚合模型”,”更新模型”以及”主键模型”,其中主键模型在2.2版本之后还支持部分更新。

    1.1.3.1.基于StarRocks用户画像应用实践细节

    在用户画像实现技术细节这部分,我们会重点介绍如下内容:明细数据存储方案的选择、不同数据类型的处理方式以及筛选方式、实时离线标签的处理流程。

    1.1.3.1.1.明细数据存储方案

    用户画像的明细数据存储,通常有两种方案:宽表和高表。那基于StarRocks我们是怎么选择的?

    1.1.3.1.1.1.宽表方案

    宽表方案比较适合标签比较固定的场景,好处就是使用起来比较简单,不存在多表关联,问题在于后续新增标签的成本较高。在好好住的画像里实时标签采用的是宽表方案。在”实时、离线标签处理方式“小节,我们会详细介绍实时标签的处理流程。

    1.1.3.1.1.2.高表方案

    相对宽表方案,高表模型,就比较适合灵活多变的业务场景,新增标签的成本低。使用这种方案,我们需要将不同数据类型标签导入对应的高表中。目前高表有3类:字符串高表,整形高表(int/bigint),浮点数高表。数据进入高表时,按照数据类型处理成不同的标签值(tag_value)。日期类型统一处理成yyyyMMdd格式,后续在聚合表中日期可以根据需求转换成“近一天"、"近一周"的字符串类型。整形、浮点型数据按照标签定义时设置的区间块进行切分,需要注意的是整形和浮点数的tag_value都是int类型。例如:age标签采用单位为‘10’的间隔,‘左闭右开’的方式划分区间,然后向上取整获得tag_value(33->4,25->3),浮点类型标签total_money采用‘5000’为间隔划分区间(17311.1->4,9812.54->2)。

    整型、浮点类型高表除了存储tag_value外,还会额外新增一个字段real_value,这个字段用于保存原始值,这个字段的作用后续会解释。

    高表生成完后,创建两张聚合模型表user_profile_string_bits和user_profile_int_bits(这两张表使用bitmap类型字段‘uids’保存用户id)。前者数据来自字符串高表(ods_origin_string),后者数据来自整形高表(ods_origin_int)和浮点数高表(ods_origin_double)。后续绝大多数用户圈选都是基于这两张聚合表,但也有特殊情况会用到明细高表。
    在这里插入图片描述
    明细高表和聚合表示例

    --double类型数据高表
    CREATE TABLE `ods_origin_double` ( 
      `dt` date NULL COMMENT "日期",
      `tag_name` varchar(50) NULL COMMENT "标签id",
      `tag_value` varchar(50) NULL COMMENT "标签枚举值",
      `uid` int(11) NULL COMMENT "uid",
      `real_value` double NULL COMMENT "标签真实值"
    ) ENGINE=OLAP
    DUPLICATE KEY(`dt`, `tag_name`, `tag_value`)
    COMMENT "OLAP"
    PARTITION BY RANGE(`dt`)
    (PARTITION p20220803 VALUES [('2022-08-03'), ('2022-08-04')),
    PARTITION p20220804 VALUES [('2022-08-04'), ('2022-08-05')),
    PARTITION p20220805 VALUES [('2022-08-05'), ('2022-08-06')))
    DISTRIBUTED BY HASH(`dt`, `tag_name`, `tag_value`) BUCKETS 300
    PROPERTIES (
    "dynamic_partition.enable" = "true",
    "dynamic_partition.time_unit" = "DAY",
    "dynamic_partition.time_zone" = "Asia/Shanghai",
    "dynamic_partition.start" = "-1",
    "dynamic_partition.end" = "1",
    "dynamic_partition.prefix" = "p"
    );
    
    --聚合表
    CREATE TABLE `user_profile_int_bits` (
      `dt` date NULL COMMENT "日期",
      `tag_name` varchar(50) NULL COMMENT "",
      `tag_value` varchar(50) NULL COMMENT "",
      `uids` bitmap BITMAP_UNION NULL COMMENT "uid的bitmap"
    ) ENGINE=OLAP
    AGGREGATE KEY(`dt`,`tag_name`, `tag_value`)
    PARTITION BY RANGE(`dt`)
    (PARTITION p20220803 VALUES [('2022-08-03'), ('2022-08-04')),
    PARTITION p20220804 VALUES [('2022-08-04'), ('2022-08-05')),
    PARTITION p20220805 VALUES [('2022-08-05'), ('2022-08-06')))
    DISTRIBUTED BY HASH(`dt`, `tag_name`, `tag_value`) BUCKETS 10;
    
    --将明细数据插入到聚合表
    insert into user_profile_int_bits 
    select '2022-08-04',tag_name,tag_value,to_bitmap(uid) from ods_origin_double
    
    
    • 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
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    1.1.3.1.2.不同数据类型标签处理方式以及筛选条件
    1.1.3.1.2.1.int/long/float/double

    int,double类型数据在导入画像时会按照一定区间切块。以年龄举例,可以按5岁一个区间切块,例如:0-5岁落在区间块’1’上,15-20落在’4’区间块上。这样做的好处就是,能有效的控制tag_value的取值,方便后续更好的构建用户画像聚合模型(bitmap),加速查询;缺点也比较明显,筛选的颗粒度比较粗,只能按照事先定义的区间进行筛选,没办法任意指定区间。

    当然不可避免有些场景就是没办法预先划分区间,这种情况只能查询明细数据。但有个比较棘手的问题,像float/double这种数据类型是没办法设置成排序键的,意味着这要涉及到此类标签筛选,会存在数据大规模扫描。那有什么解决办法呢?对于这种场景,我们先将用户大查询条件转换成对应的区间块查询,然后在对real_value进行过滤查询,减少数据的扫描量。在生产实践中,这种方式虽然在使用过程中会增加一定复杂度,但是查询数据时能够直接命中数据所在的tablet,会有不错的剪枝效果。

    将double类型数据插入高表(累计成交额:total_money),区间处理时采用左闭右开方式,例如:5000将会落在区间值为‘1’的区块。

    insert into ods_origin_double 
    SELECT '2022-08-01','total_money',total_money/5000 +1 ,uid,total_money from orders
    
    • 1
    • 2

    使用场景举例:筛选北京累计消费金额在[5000,30000)的用户。

    WITH a AS (
    SELECT user_bits FROM  user_profile_string_bits WHERE tag_name='province' AND tag_value='北京市' 
    UNION ALL
    SELECT bitmap_union(to_bitmap(uid)) user_bits FROM ods_origin_double
    WHERE tag_name='total_money' AND tag_value in (1,2,3,4,5) AND real_value>=5000 AND real_value<30000
    )
    SELECT bitmap_count(bitmap_intersect(user_bits))FROM a
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    1.1.3.1.2.2.String/date

    字符串类型标签处理比较简单,标签值采用标签定义时设置的枚举值。而时间类型标签则根据实际业务需求转换成‘yyyy-MM-dd/yyyyMMdd’类型的字符串,或者转换成区间的方式:“近1日”,“近7日”,“近14日”,“近15日”,“近1月”,“近2月”,“近3月”,“近半年”,“近一年”,“一年以上”。前者处理时比较简单,使用时也比较灵活,但是在区间查询时会有更多的资源消耗,特别是大区间查询。

    这里以”上次登录时间“为例,介绍下在生产环境中我们是如何处理日期类型标签。首先我们使用聚合模型存储所有日期类型标签的每日用户聚合数据,user_profile_date_bits表结构如下:

    CREATE TABLE `user_profile_date_bits` (
      `tag_name` varchar(50) NULL COMMENT "",
      `tag_value` varchar(50) NULL COMMENT "yyyy-MM-dd",
      `uids` bitmap BITMAP_UNION NULL COMMENT "uid的bitmap"
    ) ENGINE=OLAP
    AGGREGATE KEY(`tag_name`, `tag_value`)
    COMMENT "OLAP"
    DISTRIBUTED BY HASH(`tag_name`) BUCKETS 20
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    每天将用户登录数据插入到user_profile_string_bits表中

    insert into user_profile_date_bits
    SELECT 'login_time','2022-07-01',to_bitmap(uid) from ods_origin_string 
    where tag_name='login_time' and dt='2022-07-01';
    
    • 1
    • 2
    • 3

    基于每日用户聚合后的数据,计算‘近7天’的用户登录数据

    insert into user_profile_string_bits
    select 'login_time','近7天',bitmap_union(uids) from user_profile_date_bits 
    where tag_name='login_time' and tag_value >'#{date-7,yyyy-MM-dd}' and 
    tag_value<='#{date-1,yyyy-MM-dd}';
    
    • 1
    • 2
    • 3
    • 4
    1.1.3.1.2.3.筛选条件

    在这里插入图片描述
    这里重点说明下筛选条件“不等于”,“不包含”的处理方式。这类操作,我们通常会先转换成“等于”、“包含”查询,然后再进行“否操作”,这样能极大的减少数据计算量。如何实现呢?我们会在starrock中建一张表user_profile_alluser,这张表里是一张聚合表,且这种表里只有一行数据,这行数据的uids字段会存储所有的用户id,每天把所有的用户uid聚合到‘uids’字段中。取“否”操作就转换成与uids这个字段进行差集操作。

    CREATE TABLE `user_profile_alluser` (
      `tag_name` varchar(50) NULL COMMENT "",
      `uids` bitmap BITMAP_UNION NULL COMMENT "all user"
    ) ENGINE=OLAP DISTRIBUTED BY HASH(`tag_name`) BUCKETS 1;
    
    --每天把所有的用户uid存到user_profile_alluser表中
    insert into user_profile_alluser
    select 'alluser',to_bitmap(uid) from ods_origin_users;
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8

    例如:圈选“近7天没有登录的用户”

    with a AS ( --先查询“最近7天登录用户”
    SELECT  uids 
    FROM user_profile_string_bits
    WHERE tag_name ='login_time'
            AND tag_value='近7天'
            AND dt='${date-1,yyyy-MM-dd}'), 
    b AS (
    SELECT uids
        FROM user_profile_alluser)
    SELECT bitmap_count(bitmap_andnot(a.uids,b.uids))
    FROM a,b
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    1.1.3.1.3.实时、离线标签处理方式

    在这里插入图片描述
    离线标签的数据,每天通过hive外表方式导入到Starrocks高表中,然后生成聚合模型画像表。

    实时标签的数据主要来源有两部分:日志和binlog,我们使用日志采集工具将数据发送到kafka中,然后将数据统一处理成如下格式。

    {
        "data":json字符串,
        "columns":"column1,column2"
    }
    示例:
    {"data":{"uid":13653435,"last_login_day":20220720},"columns":"uid,last_login_day"}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6

    再由统一的消费程序upsert到StarRocks的宽表user_profile_real_time中。消费程序是我们参照StreamLoad方式开发的工具包,通过攒批方式,将数据以分钟级别频度推送到StarRocks。user_profile_real_time采用主键模型。实时标签数据采用宽表方案存储的主要考虑点如下:

    • 公司实时标签需求量较离线标签少,且新增标签频率不会特别高。
    • StarRocks主键模型在2.2版本之后支持部分字段更新功能(关键点)。
    • 宽表新增字段成本较低,且不会影响数据表正常使用。

    对于新增实时标签,具体步骤如下:

    • 第一步:对实时标签宽表进行新增字段。
    • 第二步:将实时标签数据数据流按统一的数据格式发送到kafka。
    • 第三步:从数仓获取新增标签历史数据,并通过StreamLoad方式更新到实时标签宽表中。(初始化历史数据)

    最后我们以一个例子介绍下如何使用离线、实时标签联合圈选。
    例:“圈选北京市用户生产等级(实时标签)是’Lv0‘,并且年龄在27-32之间,用户质量评分在[20-40]之间的用户”

    WITH a AS (
        SELECT user_bits
        FROM user_profile_string_bits
        WHERE tag_name='province'
                AND tag_value='北京市'
                AND dt='${date-1,yyyy-MM-dd}'
        UNION ALL 
        SELECT bitmap_union(to_bitmap(uid)) user_bits
        FROM user_profile_real_time  --实时宽表
        WHERE tag_name='用户生产等级' 
                AND tag_value='Lv0'
        UNION ALL 
        SELECT bitmap_union(to_bitmap(uid)) user_bits
        FROM ods_origin_int  --int类型高表
        WHERE tag_name='age'
                AND tag_value>=5
                AND tag_value<=7
                AND real_value>=27
                AND real_value<=32
        UNION ALL 
        SELECT bitmap_union(to_bitmap(uid)) user_bits
        FROM ods_origin_int --int类型高表
        WHERE tag_name='用户质量'
                AND tag_value>=3
                AND tag_value<=4
                AND real_value>=20
                AND real_value<=40 )
    SELECT bitmap_count(bitmap_intersect(user_bits)) FROM a
    
    • 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
    • 28

    1.1.4.后续规划

    目前画像平台已经能够满足日常的业务需求。但在数据分析上能做的还有很多,我们计划将埋点系统的事件分析和画像平台进行整合,满足更多的事件分析场景。

  • 相关阅读:
    1105 链表合并 – PAT乙级真题
    基于深度学习的菠萝与果叶视觉识别及切断机构设计
    C++单例的安全实现,double-check(双重检查锁定)的安全实现方法
    C语言之函数
    Python进阶学习分享之循环设计
    Unity自定义Timeline总结
    ipad协议超稳定
    【C++】类和对象(中篇)(万字)
    【Vue实战】基于Vue的九宫格在线抽奖附源代码
    原来,C语言操作Mysql这么简单
  • 原文地址:https://blog.csdn.net/woloqun/article/details/126226657