1天学会开发工业级推荐系统的特征工程代码:保姆级教程

一、推荐算法为何要精做特征工程

机器学习工作流就好比是一个厨师做菜的过程,简单来说,清洗食材对应了清洗数据,食材的去皮、切片和搭配就对于了特征工程的过程,食物的烹饪对应了模型训练的过程。如果你觉得数据清洗和特征工程不重要,莫非是你想吃一份没有经过清洗、去皮、切片、调料,而直接把原始的带着泥沙的蔬菜瓜果放在大锅里乱炖出来的“菜”? 先不说卫生的问题,能不能弄熟了都是个问题。

在完整的机器学习流水线中,特征工程通常占据了数据科学家很大一部分的精力,是因为特征工程能够显著提升模型性能,高质量的特征能够大大简化模型复杂度,让模型变得高效且易理解、易维护。在机器学习领域,“Garbage In, Garbage Out”是业界的共识,对于一个机器学习问题,数据和特征决定了机器学习的上限,而模型和算法只是逼近这个上限而已。

与计算机视觉、语音、NLP等领域不同,推荐算法模型很大程度上依赖人工特征工程为其提供输入数据,这是因为推荐算法需要处理的通常是结构化的关系型数据,这些原始数据一般需要经过清洗、采样、变换、统计、编码等方式生成最终的特征数据,模型本身无法直接完成所有这些加工逻辑。关于工业级推荐系统中如何做特征工程,请参考这篇文章《工业级推荐系统中的特征工程》。

上面这篇文章从一个较概括和抽象的角度详细介绍了工业级推荐系统中如何做特征工程,但并未提及代码实现方面的内容。本文的目标就是帮助读者理解并掌握这些特征工程方法在真实的生成环境中是如何实现的。本文从一个公开数据集出发,详细讲解一个工业级推荐系统的特征工程代码是如何一步一步开发出来的。开发语言为MaxCompute SQL,非常类似于Hive/Spark SQL。

查看本文完整代码:Github

二、数据集简介

本文使用的训练、评估、测试数据集来自REES46 Marketing Platform多类目电商平台的行为数据。该数据集包含了电商平台从2019年10月到2020年4月的28.5亿条用户行为记录。

数据集中的每一行表示一个用户对一个商品行为事件。数据集的Schema如下:

Property Description
event_time Time when event happened at (in UTC).
event_type one kind of event: view, cart, purchase.
product_id ID of a product
category_id Product's category ID
category_code Product's category taxonomy (code name) if it was possible to make it. Usually present for meaningful categories and skipped for different kinds of accessories.
brand Downcased string of brand name. Can be missed.
price Float price of a product. Present.
user_id Permanent user ID.
user_session Temporary user's session ID. Same for each user's session. Is changed every time user come back to online store from a long pause.

Event types
事件类型包括:

  • view - 用户浏览了某个商品
  • cart - 用户把某个商品加入了购物车
  • purchase - 用户购买了某个商品
image.png

User Session
用户会话:

  • 一次会话中可能包含了多个用户行为,甚至多个购买行为。

数据探查

示例数据:

event_time event_type product_id category_id category_code brand price user_id user_session
2019-11-01 00:00:00 UTC view 1003461 2053013555631882655 electronics.smartphone xiaomi 489.07 520088904 4d3b30da-a5e4-49df-b1a8-ba5943f1dd33
2019-11-01 00:00:00 UTC view 5000088 2053013566100866035 appliances.sewing_machine janome 293.65 530496790 8e5f4f83-366c-4f70-860e-ca7417414283
2019-11-01 00:00:01 UTC view 1004775 2053013555631882655 electronics.smartphone xiaomi 183.27 558856683 313628f1-68b8-460d-84f6-cec7a8796ef2
2019-11-01 00:01:04 UTC purchase 1005161 2053013555631882655 electronics.smartphone xiaomi 211.92 513351129 e6b7ce9b-1938-4e20-976c-8b4163aea11d
2019-11-01 00:06:33 UTC purchase 1801881 2053013554415534427 electronics.video.tv samsung 488.80 557746614 4d76d6d3-fff5-4880-8327-e9e57b618e0e
2019-11-01 00:07:38 UTC purchase 30000218 2127425436764865054 construction.tools.welding magnetta 254.78 515240495 0253151d-5c84-4809-ba02-38ac405494e1

