任务背景
通过上一次对营销短信的人群与时段的出色分析,推广活动圆满达成了目标,当日订单量不断刷新新高。但与此同时,另一个问题暴露出来:部分供应商对活动准备不足,导致生产跟不上,很多商品发货周期变长,用户的整体满意度受到影响。
接下来即将迎来下一次的大促节日。这次节日同以往不同:用户在开始活动的前三天内只能浏览商品、收藏或添加购物车,不能下单,等三天过后才开始秒杀下单。为了避免再次出现准备不足的问题。
这一次的任务很明确:根据用户前三天预热期的行为记录,预测出每款商品的销量,这样可提前知会供应商按照预测销量进行准备,避免之前准备不足的问题。
这次能拿到的数据和上次一样。
- 用户行为表:最近 6 个月的用户行为记录。
- vip 会员表:用户 vip 会员开通情况。
- 用户信息表:用户的相关信息。
三个表的字段说明如下所示。
用户行为表(user_behavior_time_resampled.csv)
| 字段 | 说明 |
|---|---|
| user_id | 用户 id |
| item_id | 商品 id |
| seller_id | 店铺(商家)id |
| action_type | 行为类型:0 点击、1 加入购物车、2 下单、3 收藏 |
| brand_id | 品牌 id(存在缺失值) |
| time | 行为发生时间 |
vip 会员表(vip_user.csv)
| 字段 | 说明 |
|---|---|
| user_id | 用户 id |
| merchant_id | 商家 id(后重命名为 seller_id 以便关联) |
| label | 是否为该商家 VIP 用户:1 是、0 否 |
用户信息表(user_info.csv)
| 字段 | 说明 |
|---|---|
| user_id | 用户 id |
| age_range | 年龄段 |
| gender | 性别 |
| 其他 | 用户基础属性字段 |
问题分析
从任务说明可以发现,核心任务是从其他行为(点击、添加收藏、添加购物车)预测商品的销量。这和上一次的分析维度不同,这次主要是从商品维度进行分析。
三个数据源中,VIP 会员表和用户信息表基本都是用户的信息,而用户行为表则包含了商品的信息,所以这次重点从用户行为表入手。但面临的挑战是:用户行为表是面向行为的,每一行代表一个用户的一次行为,可能是点击、可能是加入购物车等。首先需要从这个表中抽象出一个面向商品的表,然后再尝试建立预测销量的模型。
数据清洗
首先导入必要的工具包以及加载用户行为表。
import pandas as pd
import numpy as np
df_user_log = pd.read_csv("data_set/user_behavior_time_resampled.csv")
df_vip_user = pd.read_csv("data_set/vip_user.csv")
先回顾 user_log 表的基本情况,打印出来看一下。
df_user_log
输出为:
该表一共有 1098 万行记录,用户的每一次行为就是一行。其中本次任务主要关心的是 item_id(用于区分商品),以及 action_type(用户行为的标记)。
现在查看这个表的缺失值记录。
df_user_log.isnull().sum()
输出:
user_id 0
item_id 0
cat_id 0
seller_id 0
brand_id 18132
time_stamp 0
action_type 0
timestamp 0
click 0
cart 0
order 0
fav 0
dtype: int64
brand_id 有 18132 个缺失值,但本次任务并不需要考虑 brand_id,所以不需要处理。
下一步查看 action_type 的取值。从数据集的描述来看,action_type 只存在 0-3 的取值,由于这个字段很重要,确认是否存在异常值。
df_user_log["action_type"].value_counts()
输出为:
0 9709458
2 659577
3 600738
1 15293
Name: action_type, dtype: int64
可以看到,action_type 的取值没有异常值,并且点击行为远高于其他三种行为,其中加到购物车的数量最少。
目前数据集已基本满足需求,接下来进入下一个阶段:特征工程。
特征工程
为什么需要特征工程
在绝大多数需要建立模型的数据分析任务里,特征工程都是必不可少的环节。顾名思义,特征工程就是指准备特征、筛选特征的工作,只是这项工作具有一定的重要性和复杂度,所以也会叫作工程。
以线性回归模型为例,通过上一节的学习,都知道线性回归模型是找出自变量和因变量的关系。现实问题中,因变量很好找,因为往往因变量就是预测的任务。比如在这个例子中,因变量就是预测目标:商品的销量。
另一方面,自变量,也称为模型的特征,往往没那么明显。一个原因是因变量往往受非常多的因素影响,有直接的也有间接的;另一个原因是拿到的数据集上往往没有现成的特征,需要从原始数据集中经过分析、变换之后才能得到要的特征。
特征分析
特征工程的第一步,是根据已有的数据(哪怕不是直接可用的数据)以及因变量,来预设出一组特征;第二步是从原始数据中计算出这些特征;第三步是用这些特征对模型进行训练,如果训练出的模型有问题,则需要重新回到第一步,尝试重新选择其他的特征。
这次的因变量是商品的销量,能够作为参考的数据是用户在预热期三天的行为。用户在浏览商品时不能下单,主要的行为就是:点击商品、把商品添加到购物车、收藏商品。那对于商品来说,商品的点击数、加购物车的次数、收藏的次数是否可以作为销量的特征呢?
判断特征是否有效,可以从特征的值改变是否会影响因变量的值这个标准出发。这三个特征,不管是点击数、收藏数还是加购物车的次数越多,就说明商品越受欢迎,则越可能有用户来购买,反之亦然。所以这三个特征都可以作为本次任务的特征。
除了商品维度的特征,商品所在店铺的特征是否对商品的销量有帮助呢?比如在一些商品数比较多的店铺,往往用户搜索进入的概率就大,那商品被用户看到的概率也就变大。另一方面,店铺的 VIP 用户多寡似乎也能说明店铺是否优质,优质店铺的商品理论上也更好卖一些。
初步总结了五个可能对商品销量有影响的特征。
商品维度:
- 点击数;
- 添加到购物车的次数;
- 收藏数。
商品所在店铺的维度:
- 店铺的商品数;
- 店铺所拥有的 VIP 用户数。
商品特征计算
首先尝试计算商品的特征。从三张表的描述可以得知,用户行为表中有用户对商品的行为记录,所以从用户行为表入手来计算商品的特征。
基本的思路就是把用户行为表按照 item_id 的维度聚合起来,点击数、加购物车数、收藏数和购买数都应该是聚合后的表的列。但目前用户行为表中没有这四列,相关的信息都由 action_type 来表示。
所以第一步,把 action_type 的内容展开成四列来分别表示:点击、加购物车、收藏和下单。
规则是:如果 action_type 是收藏时,则收藏列为 1,其他三列为 0;action_type 为下单时,下单一列为 1,其他三列为 0,以此类推。
这里涉及生成新列的时候,不仅是从现有列直接计算生成,还需要有一定的逻辑判断(判断 action_type 的值),所以用 Series 的 apply 函数来实现。apply 函数可以把一个函数执行到指定的 Series 上,然后把函数的返回值作为新的 Series 返回。
代码如下:
# 分别插入 click、cart、order、fav 四列,表示点击、加购物车、下单和收藏
# 规则见上面的文字描述
df_user_log["click"] = df_user_log.action_type.apply(lambda l:1 if l==0 else 0)
df_user_log["cart"] = df_user_log.action_type.apply(lambda l:1 if l==1 else 0)
df_user_log["order"] = df_user_log.action_type.apply(lambda l:1 if l==2 else 0)
df_user_log["fav"] = df_user_log.action_type.apply(lambda l:1 if l==3 else 0)
# 查看添加之后的 user_log 表
df_user_log
解释一下上面的代码,这里用到了 lambda 表达式。lambda 表达式是一种精简的函数表示法,形式如下:
def fn(参数):
return 参数表达式
在 apply 函数中使用 lambda 表达式,Series 会对每个元素都执行一次这个表达式,当前执行的元素就是 lambda 表达式的参数,也就是 l。然后每一次执行,都会判断 l 的值(也就是 action_type)来决定返回 0 还是 1。
最后,所有 lambda 表达式的结果汇总起来成一个新的 Series,作为 apply 函数的返回值,然后将其赋值给创建的新列。
可以看到 click 这一列都是 1,而其他三列都是 0。click 行为本身就远比其他三个多,理论上也说得通。为了避免有 Bug,还是查看一下这几列的数据分布。
print("click 数据分布:\n",df_user_log.click.value_counts())
print("order 数据分布:\n",df_user_log.order.value_counts())
print("fav 数据分布:\n",df_user_log.fav.value_counts())
print("cart 数据分布:\n",df_user_log.cart.value_counts())
输出为:
click 数据分布:
1 9709458
0 1275608
Name: click, dtype: int64
order 数据分布:
0 10325489
1 659577
Name: order, dtype: int64
fav 数据分布:
0 10384328
1 600738
Name: fav, dtype: int64
cart 数据分布:
0 10969773
1 15293
Name: cart, dtype: int64
从上面的数据可以看出,基本符合预期。
之后,从原始的行为表中筛选出感兴趣的字段。
df_clean = df_user_log[["item_id", "click", "cart", "order","fav"]]
df_clean
输出为:
筛选之后,行没有变化,但只剩下五列了。接下来,将这个表以 item_id 聚合,同一个 item_id 的点击数、下单数、加购物车数和收藏数进行求和。这样就可以得到每个商品的汇总结果了。
df_item = df_clean.groupby(["item_id"]).sum()
df_item
输出为:
可以看到,输出的表中 item_id 已经成为索引,并且结果表只剩下 75 万行记录。核心原因就是把 item_id 相同的记录聚合了,所以现在 df_item 已经是一个商品维度的特征表。
以上面的 1113162 号商品为例,数值显示它被点击过 17 次、没有被加过购物车、被购买过两次、被收藏过一次。
至此,商品维度的特征就准备完毕了。
店铺特征计算
店铺特征包含两个,需要分别进行计算。但一致的是,都希望得到一个以店铺 id,也就是 seller_id 作为 index 的表,而特征则是表的列。
(1)店铺拥有的 vip 用户数
首先回顾一下 vip 用户表。
df_vip_user
输出为:
表里的 merchant_id 就是商家的 id,但是在用户行为表中,商家的 id 字段为 seller_id。所以为了后续对应方便,首先需要将该列重命名为 seller_id。
然后计算每一个商家的 VIP 用户数。该表存储的是用户是否是某个商家的 VIP,label 字段为 1 代表是。这里只考虑商家维度,所以只需简单按照 seller_id 聚合该表,让 seller_id 一致的记录对应的 label 字段求和,即可得到每个商家的 VIP 用户数。
# 重命名 merchant_id 为 seller_id
df_vip_user = df_vip_user.rename( columns={"merchant_id" : "seller_id"})
# 选取 seller_id和label 这两列,并将结果通过 seller_id 聚合,相同 seller_id 的记录的label求和
df_brand_vip_users = df_vip_user[["seller_id", "label"]].groupby("seller_id").sum()
# 查看保存了店铺 vip 用户数的表的内容
df_brand_vip_users
输出为:
聚合完毕后,表的索引就会是 seller_id,label 列则代表 VIP 用户的数量。比如最后一行记录的含义就是 id 为 4993 的店铺有 4 个 VIP 用户。这样,第一个店铺特征表已经准备完毕,表的名字为 df_brand_vip_user。
(2)计算每个店铺的商品数
计算店铺的商品数,首先从用户行为表中筛选出 seller_id 和 item_id。因为只关心店铺的商品数,所以先按 item_id 去重,这样留下来的记录就是商品的总数。然后在这个表的基础上,将 item_id 列都赋值为 1,再按 seller_id 聚合,让 item_id 做求和运算,就可以得出每个 seller_id 对应的商品数。
代码如下:
# 筛选出 seller_id 和 item_id
df_seller_item_count = df_user_log[["seller_id", "item_id"]]
# 按item_id 去重,因为 itemid 一样的记录,seller_id 肯定也一样,所以 seller_id没有影响
df_seller_item_count = df_seller_item_count.drop_duplicates("item_id")
# 将 item_id 列赋值为 1,方便做求和
df_seller_item_count["item_id"] = 1
# 按seller_id 聚合,然后针对 item_id 列求和
df_seller_item_count = df_seller_item_count.groupby("seller_id").sum()
# 将 item_id 列改为 item_count, 避免有歧义
df_seller_item_count = df_seller_item_count.rename(columns = {"item_id" : "item_count"})
# 查看结果
df_seller_item_count
输出为:
可以看到,第二个店铺特征的表也准备好了,index 为 seller_id,有一列 item_count 代表店铺的商品总数。
商品-店铺特征关联
接下来,把两个店铺特征表拼接到商品特征表中。
先看一下商品特征表。
df_item
输出为:
可以看到,目前商品特征表中只有 item_id,没有 seller_id,所以没有办法直接关联店铺特征表。所以第一步,首先需要给商品特征表增加 seller_id 字段。
要增加 seller_id,只需用类似之前的方法,从原始行为表中抽取出 seller_id 和 item_id 的对应关系表,然后再将对应关系表拼接进商品特征表中即可。代码如下:
# 从原始行为表中取出 item_id 和seller_id ,构成新表
df_brand_item_map = df_user_log[["item_id", "seller_id"]]
# 按照item_id去重,去重后得到的结果就相当于是 item id 和 seller_id 的映射关系
df_brand_item_map = df_brand_item_map.drop_duplicates("item_id")
# 将 df_brand_item_map 映射进 df_item, 以 item_id 为 key
df_item = df_item.merge(df_brand_item_map, how="left", on="item_id")
# 查看最新的商品特征表
df_item
输出:
可以看到,现在商品特征表已经多了 seller_id 字段,接下来将两个店铺特征表拼接进商品特征表。
首先拼接店铺的 VIP 用户数表。代码如下:
# 将店铺vip用户特征表拼接到商品特征表中,以 seller_id 为 key
df_item = df_item.merge(df_brand_vip_users,how = "left", on="seller_id")
df_item
输出:
从输出来看,店铺 VIP 会员数已经拼接到表里,但出现了空值,这也是符合预期的。毕竟 VIP 用户表里不是每个店铺都有 VIP 用户。为了不影响后续的模型,通过之前学习的填补缺失值的方法,给空值填为 0,这也是符合逻辑的。
# 用 0 填充缺失值
df_item["label"] = df_item["label"].fillna(0)
df_item
输出:
可以看到,通过 fillna 之后,缺失值被替换为 0。
下一步,拼接店铺的商品数特征表。代码如下:
df_item = df_item.merge(df_seller_item_count, how = "left", on = "seller_id")
df_item
输出:
现在商品特征表已经完成,不仅有商品的点击、加购物车、下单和收藏记录,还有商品所在店铺的 VIP 用户数和店铺的商品数。下一步基于这张表建立模型进行分析。
回归分析
通过特征工程的环节,已经整理好了能够预测商品销量的特征表,下面建立线性回归模型进行分析。
拆分训练、测试集合
在训练之前,还需要考虑一个问题:训练出模型后,如何衡量模型的好坏?总不能等到真正要在生产环境用上模型的时候才进行测试。
一般常见的做法是,将现有的数据拆成两份,比如 70% 一份、30% 一份。70% 的数据用来训练模型,训练完之后用剩下的 30% 的数据进行测试。这样就能够在模型上线之前就能衡量模型的好坏。sklearn 提供了现成的 train_test_split 函数,可以帮助实现数据集的分割。
首先第一步,从商品特征表中拆开自变量和因变量。
- 因变量是很明显的,就是 order 列,代表商品被下单的数量。
- 自变量就是除了 order 列之外的所有数值特征,也就是从商品特征表删掉 item_id、seller_id 和 order 列,剩下的都可以作为自变量。
代码如下:
# 导入分割的方法
from sklearn.model_selection import train_test_split
# 自变量的数据表
X = df_item.drop(columns=["order", "seller_id","item_id"])
# 因变量
y = df_item["order"]
# 分别切割出训练集,测试集,测试机的比例是 20%
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.20, random_state=42)
# 查看自变量的测试集合
X_train
输出:
商品特征表本身有 75 万行。这里打印出的训练集有 60 万行,刚好占比 80%。可以看到 train_test_split 函数工作正常。
建立线性回归模型
接下来的步骤比较简单:建立回归模型,然后调用 fit 方法从 X_train 和 y_train 中训练模型。
linear_model = LinearRegression()
linear_model.fit(X_train, y_train)
训练结束之后,用 linear_model 来 predict X_test,然后拿 predict 函数返回的结果和 y_test 比较,就能知道模型的误差。
# predict X_test 对应的 y 值
y_pred = linear_model.predict(X_test)
# 用 scores 方法,查看模型自变量和因变量的相关性
print("Scores:", linear_model.score(X_test, y_test))
# 查看 predict y 和 y test的平均绝对误差print("MAE:", mean_absolute_error(y_test, y_pred))
# 查看模型的 b 值
print("intercept:", linear_model.intercept_)
# 查看模型的系数
print("coef_:", linear_model.coef_)
输出:
Scores: 0.4417000916369106
MAE: 0.9939945446528016
intercept: 0.32490105754921395
coef_: [-1.09570715e-03 7.64421017e+00 7.20958736e-01 6.07684870e-04
-3.10573289e-04]
模型的相关性分数是 0.44,对于现实世界中的回归问题来说也算是不错的成绩,说明特征还是很大程度能够影响下单量。MAE 是 0.99,说明对于测试集而言,模型预测的结果和实际的真实结果非常接近,说明模型的拟合比较好。
从模型系数可以看到,前三个特征 click、fav、cart 的系数比较大,后两个特征 label 和 item_count 的系数比较小,说明对于商品的下单量而言,前三个特征更加重要、关联性更强,后两者则对结果影响相对较小。
有时候凭主观判断对特征重要性的判断可能不准,可以都一并带上,然后在模型训练环节通过拟合算法自动找到不同特征的重要性,这样就比人的判断可靠得多。
测试一下模型:假设某个商品在预热期间一共有 100 次点击、2 次加购物车、6 次收藏,这个商品所在的店铺有 10 个 vip 用户、一共有 30 个商品,根据模型预测这个商品的下单数:
print(linear_model.predict([[100, 2 ,6, 10, 30]]))
输出:
[19.82626275]
代表可能会被买 19 次。
小结
至此,本文实战介绍完毕,总结一下本文主要学习的内容。
- 回归类型任务的分析思路与方法;
- 常见的特征工程的思路与方法;
- 通过 df.groupby("xxx").sum() 的形式来聚合数据表;
- 通过 df.merge 函数来拼接数据表,前提是两个数据表有相同的字段;
- 通过 train_test_split 来将数据集拆分为训练集和测试集;
- 通过 LinearRegression 来建立线性回归模型。
通过本文的学习,可以对回归预测类型的分析建立初步认识。当然这是一块非常大的领域,挑战也很多,本文重点还是以入门为主,有兴趣的读者可以自行深入研究。
版本差异(数据科学栈 → 当前版本)
| 库 | 本文编写时 | 当前稳定版 | 升级要点 |
|---|---|---|---|
| Python | 3.8-3.12 | 3.14 | 3.12+ 起性能显著提升;3.14 PEP 649/750 |
| NumPy | 1.x/2.0 | 2.3.x | np.float_ 等别名移除;NEP 50 类型提升 |
| Pandas | 1.x/2.x | 3.0.x | Copy-on-Write 默认开启;inplace 移除;字符串 dtype 变化 |
| Matplotlib | 3.x | 3.x 稳定版 | API 兼容,样式更新 |
| Seaborn | 0.12/0.13 | 0.13.x | API 稳定 |
| scikit-learn | 1.x | 1.7.x | API 稳定,新算法持续加入 |
本文讲解的数据分析流程(读取→清洗→分析→可视化)与核心 API 在最新版本中成立;升级时重点关注 Pandas 3.0 的 Copy-on-Write 与 NumPy 2.x 的类型变化。