You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

419 lines
26 KiB
Markdown

This file contains invisible Unicode characters!

This file contains invisible Unicode characters that may be processed differently from what appears below. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to reveal hidden characters.

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 07回归分析怎样用模型预测用户的生命周期价值
你好,我是黄佳。
首先,恭喜你成功通过“获客关”。在获客关中,我们把互联网电商“易速鲜花”的用户们分成了高、中、低三个价值组,你还记得这个项目是属于监督学习还是无监督学习吗?没错,是无监督学习。
今天,我们开启“变现关”的挑战。而且我们将进入更为常见、更主流的机器学习应用场景,监督学习的实战。更确切地说,这是用监督学习解决回归问题的一次实战。
![](https://static001.geekbang.org/resource/image/fb/aa/fb47103938cab174b7093479f02485aa.jpg?wh=2284x1033)
监督学习主要应用于回归和分类两大场景分别可以用来预测数值和进行分类判断这两类问题也是我们课程的两大重点。在这节课中你就能够学到用回归来进行数值预测的方法了。因为这是监督学习项目所以我们会完全跟着前面讲的“机器学习实战5步”来走。
# 定义问题
请你想象一下你刚为客户做了分组画像老板推门而入“价值分组这个项目做得不错嘛现在有这么一个新需求你看看你这边还有什么idea。”
他继续说道“你也知道现在流量太贵了拉新成本平均下来每注册一个用户我几乎要花接近500元。我是这么想的。500元说贵也贵说不贵也不贵关键还是要看这些用户能给我们带来多大价值、多大回报你说对吧要是多数人用我们的App用几次就不用了订花的总消费甚至比500元还少那就没什么意思了。所以你看能不能根据历史数据预测一下新用户未来一两年的消费总额
那现在我们来分析一下这个问题。其实这个问题的实质是计算一个用户使用某产品的过程中消费总量是多少。比如某类手机App用户的平均使用长度是两年左右那么两年内用户在App上消费所产生的总收益就是用户的生命周期价值英文是Lifetime Value简称LTV有时我们也叫CLVCustomer Lifetime Value
请你想想看如果你得到一个新用户的成本是500元看上去是很贵但如果这个人在你的店铺买花的钱预计会超过万元那么扣除进货成本和获客成本你还是赚到了ROI高嘛。
![](https://static001.geekbang.org/resource/image/c0/b7/c0267aa171088a27f6c00470749749b7.png?wh=415x239 "用户的LTV越高我们ROI越高")
所以,我们的目标就是通过现有数据,找到一个能预测出用户生命周期价值的模型,来指导我们获客的成本,避免超出回报的盲目投入。
那对于这个问题,我们还是会使用上一个项目中的原始数据集,你在[这里](https://github.com/huangjia2019/geektime/tree/main/%E5%8F%98%E7%8E%B0%E5%85%B307)就能下载到。拿到数据集后,我们就可以开始数据的预处理工作了。
# 数据预处理
通过前面的课程,我想你应该已经想到:“用户生命周期价值”是一种连续性数值,对这种连续性数值的预测,属于一个回归问题。那么,在数据预处理阶段,我们就要确定把哪些特征字段输入到回归模型中。
![](https://static001.geekbang.org/resource/image/7b/e9/7bf08c35eecd0e311942269c801296e9.png?wh=709x161)
我们看到这个数据集的字段包括订单号、产品码、消费日期、产品说明、数量订单、单价、用户码和城市。那么哪些字段和用户的LTV相关呢
很显然像订单号、用户码、产品说明这些信息肯定和用户的LTV值是不相关的。你可能会说用户所在的“城市”这个字段也许和用户的消费能力有一定的关联。
这看起来有一定的道理。不过像“北京”、“上海”这样的文本字段肯定不能直接被输入回归模型如果要考虑城市信息也应该转换为0、1的哑变量值再输入模型。也就是说转换成“是否北京”(值是0或1)“是否上海”值是0或1“是否深圳”值是0或1……
你看这样一下子就增加了好多个特征而且对这个数据集来说城市对于LTV值的影响其实并不大。综合这些因素在这个项目中我们就不考虑“城市”这个字段了。
看到这你也许会想那这么说的话用户的消费金额肯定和LTV非常相关可是用户的消费金额不就是LTV本身嘛用自己去预测自己这样的模型有什么意义呢
这个想法没错用户的消费金额确实是和LTV最为相关的变量。不过我们要做两个小调整来解答你的困惑。
第一个调整是我们可以考虑用头3个月的消费金额而不是全部一整年的消费金额来预测用户后续一年或两年的“价值”。这样根据历史数据搭建起模型后对于新注册的用户我们只需要观察其头3个月的表现就能够预测他今后一、两年的消费总量。如果某类App用户平均使用长度是一年或两年的话这也就是该用户的生命周期价值。
第二个调整是用我们在前两节课中学到的R、F、M值来做特征变量这就避免了单一维度建模的局限性。在RMF用户分组中我们不仅可以得到消费金额还能得到新近度、消费频率。这些层级把消费频率、最近消费日期这些非数值变量转化成了数值变量而且这些数值与用户的LTV也都密切相关。
因此,**在我们的模型中可以用头3个月的R、F、M这3个数值作为特征也就是回归模型的自变量。而回归模型所要预测的因变量即数据集的标签就是一年的总消费额你可以认为它就是用户的LTV。**
我要说明一下这里的3个月、12个月都只是思路上的示意我们不去考虑用户平均会使用该App一年还是两年、三年。在不同业务场景中计算RFM特征值的时间区间和LTV的时间区间可以视情况而定。
下面我们要做的就是数据清洗,这其中包括删除不符合逻辑的负值、查看有没有缺失值、添加每个订单的总价字段等。这部分内容和上一讲的类似,我就不再重复了,你如果不清楚,可以去回顾[上一讲](https://time.geekbang.org/column/article/416824)的内容。
在数据预处理阶段唯一不同的就是当前项目需要显示数据集的时间跨度因为我们要拆分出头3个月的数据作为输入特征并且只考虑12月的总消费金额作为LTV所以我们要通过数据集的时间跨度来把它分为两部分一部分用来构建RFM特征个月另一部分用来构建LTV这个标签整个1个月
1. **整理数据集记录的时间范围**
通过这段代码,我可以知道当前数据集一共覆盖了多长的时间。
```typescript
import numpy as np #导入NumPy
import pandas as pd #导入Pandas
df_sales = pd.read_csv('易速鲜花订单记录.csv') #载入数据
print('日期范围: %s ~ %s' % (df_sales['消费日期'].min(), df_sales['消费日期'].max())) #显示日期范围(格式转换前)
df_sales['消费日期'] = pd.to_datetime(df_sales['消费日期']) #转换日期格式
print('日期范围: %s ~ %s' % (df_sales['消费日期'].min(), df_sales['消费日期'].max()))#显示日期范围
```
输出如下:
```typescript
日期范围(格式转化前): 1/1/2021 10:11 ~ 9/9/2020 9:20
日期范围(格式转化后): 2020-06-01 09:09:00 ~ 2021-06-09 12:31:00
```
结果显示数据集中的时间跨度是从2020年6月到2021年6月9号。
因为我们希望求的是整年的LTV所以这里我们把不完整的2021年6月份的数据删除
```typescript
df_sales = df_sales.loc[df_sales['消费日期'] < '2021-06-01'] #只保留整月数据
print('日期范围: %s ~ %s' % (df_sales['消费日期'].min(), df_sales['消费日期'].max())) #显示日期范围
```
输出如下:
```typescript
日期范围(删除不完整的月份): 2020-06-01 09:09:00 ~ 2021-05-31 17:39:00
```
目前的数据集中共包含了整整12个月的数据。下面我们开始构建机器学习数据集的特征和标签字段。
2. **构建特征和标签**
基于前面的分析我们用前3个月的R、F、M值作为特征字段然后把整个12个月的消费金额视为LTV作为标签字段。
首先我们把头3个月的销售数据拆分出来形成独立的df\_sales\_3m对象。这部分数据将是对用户LTV预测的依据。
```typescript
df_sales_3m = df_sales[(df_sales.消费日期 > '2020-06-01') & (df_sales.消费日期 <= '2020-08-30')] #构建仅含头三个月数据的数据集
df_sales_3m.reset_index(drop=True) #重置索引
```
接下来我们创建以用户码为主键的df\_user\_LTV对象利用头3个月的数据构建R、F、M层级形成新特征。具体的思路和步骤我们在[第5讲](https://time.geekbang.org/column/article/415910)中讲解过,我就不啰嗦了:
```typescript
df_user_LTV = pd.DataFrame(df_sales['用户码'].unique()) #生成以用户码为主键的结构
df_user_LTV.columns = ['用户码'] #设定字段名
df_user_LTV.head() #显示头几行数据
df_R_value = df_sales_3m.groupby('用户码').消费日期.max().reset_index() #找到每个用户的最近消费日期,构建df_R_value对象
df_R_value.columns = ['用户码','最近购买日期'] #设定字段名
df_R_value['R值'] = (df_R_value['最近购买日期'].max() - df_R_value['最近购买日期']).dt.days #计算最新日期与上次消费日期的天数
df_user_LTV = pd.merge(df_user_LTV, df_R_value[['用户码','R值']], on='用户码') #把上次消费距最新日期的天数(R值)合并至df_user结构
df_F_value = df_sales_3m.groupby('用户码').消费日期.count().reset_index() #计算每个用户消费次数,构建df_F_value对象
df_F_value.columns = ['用户码','F值'] #设定字段名
df_user_LTV = pd.merge(df_user_LTV, df_F_value[['用户码','F值']], on='用户码') #把消费频率(F)整合至df_user结构
df_M_value = df_sales_3m.groupby('用户码').总价.sum().reset_index() #计算每个用户三个月消费总额,构建df_M_value对象
df_M_value.columns = ['用户码','M值'] #设定字段名
df_user_LTV = pd.merge(df_user_LTV, df_M_value, on='用户码') #把消费总额整合至df_user结构
df_user_LTV #显示用户表结构
```
最后我们输出显示df\_user\_LTV对象就会看到头三个月的R值、F值、M值都已经作为特征存到我们的数据集df\_user\_LTV中了。到这里特征构建完毕。
![](https://static001.geekbang.org/resource/image/43/y2/430c2cdae13627cf5be68674cc1d4yy2.png?wh=211x167)
下面我们再来看怎么构建数据集的标签。
我们说过标签就是我们需要去预测或者判断的东西。而机器学习就是通过已知来预测未知通过训练数据集来寻找规律发现特征和标签之间的联系。所以我们下一步要做的就是把LTV值加入到df\_user\_LTV中这样数据集才完整。
我们先根据一整年的数据计算出每一个用户的LTV值也就是12个月的总消费金额
```typescript
df_user_1y = df_sales.groupby('用户码')['总价'].sum().reset_index() #计算每个用户整年消费总额,构建df_user_1y对象
df_user_1y.columns = ['用户码','年度LTV'] #设定字段名
df_user_1y.head() #显示头几行数据
df_LTV = pd.merge(df_user_LTV, df_user_1y, on='用户码', how='left') #构建整体LTV训练数据集
df_LTV #显示df_LTV
```
然后再把得到的LTV值整合到之前构建的df\_user\_LTV中就形成了完整的、带标签的LTV数据集。
![](https://static001.geekbang.org/resource/image/93/ea/93e42f2785abde4cdfbac43a48a088ea.png?wh=280x351)
现在在这个数据集中R、F、M值来自于头3个月收集的数据是模型的特征LTV值来自于整年的数据是模型的标签。这非常符合我们的目标**用短期数据,来预测用户的长期价值**。
数据集形成之后你会发现用户的数量从原来的981个减少到了361个这是因为在头三个月出现过消费行为的用户数就只有361个。所以我们后续基于这361个用户的数据来开展机器学习建模就可以了。
3. **创建特征集和标签集**
刚刚我们把特征和标签整合在一起,是为了形成完整的数据集。不过,标签集和特征集要分别输入机器学习模型,所以要分别创建。
我们先来构建特征集X
```typescript
X = df_LTV.drop(['用户码','年度LTV'],axis=1) #特征集
X.head() #显示特征集
```
在这段代码中我们除了移除了LTV值之外还移除了用户码字段。因为用户码对于回归模型的训练毫无意义而且用户码也是数字会对模型形成干扰。如果不移除的话机器就会把它也视作一个变量认为15291比15100大这显然不合逻辑。
然后我们再来构建标签集y。这里多说一句在机器学习中特征集的X大写标签集的y小写似乎是个惯例。这可能是因为通常情况下X是一个向量而y是一个数值。
```typescript
y = df_LTV['年度LTV'] #标签集
y.head() #显示标签集
```
构建好特征集和标签集后,我们就可以把它们拆分为训练集、验证集和测试集了。
4. **拆分训练集、验证集和测试集**
我们用scikit-learn工具包中的拆分工具train\_test\_split进行拆分
```typescript
from sklearn.model_selection import train_test_split
# 先拆分训练集和其它集
X_train, X_rem, y_train, y_rem = train_test_split(X,y, train_size=0.7,random_state = 36)
# 再把其它集拆分成验证集和测试集 
X_valid, X_test, y_valid, y_test = train_test_split(X_rem,y_rem, test_size=0.5,random_state = 36)
```
请你注意,这里我做了两重的拆分,至于为什么要这样做,你可以回顾下[第三讲](https://time.geekbang.org/column/article/414504)的内容。
最后我们得到的数据集X\_train、X\_valid和X\_test的字段与X中的字段还是一样y\_train、y\_valid、y\_test中的字段和y的也一样只是它们的行数发生了改变
* X\_train是288行×4列
* y\_train是288行×1列
* X\_valid是73行×4列
* y\_valid是73行×1列
* X\_test是73行×4列
* y\_test是73行×1列。
好,到这里,我们的数据准备工作就全部完成啦。在这个项目中,这部分工作几乎占了大头,好在我们已经攻克,下面我们一起进入选算法并创建模型的环节。
# 选择算法创建模型
因为这是一个回归问题,所以,在模型类型的选择方面,我们肯定使用的是回归算法。这是基于问题本身的性质而确定的,毋庸置疑。
不过我们说过,在机器学习中,能够解决回归问题的常见算法有不少:
![](https://static001.geekbang.org/resource/image/84/34/844348a55550d08968ffb1d0dcaf3a34.jpg?wh=2248x1265)
一般来说我们在解决具体问题的时候会选择多种算法进行建模相互比较之后再确定比较适合的模型。由于篇幅所限我们不会使用上述全部算法建立模型这里我会带你比较3种算法的效率最基本的线性回归模型、决策树模型和随机森林模型你可以自己试着使用其它的算法创建别的模型
线性回归我们已经用过了,它是通过梯度下降找到最优的权重和模型偏置的最基本的回归算法。这里,我会用它做为一个基准模型,把其它模型的结果与其相比较,来确定优劣。
而决策树算法简单地说是从样本数据的特征属性中通过学习简单的决策规则也就是我们耳熟能详的IF ELSE规则来预测目标变量的值。这个算法的核心是划分点的选择和输出值的确定。
下面,我给你画了一张图,来帮你理解决策树是怎么进行判断预测的。
![](https://static001.geekbang.org/resource/image/be/db/be2728ac9936a4c2dd1692c227dfffdb.jpg?wh=2248x2885)
你可以看到,这种算法是根据两个特征$x\_{1}$和$x\_{2}$的值以及标签y的取值来对二维平面上的区域进行精准分割以确定从特征到标签的映射规则。根据树的深度和分叉时所选择的特征的不同我们可以训练出很多棵不一样的树来。
而随机森林呢,就是由多棵决策树构成的集成学习算法。它既能用于分类问题,也能用于回归问题。而且无论是解决哪类问题,它都是相对优秀的算法。在训练模型的过程中,随机森林会构建多个决策树,如果解决的是分类问题,那么它的输出类别是由个别树输出的类别的众数而定;如果解决的是回归问题,那么它会对多棵树的预测结果进行平均。
关于集成学习,我后面还会单独拿出来给你讲解。现在你只需要知道,随机森林纠正了决策树过度拟合其训练集的问题,在很多情况下它都能有不错的表现。这里的“过拟合”,其实就是说模型对训练集的模拟过头了,反而不太适合验证集和测试集。
下面我们导入并创建线性回归模型、决策树模型和随机森林模型。
```typescript
from sklearn.linear_model import LinearRegression #导入线性回归模型
from sklearn.tree import DecisionTreeRegressor #导入决策树回归模型
from sklearn.ensemble import RandomForestRegressor #导入随机森林回归模型
model_lr = LinearRegression() #创建线性回归模型
model_dtr = DecisionTreeRegressor() #创建决策树回归模型
model_rfr = RandomForestRegressor() #创建随机森林回归模型
```
在代码中,有几个缩写我解释一下:
* lr是Linear Regression线性回归的缩写
* dtr是Decision Tree Regresssor决策树回归的缩写
* rfr是Random Forest Regressor随机森林回归的缩写。
对于决策树和随机森林算法来说它们既有回归算法Regressor也有分类算法Classifer。以后我们用到分类模型的时候我就会把决策树分类模型命名为model\_dtc把随机森林分类模型命名为model\_rfc其中的“c”就代表Classifer。
创建好模型之后,我们就可以开始训练机器了。
# 训练模型
我们直接对线性回归、决策树模型和随机森林模型进行训练、拟合:
```typescript
model_lr.fit(X_train, y_train) #拟合线性回归模型
model_dtr.fit(X_train, y_train) #拟合决策树模型
model_rfr.fit(X_train, y_train) #拟合随机森林模型
```
你不要小看上面这几个简单的**fit语句这是模型进行自我学习的关键过程**。我们前面说了在线性回归算法中机器是通过梯度下降逐步减少数据集拟合过程中的损失让线性函数对特征到标签的模拟越来越贴切。而在决策树模型中算法是通过根据特征值选择划分点来确定输出值的在随机森林算法中机器则是生成多棵决策树并通过Bagging的方法得到最终的预测模型。
不过,拟合之后的模型是否有效,我们还无法确定,需要进行验证集上的预测并验证预测结果。
# 评估模型
下面我们用这三种模型对验证集分别进行预测。
```typescript
y_valid_preds_lr = model_lr.predict(X_valid) #用线性回归模型预测验证集
y_valid_preds_dtr = model_dtr.predict(X_valid) #用决策树模型预测验证集
y_valid_preds_rfr = model_rfr.predict(X_valid) #用随机森林模型预测验证集
```
为了看看这些模型预测的LTV值是否大体上靠谱我们先来随机选择其中一行数据看看模型的预测结果。
```typescript
X_valid.iloc[2] #随便选择一个数据
```
这行数据的特征如下:
```typescript
R 1.00
F 153.00
M 1413.83
Name: 163, dtype: float64
```
然后我们再显示一下三个模型对这一行数据所预测的LTV值以及该用户的LTV真值。
```typescript
print('真值:', y_valid.iloc[2])  #真值
print('线性回归预测值:', y_valid_preds_lr[2])  #线性回归模型预测值
print('决策树预测值:', y_valid_preds_dtr[2])  #决策树模型预测值
print('随机森林预测值:', y_valid_preds_rfr[2]) #随机森林模型预测值
```
输出:
```typescript
真值: 4391.9399999999905
线性回归预测值: 7549.22894678151
决策树预测值: 4243.209999999997
随机森林预测值: 4704.671799999999
```
可以看到相对而言对这个数据点来说决策树和随机森林所预测的y值更接近真值。
当然,一个数据点接近真值完全不能说明问题,我们还是要用$R^2$、MSE等评估指标在验证集上做整体的评估比较模型的优劣。
下面我们用$R^2$指标,来评估模型的预测准确率:
```typescript
from sklearn.metrics import r2_score, median_absolute_error #导入Sklearn评估模块
print('验证集上的R平方分数-线性回归: %0.4f' % r2_score(y_valid, model_lr.predict(X_valid)))
print('验证集上的R平方分数-决策树: %0.4f' % r2_score(y_valid, model_dtr.predict(X_valid)))
print('验证集上的R平方分数-随机森林: %0.4f' % r2_score(y_valid, model_rfr.predict(X_valid)))
```
输出如下:
```plain
验证集上的R平方分数-线性回归: 0.4333
验证集上的R平方分数-决策树: 0.3093
验证集上的R平方分数-随机森林: 0.4677
```
我们把这个结果用图表来显示一下,会更加直观:
![](https://static001.geekbang.org/resource/image/47/69/477cb3f996975aa3475c6397bab40569.png?wh=384x267)
我们可以看到,在都没有经过任何参数设定的情况下,和线性回归、决策树相比,随机森林算法显示出了更好的预测能力。
最后,我们在随机森林上面运行测试集,并绘制出预测值和真值之间的散点图:
```
y_test_preds_rfr = model_rfr.predict(X_test) #用模型预随机森林模型预测验证集
plt.scatter(y_test, y_test_preds) #预测值和实际值的散点图
plt.plot([0, max(y_test)], [0, max(y_test_preds)], color='gray', lw=1, linestyle='--') #绘图
plt.xlabel('实际值') #X轴
plt.ylabel('预测值') #Y轴
plt.title('实际值 vs. 预测值') #标题
```
输出如下:
![](https://static001.geekbang.org/resource/image/ff/96/ffa9daf7d1b7c0f9697c08bb6a1f3696.png?wh=1000x699)
我们希望实际值和预测值基本上是相等的预测值越接近真值则误差越小。举例来说图中一个全年消费12000元的用户所预测出来的LTV值也在12000元左右。这样的情况越多就表明模型越准确。
现在有了这个机器学习模型我们再回过头看一下在这一讲的开始老板提出的问题如何判断获客成本是否过高根据模型预测结果我们可以进一步观察处于R、F、M各个层级中的用户看他们的LTV值大概是多少这样就不难得知每个层级的获客成本应该控制在什么范围了。
对于高RFM价值的客户来说我们可以适当多投入获客成本而对于低RFM价值的客户我们就要严格控制获客成本了。所以根据这个模型我们可以得出一个**获客成本的指导区间**。而且,通过该模型,我们**还可以便捷地计算出每个新用户的LTV值。**
# 总结一下
好,今天这一讲到这里就结束了,我们来回顾一下你在这节课中学到了什么。
在这一讲中我们应用机器学习的实战5步解决了一个回归问题。在这一过程中最重要的部分是构建特征也就是把原始数据转化成R值、F值和M值来作为新特征进行机器学习。而**这个过程本身就是一个很有意思的特征工程**。
![](https://static001.geekbang.org/resource/image/77/9f/7780d2ec6af77dd8f482d9551e65d49f.png?wh=616x424)
在模型选择的方面,我们使用普通的线性回归算法作为基准模型。然后,再拿其它的算法(这里我们选择的是决策树和随机森林)与之比较,从而找出更优的算法。请你注意,这里所谓的更优,仅针对于当前的场景而言,并不是说随机森林算法就一定优于线性回归算法。
当然,一般来说,随机森林简单且容易解释。如果对于任何一个特定问题,你能找到比随机森林还好的算法,那么就可以说是相当成功了。
在这次实战中,我们只是简单地调用模型,还并没有进行任何的参数优化步骤。以后,我们还会对随机森林算法做调优的工作。
# 思考题
这节课就到这里了最后我给你留3个思考题
1. 在这次实战中我们放弃了用户所在的“城市”这个信息请你使用Pandas中的get\_dummies这个工具来添加“城市”相关的哑变量然后添加到特征集中输入模型。
**提示**
```typescript
city = pd.get_dummies(df_sales.城市, prefix='城市')
df_sales = pd.concat([df_sales, city], axis=1)
```
2. 其实SVM和朴素贝叶斯也可以解决回归问题请你使用这两种算法或其它回归算法来尝试解决这个问题然后比较各个算法的优劣。
**提示**
```plain
from sklearn.svm import SVR
from sklearn.linear_model import BayesianRidge
```
3. 在验证时,我选择了$R^2$作为回归问题的评估指标,你能否尝试使用均方误差、中值绝对误差等评估指标,来验证我们的模型呢?
**提示**:除了$R^2$是越大越好之外,其它评估指标都是越小越好。
```plain
from sklearn.metrics import mean_squared_error
from sklearn.metrics import median_absolute_error
```
欢迎你在留言区分享你的想法和收获,我在留言区等你。如果这节课帮到了你,也欢迎你把这节课分享给自己的朋友。我们下一讲再见!
![](https://static001.geekbang.org/resource/image/5a/fa/5af212b016be19f8742bafbdd35b26fa.jpg?wh=2284x1149)