以2019年十一月的数据为例,展示一些统计数据。

  • 记录数:67501979

  • category_code

    • [null]: 32%
    • lectronics.smartphone: 24%
    • other: 43%
    • image.png
  • brand

    • [null]: 14%

    • samsung: 12%

    • other: 75%

    • Top Brands

      brand count
      samsung 198670
      apple 165681
      xiaomi 57909
      huawei 23466
      oppo 15080
      lg 11828
  • user_session

    • 唯一值个数:13776051
  • price

    • mean: 292
    • std. Deviation: 356
    • min: 0
    • max: 2.57k
    • image.png
  • event_time (Vistors Daily Trend)

    • Does traffic flunctuate by date?
    • image.png

三、任务目标

建立模型预测用户在加购某商品后,最终是否会购买。

根据上述学习目标,通过下列准则构建样本标签:

  • 正样本:用户购买了某商品
  • 负样本:用户把某商品加入购物车,但之后并没有购买(在同一session内)
  • 浏览行为不用来构建样本(但可以用来做特征)

数据集的划分方法如下:

  • 训练集:19年11月-20年2月
  • 验证集:20年3月
  • 测试集:20年4月

其中,19年10月的数据不用来做训练,是因为特征构建时最长的统计周期是30天,10月份的数据在一些统计值上会发生统计不完全的问题。

四、特征工程

限于篇幅的原因,我们在文章中只展示部分代码,完整的代码请查看Github

首先,我们创建一个和原始数据对应schema的MaxCompute Table ecommerce_behavior,并把从Kaggle下载的数据集通过 odpscmdtunnel 命令上传至 MaxCompute平台。举例如下:

CREATE TABLE IF NOT EXISTS ecommerce_behavior
(
    event_time    STRING,
    event_type    STRING,
    product_id    BIGINT,
    category_id   BIGINT,
    category_code STRING,
    brand         STRING,
    price         DOUBLE,
    user_id       BIGINT,
    user_session  STRING
) 
PARTITIONED BY
(
    dt            STRING
);

odpscmd -e "tunnel u -cf true -h true -acp true  2020-Apr.csv ecommerce_behavior/dt=202004;"

在预处理阶段,category_code(类目路径)被分裂为cat_0,cat_1,cat_2三个字段,分别表示一级类目、二级类目、三级类目;根据event_time生成两个新的特征:ts_weekday(value: 0-7),ts_hour(value: 0-23),分别表示星期几和几点钟。代码如下:

INSERT OVERWRITE TABLE ecommerce_behavior_table PARTITION(dt)
SELECT *, DATEPART(event_time,'hh') ts_hour, WEEKDAY(event_time) ts_weekday,
    TO_CHAR(event_time, 'yyyymmdd') as dt
FROM (
    SELECT DATETIME(replace(event_time, ' UTC', '')) as event_time,
        event_type, product_id, category_id, category_code, brand, price,
        user_id, user_session, split(category_code, '\\.')[0] cat_0,
        split(category_code, '\\.')[1] cat_1,
        split(category_code, '\\.')[2] cat_2,
        split(category_code, '\\.')[3] cat_3
    FROM ecommerce_behavior
    WHERE dt > 0
);

1. 缺失值填充

通过数据分析发现,类目和品牌字段存在一些缺失值,由于类目和品牌都属于类别型特征,在其他处理之前我们用一个固定值“UNKNOWN”填充缺失值。

2. 生成价格档位 & 价格环比Diff Ratio

电商场景下,购买需求很大程度上受到价格因素的影响,因此价格是一个很重要的特征。然而,由于商品价格的跨度非常大,从前面数据探查的结果可以看出,该数据集上价格基本上遵循幂律分布。我们需要一个特征来度量该商品是“贵”还是“便宜”,以及“贵”或“便宜”的程度是多少,然而不同类目的商品价格区间是不一样的,如果不考虑商品的品类,那么价格的绝对值是无法度量商品是“贵”还是“便宜”的。通常我们会认为200块钱买一部手机是便宜的,但是100块钱买一瓶饮料却非常贵。

我们可以对商品价格做z-score标准化来生成输入给模型的特征。
norm\_price=\frac{price-mean_c(price)}{std_c(price)}
其中,mean_c(price)表示类目c的商品的平均价格,std_c(price)表示类目c的商品价格的标准差。

CREATE TABLE IF NOT EXISTS category_price_info
(
    category_id BIGINT COMMENT '商品类目ID',
    avg_price DOUBLE COMMENT '当前类目下的平均价格',
    std_price DOUBLE COMMENT '当前类目下价格的标准差'
)
PARTITIONED BY (dt STRING COMMENT '日期')
;

INSERT OVERWRITE TABLE category_price_info PARTITION(dt)
SELECT category_id, AVG(price) avg_price, STDDEV_SAMP(price) std_price, dt
FROM ecommerce_item_table
WHERE dt>0 AND category_id IS NOT NULL AND price IS NOT NULL
GROUP BY dt, category_id;

-- 请查看`ecommerce_behavior_wide_table`表的生成代码
select if(std_price=0.0, 0.0, (price-avg_price)/std_price) as norm_price, ...

转换后的norm\_price服从标准的正态分布,对于大多数模型,尤其是深度神经网络这样的模型,是非常友好的;这点可以从深度学习常用的batch normalization技术上得到验证。

好了,现在我们有一个可以度量商品“贵”或“便宜”的程度特征了。然而,现实中有很多用户对不同品类的商品的价格敏感度是不一样的,比如某用户可能喜欢买贵的电子产品,同时也偏好便宜的日用品。如果我们还需要度量某用户对不同品类的价格的偏好程度,那么仅仅有norm\_price还是不够的,因为norm\_price是一个连续值,没法直接和其他特征做交叉统计。进一步,我们可以对价格做档位划分,比如可以在每个类目下把商品按照价格从低到高排序,然后通过分位点划分为10个宽度相等的区间,把区间的编号作为商品的价格档位。SQL代码如下:

CREATE VIEW IF NOT EXISTS price_level_lookup_table AS
SELECT product_id, category_id, MIN(price_level) price_level
FROM (
    SELECT product_id, category_id, 
        NTILE(10) OVER(PARTITION BY category_id ORDER BY price)-1 AS price_level
    FROM (
        SELECT DISTINCT product_id, category_id, price
        FROM ecommerce_item_table
        WHERE dt > 0
    )
)
GROUP BY product_id, category_id;

上述步骤得到的price_level是一个值域为[0, 10)的离散值,可以和类目、用户组合成新特征,以及在新的组合特征的基础上生成新的统计特征。

在经济学中,价格是影响供给和需求的重要因素。价格的变化会显著影响需求量,因此把价格的变化建模到特征中会是一个不错的选择。我们构建了当前商品价格与前一天的最小价格之间的差异变化率作为特征。

3. Categorifying

对于类别型特征,如商品的类目、品牌等,推荐首先做Categorifying,即把类别型特征转换为该特征值的列表的索引位置(index),得到一个从0到|C|的非负整数,这里|C|表示该特征唯一值的个数。

Categorifying操作的好处是可以节省在线feature store的存储空间,因为原来的字符串值都被转换成了数字。同时,Categorifying的结果可以更方便地输入给深度神经网络模型的Embedding Layers,反之,如果不做Categorifying那么就需要的对原始类别型特征做hash之后,再去查Embedding Lookup Table,为了防止hash冲突,通常Embedding Lookup Table的长度都设置得比实际需要的更大,这样就会让模型多了很多不必要的参数,给模型的存储和分发都带来了额外的负担。

Categorifying的代码如下:

CREATE VIEW IF NOT EXISTS category_lookup_table AS
SELECT category_id, ROW_NUMBER() OVER() - 1 AS category_idx
FROM (
    SELECT DISTINCT category_id
    FROM ecommerce_item_table
    WHERE dt > 0
);

4. 分箱 & 组合

之前提到的价格档位就是一种分箱操作,除此之外,我们还可以自定义分箱的边界。比如,对于时间ts_hour字段我们可以根据统计数据分析自定义如下分箱边界,生成新的特征hour_bin

  1. 0-3 Night: 较低的购买概率
  2. 4-7 Early morning: 中等的购买概率
  3. 8-14 Morning/Lunch: 较高的购买概率
  4. 15-20 Afternoon: 较低的购买概率
  5. 21-23: Evening: 高购买概率

组合特征是一个常用的特征转换操作。有时候单个独立的类别型特征对于目标的预测帮助不大,但把两个或多个特征组合起来时就能够很好地帮助模型预测目标。比如,我们可以把weekday和hour_bin组合起来。

-- 完整代码请查看 `ecommerce_behavior_view` 试图的生成代码
SELECT
    CASE 
        WHEN ts_hour<=3 THEN 0
        WHEN ts_hour<=7 THEN 1
        WHEN ts_hour<=14 THEN 2
        WHEN ts_hour<=20 THEN 3
        ELSE 4 
    END AS hour_bin, ...
SELECT CONCAT(ts_weekday, '_', hour_bin) as weekday_hour

5. 样本去重和标签生成

我们把同一用户在同一session中对同一商品的多个相同类型的行为事件判定为重复样本,再接下来的生成统计特征之前,我们先执行样本去重,SQL的主要逻辑如下。

-- 完整代码请查看 `ecommerce_behavior_view` 试图的生成代码
SELECT `(rn)?+.+`
FROM (
    SELECT ...,
        ROW_NUMBER() OVER (PARTITION BY user_id, user_session, product_id, event_type ORDER BY event_time DESC) rn
    FROM ecommerce_behavior_table
    WHERE dt>=@bizdate
)
WHERE rn=1 -- drop_duplicates

下一小节要介绍的上下文特征需要依赖样本去重逻辑的执行才能正确计算出来。

根据学习任务的目标,定义用户在同一session中购买了某商品后,该session中当前用户对目标商品的所有行为事件都关联正样本的标签,否则关联负样本的标签。代码如下:

CREATE VIEW IF NOT EXISTS ecommerce_session_purchase(@bizdate STRING)
AS
SELECT NVL(user_session, 'UNKNOWN') user_session, user_id, product_id,
    MAX(IF(event_type='purchase', 1, 0)) is_purchased -- label
FROM ecommerce_behavior_table
WHERE dt>=@bizdate
GROUP BY NVL(user_session, 'UNKNOWN'), user_id, product_id
;

后续介绍的统计特征都需要依赖标签来做Target Encoding相关的操作。

6. 上下文特征

由于数据集提供的字段比较少,因此我们能够利用的上下文特征比较有限。我们构建的一个比较重要的上下文特征为 activity_count, 即当前session中到目前为止的行为次数。我们使用窗口函数SUM的累积求和功能来计算activity_count的值,代码如下。

INSERT OVERWRITE TABLE ecommerce_behavior_wide_table PARTITION(dt)
SELECT is_purchased, event_time, event_type, A.product_id, category_id, category_code, brand, price, A.user_id,
  A.user_session, cat_0, cat_1, cat_2, ts_hour, ts_weekday, hour_bin, weekday_hour, product_idx, category_idx,
  brand_idx, cat_0_idx, cat_1_idx, cat_2_idx, code_idx, price_level,
  SUM(IF(event_type IN ("purchase", "cart"), 1, 0)) OVER (PARTITION BY A.user_session ORDER BY event_time) as activity_count,
  if(std_price=0.0, 0.0, (price-avg_price)/std_price) as norm_price,
  A.dt
FROM ecommerce_behavior_view('0') A
JOIN ecommerce_session_purchase('0') B
ON A.dt=B.dt AND A.user_session = B.user_session AND A.product_id=B.product_id AND A.user_id=B.user_id
;

7. 特征值统计

在搜索、推荐、广告场景下,基于类别型特征生成新的统计特征是常规做法。个人认为,成功的特征工程要达成的目标就是降低使用复杂的模型的必要性。好的特征本身应该具有很好的信息量和区分度,不再需要模型本身的有很强的建模能力。那么,如何才能构建既有信息量又有区分度的特征呢?

在推荐场景下,对类别型特征或者执行分箱操作之后的数值型特征进行统计编码是常见套路,这一过程称之为bin-counting,详情请查看《工业级推荐系统中的特征工程》。

首先,我们需要一些基础的计数统计值,以CTR预估为例,目标是预测用户是否回点击某Item,那么我们需要如下统计计数值:

User Number of clicks Number of non-clicks
Alice 27 534
Bob 96 235
Joe 9 374

我们还需要对特征之间的交叉组合做正负样本的统计,如用户对不同商品类目的行为计数:

User,Category Number of clicks Number of non-clicks
Alice,1001 7 134
Bob,1002 17 235
Joe,1101 2 274

在实际的应该过程中,我们需要统计不同时间周期、不同行为类型以及不同组合特征下正、负样本的计数值,并基于这些统计值做特征编码。

首先,按业务日期统计商品、以及商品属性(看成是一种binning的结果)不同行为类型下的正、负样本数量:

CREATE TABLE IF NOT EXISTS ecommerce_item_count_daily
(
    field_idx   BIGINT,
    event_type  STRING,
    total_cnt   BIGINT,
    positive_cnt BIGINT,
    probs       DOUBLE
)
COMMENT 'item field counting'
PARTITIONED BY (field STRING COMMENT 'field name', dt STRING COMMENT 'yyyymmdd')
;

FROM (
    SELECT *
    FROM ecommerce_behavior_wide_table
    WHERE dt > 0 AND event_type IN ('purchase', 'cart', 'view')
)
INSERT OVERWRITE TABLE ecommerce_item_count_daily PARTITION(field='product', dt)
SELECT product_idx, event_type, COUNT(1) cnt, SUM(is_purchased) positive_cnt, AVG(is_purchased) probs, dt
GROUP BY product_idx, event_type, dt

INSERT OVERWRITE TABLE ecommerce_item_count_daily PARTITION(field='price_level', dt)
SELECT price_level, event_type, COUNT(1) cnt, SUM(is_purchased) positive_cnt, AVG(is_purchased) probs, dt
GROUP BY price_level, event_type, dt

INSERT OVERWRITE TABLE ecommerce_item_count_daily PARTITION(field='cate_price_level', dt)
SELECT category_idx*10+price_level, event_type, COUNT(1) cnt, SUM(is_purchased) positive_cnt, AVG(is_purchased) probs, dt
GROUP BY category_idx, price_level, event_type, dt

INSERT OVERWRITE TABLE ecommerce_item_count_daily PARTITION(field='category', dt)
SELECT category_idx, event_type, COUNT(1) cnt, SUM(is_purchased) positive_cnt, AVG(is_purchased) probs, dt
GROUP BY category_idx, event_type, dt

......

然后,按照最近1天、3天、7天、30天的时间滑动窗口,统计不同field的不同bin在不同行为类型下的正负样本数量,以及当前field在不同类型的行为下全局正负样本数量。

INSERT OVERWRITE TABLE ecommerce_item_cumulate_count PARTITION(field, dt)
SELECT field_idx, event_type, 
    total_cnt_1d, pos_cnt_1d, total_cnt_1d-pos_cnt_1d AS neg_cnt_1d, 
    SUM(pos_cnt_1d) OVER(PARTITION BY dt, field, event_type) global_pos_cnt_1d,
    SUM(total_cnt_1d-pos_cnt_1d) OVER(PARTITION BY dt, field, event_type) global_neg_cnt_1d,
    total_cnt_3d, pos_cnt_3d, total_cnt_3d-pos_cnt_3d AS neg_cnt_3d, 
    SUM(pos_cnt_3d) OVER(PARTITION BY dt, field, event_type) global_pos_cnt_3d,
    SUM(total_cnt_3d-pos_cnt_3d) OVER(PARTITION BY dt, field, event_type) global_neg_cnt_3d,
    total_cnt_7d, pos_cnt_7d, total_cnt_7d-pos_cnt_7d AS neg_cnt_7d, 
    SUM(pos_cnt_7d) OVER(PARTITION BY dt, field, event_type) global_pos_cnt_7d,
    SUM(total_cnt_7d-pos_cnt_7d) OVER(PARTITION BY dt, field, event_type) global_neg_cnt_7d,
    total_cnt_30d, pos_cnt_30d, total_cnt_30d-pos_cnt_30d AS neg_cnt_30d, 
    SUM(pos_cnt_30d) OVER(PARTITION BY dt, field, event_type) global_pos_cnt_30d,
    SUM(total_cnt_30d-pos_cnt_30d) OVER(PARTITION BY dt, field, event_type) global_neg_cnt_30d,
    field, dt
FROM (
    SELECT field_idx, event_type, total_cnt as total_cnt_1d, positive_cnt as pos_cnt_1d,
        SUM(total_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 2 PRECEDING) total_cnt_3d,
        SUM(positive_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 2 PRECEDING) pos_cnt_3d,
        SUM(total_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 6 PRECEDING) total_cnt_7d,
        SUM(positive_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 6 PRECEDING) pos_cnt_7d,
        SUM(total_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 29 PRECEDING) total_cnt_30d,
        SUM(positive_cnt) OVER(PARTITION BY field, field_idx, event_type ORDER BY dt ASC ROWS 29 PRECEDING) pos_cnt_30d,
        field, dt
    FROM (
        SELECT field, event_type, field_idx, TO_CHAR(DATEADD(TO_DATE(dt, 'yyyymmdd'), delta, 'dd'), 'yyyymmdd') dt,
            SUM(BIGINT(NVL(total_cnt, 0))) total_cnt, SUM(BIGINT(NVL(positive_cnt, 0))) positive_cnt
        FROM (
            SELECT TRANS_ARRAY(4, ',', dt, field, event_type, field_idx, string(total_cnt), string(positive_cnt), range(0, 30))
                AS (dt, field, event_type, field_idx, total_cnt, positive_cnt, delta)
            FROM ecommerce_item_count_daily
            WHERE dt>0 AND field NOT IN ('weekday_hour', 'product') AND event_type != 'purchase'
        )
        GROUP BY TO_CHAR(DATEADD(TO_DATE(dt, 'yyyymmdd'), delta, 'dd'), 'yyyymmdd'), field, event_type, field_idx
    )
)
WHERE dt<'20200501';

8. 统计特征变换

对应统计得到的数值型特征,通常有两种特征变换方法:缩放(scaling)和分箱(binning)。

推荐系统的统计特征推荐使用 gauss rank 的特征缩放方法或者基于分位点的分箱方法(等频分箱)。

image.png

gauss rank 特征缩放的过程如上图所示,首先对特征值进行排序,然后缩放到 (-1,1) 的区间,最后调用一个逆误差函数 erfinv 得到高斯分布的特征值。

9. 统计特征编码

下面介绍几种常用的特征编码方法。

a) Count Encoding

统计该类别型特征不同行为类型、不同时间周期内的发生的频次。直接用统计值作为特征,需要先做特征缩放(e.g. gauss rank)或分箱。

b) Target Encoding

统计该类别型特征不同行为类型、不同时间周期内的目标转化率(如目标是点击则为点击率,如目标是成交则为购买率)。
目标转化率需要考虑置信度的问题,当置信度不够的时候,需要做平滑,拿全局或者分组的平均转化率当当前特征的转化率做一个平滑,公式如下。

image.png

c) 优势比(Odds Ratio)

优势比告诉我们某种推测的概率比其反向推测的概率大多少,公式如下。

\theta=\frac{p_1/(1-p_1)}{p_2/(1-p_2)}

例:用户Alice的点击事件

Click Non-Click Total
Alice 5 120 125
Not Alice 995 18880 19875
Total 1000 19000 20000

在我们的例子中,优势比意味着“爱丽丝点击相对于不点击的可能性”和“其他人点击相对于不点击的可能性“相比有多大。在这种情况下,计算出的数字是:
\frac{(5/125)/(120/125)}{(995/19875)/(18880/19875)}=0.7906

优势比的值有可能会非常小或非常大(例如,可能会有几乎不会点击的用户),因此我们可以对其进行简化,并加一个log转换使得除法变成减法:log(N^+)-log(N^-)

d) 证据权重(Weight Of Evidence)

Weight of evidence (WOE) 和 Information value (IV) 是简单有效的变量变换和选择方法,在二分类问题中也是很好的特征。

WOE=ln\left( \frac{Event\%}{Non Event\%} \right)

[图片上传失败...(image-89de6b-1662436007540)]

INSERT OVERWRITE TABLE ecommerce_user_bin_count_v2 PARTITION(field, dt)
SELECT 30d.user_id, 30d.field_idx, 30d.event_type, 
    nvl(pos_ntile_1d, 9), nvl(neg_ntile_1d, 9), nvl(pos_ntile_3d, 9), nvl(neg_ntile_3d, 9),
    nvl(pos_ntile_7d, 9), nvl(neg_ntile_7d, 9), nvl(pos_ntile_30d, 9), nvl(neg_ntile_30d, 9),
    nvl(pos_gauss_rank_1d, 0), nvl(neg_gauss_rank_1d, 0), nvl(pos_gauss_rank_3d, 0), nvl(neg_gauss_rank_3d, 0),
    nvl(pos_gauss_rank_7d, 0), nvl(neg_gauss_rank_7d, 0), nvl(pos_gauss_rank_30d, 0), nvl(neg_gauss_rank_30d, 0),
    nvl(woe_1d, 0), nvl(woe_3d, 0), nvl(woe_7d, 0), nvl(woe_30d, 0), 
    nvl(odds_1d, 0), nvl(odds_3d, 0), nvl(odds_7d, 0), nvl(odds_30d, 0),
    nvl(odds_ratio_1d, 0), nvl(odds_ratio_3d, 0), nvl(odds_ratio_7d, 0), nvl(odds_ratio_30d, 0),
    30d.field, 30d.dt
FROM (
    SELECT dt, field, user_id, field_idx, event_type, 
        pos_ntile_30d, neg_ntile_30d,
        GAUSS_RANK(pos_rank_30d) pos_gauss_rank_30d,
        GAUSS_RANK(neg_rank_30d) neg_gauss_rank_30d,
        LN(1e-6+pos_ratio_30d/(1e-6+neg_ratio_30d)) AS woe_30d,
        odds_30d, odds_ratio_30d
    FROM (
        SELECT dt, user_id, field, field_idx, event_type,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_30d DESC)-1 AS pos_ntile_30d,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_30d DESC)-1 AS neg_ntile_30d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_30d DESC) AS pos_rank_30d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_30d DESC) AS neg_rank_30d,
            if(global_pos_cnt_30d=0, 0, pos_cnt_30d/global_pos_cnt_30d) AS pos_ratio_30d,
            if(global_neg_cnt_30d=0, 0, neg_cnt_30d/global_neg_cnt_30d) AS neg_ratio_30d,
            LN(1e-6 + pos_cnt_30d)-LN(1e-6 + neg_cnt_30d) AS odds_30d,
            LN(1e-6+pos_cnt_30d)-LN(1e-6+neg_cnt_30d)-LN(1e-6+global_pos_cnt_30d-pos_cnt_30d)+LN(1e-6+global_neg_cnt_30d-neg_cnt_30d) AS odds_ratio_30d
        FROM ecommerce_user_cumulate_count
        where dt>='20191031' AND total_cnt_30d>0
    )
) 30d
LEFT JOIN (
    SELECT dt, field, user_id, field_idx, event_type, 
        pos_ntile_7d, neg_ntile_7d,
        GAUSS_RANK(pos_rank_7d) pos_gauss_rank_7d,
        GAUSS_RANK(neg_rank_7d) neg_gauss_rank_7d,
        LN(1e-6+pos_ratio_7d/(1e-6+neg_ratio_7d)) AS woe_7d,
        odds_7d, odds_ratio_7d
    FROM (
        SELECT dt, user_id, field, field_idx, event_type,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_7d DESC)-1 AS pos_ntile_7d,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_7d DESC)-1 AS neg_ntile_7d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_7d DESC) AS pos_rank_7d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_7d DESC) AS neg_rank_7d,
            if(global_pos_cnt_7d=0, 0, pos_cnt_7d/global_pos_cnt_7d) AS pos_ratio_7d,
            if(global_neg_cnt_7d=0, 0, neg_cnt_7d/global_neg_cnt_7d) AS neg_ratio_7d,
            LN(1e-6 + pos_cnt_7d) - LN(1e-6 + neg_cnt_7d) AS odds_7d,
            LN(1e-6+pos_cnt_7d)-LN(1e-6+neg_cnt_7d)-LN(1e-6+global_pos_cnt_7d-pos_cnt_7d)+LN(1e-6+global_neg_cnt_7d-neg_cnt_7d) AS odds_ratio_7d
        FROM ecommerce_user_cumulate_count
        where dt>='20191031' AND total_cnt_7d>0
    )
) 7d
ON 30d.dt=7d.dt AND 30d.field=7d.field AND 30d.user_id=7d.user_id AND 30d.event_type=7d.event_type AND 30d.field_idx=7d.field_idx
LEFT JOIN (
    SELECT dt, field, user_id, field_idx, event_type, 
        pos_ntile_3d, neg_ntile_3d,
        GAUSS_RANK(pos_rank_3d) pos_gauss_rank_3d,
        GAUSS_RANK(neg_rank_3d) neg_gauss_rank_3d,
        LN(1e-6+pos_ratio_3d/(1e-6+neg_ratio_3d)) AS woe_3d,
        odds_3d, odds_ratio_3d
    FROM (
        SELECT dt, user_id, field, field_idx, event_type,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_3d DESC)-1 AS pos_ntile_3d,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_3d DESC)-1 AS neg_ntile_3d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_3d DESC) AS pos_rank_3d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_3d DESC) AS neg_rank_3d,
            if(global_pos_cnt_3d=0, 0, pos_cnt_3d/global_pos_cnt_3d) AS pos_ratio_3d,
            if(global_neg_cnt_3d=0, 0, neg_cnt_3d/global_neg_cnt_3d) AS neg_ratio_3d,
            LN(1e-6 + pos_cnt_3d) - LN(1e-6 + neg_cnt_3d) AS odds_3d,
            LN(1e-6+pos_cnt_3d)-LN(1e-6+neg_cnt_3d)-LN(1e-6+global_pos_cnt_3d-pos_cnt_3d)+LN(1e-6+global_neg_cnt_3d-neg_cnt_3d) AS odds_ratio_3d
        FROM ecommerce_user_cumulate_count
        where dt>='20191031' AND total_cnt_3d>0
    )
) 3d
ON 30d.dt=3d.dt AND 30d.field=3d.field AND 30d.user_id=3d.user_id AND 30d.event_type=3d.event_type AND 30d.field_idx=3d.field_idx
LEFT JOIN (
    SELECT dt, field, user_id, field_idx, event_type, 
        pos_ntile_1d, neg_ntile_1d,
        GAUSS_RANK(pos_rank_1d) pos_gauss_rank_1d,
        GAUSS_RANK(neg_rank_1d) neg_gauss_rank_1d,
        LN(1e-6+pos_ratio_1d/(1e-6+neg_ratio_1d)) AS woe_1d,
        odds_1d, odds_ratio_1d
    FROM (
        SELECT dt, user_id, field, field_idx, event_type,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_1d DESC)-1 AS pos_ntile_1d,
            NTILE(10) OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_1d DESC)-1 AS neg_ntile_1d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY pos_cnt_1d DESC) AS pos_rank_1d,
            PERCENT_RANK() OVER(PARTITION BY dt, user_id, field, event_type ORDER BY neg_cnt_1d DESC) AS neg_rank_1d,
            if(global_pos_cnt_1d=0, 0, pos_cnt_1d/global_pos_cnt_1d) AS pos_ratio_1d,
            if(global_neg_cnt_1d=0, 0, neg_cnt_1d/global_neg_cnt_1d) AS neg_ratio_1d,
            LN(1e-6 + pos_cnt_1d) - LN(1e-6 + neg_cnt_1d) AS odds_1d,
            LN(1e-6+pos_cnt_1d)-LN(1e-6+neg_cnt_1d)-LN(1e-6+global_pos_cnt_1d-pos_cnt_1d)+LN(1e-6+global_neg_cnt_1d-neg_cnt_1d) AS odds_ratio_1d
        FROM ecommerce_user_cumulate_count
        where dt>='20191031' AND total_cnt_1d>0
    )
) 1d
ON 30d.dt=1d.dt AND 30d.field=1d.field AND 30d.user_id=1d.user_id AND 30d.event_type=1d.event_type AND 30d.field_idx=1d.field_idx
;

10. 整体流程

详情请查看《工业级推荐系统中的特征工程》。

image.png

模型训练的代码请查看GitHub

五、实验结果

模型 AUC GAUC Recall Precision F1 Score remark
MLP-gauss 0.633 0.476 0.393 0.579 0.46825 odds+woe/item
MLP-ntile 0.642 0.474 0.400 0.584 0.474727 -
GBDT v2 0.558 0.562 0.681 0.444 0.537114 -
GBDT on od 0.549 0.539 0.867 0.439 0.58277 -
GBDT on ow 0.585 0.594 0.815 0.447 0.57738 -
LR 0.649 0.583 0.370 0.592 0.455 -

六、特征重要度分析

根据GBDT算法中特征带来的信息增益计算出特征重要度如下(截取了Top N个):

name importance
user_cat_0_cart_odds_ratio_30d 0.06663279235363007
weekday_hour 0.05106705102843989
user_cat_2_purchase_gauss_rank_7d 0.04943003132939339
cat_2_cart_odds_ratio_1d 0.042615607380867004
user_cat_0_view_gini_30d 0.0248174536973238
brand_cart_gini_7d 0.023471875116229057
cat_1_view_gini_7d 0.021432124078273773
category_cart_odds_ratio_1d 0.017213333398103714
cate_hour_cart_woe_30d 0.016498396173119545
code_view_gini_1d 0.015937425196170807
cat_0_cart_info_value_30d 0.01512192189693451
price_level_view_chi_square_1d 0.014924108050763607
cate_price_level_cart_woe_3d 0.013924989849328995
cat_2_view_gini_7d 0.01232975721359253
user_cat_2_cart_odds_ratio_30d 0.012204065918922424

原文链接:https://zhuanlan.zhihu.com/p/524575369

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,835评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,598评论 1 295
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,569评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,159评论 0 213
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,533评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,710评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,923评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,674评论 0 203
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,421评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,622评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,115评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,428评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,114评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,097评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,875评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,753评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,649评论 2 271

推荐阅读更多精彩内容