数据科学工程实践:用户行为分析与建模、A/B实验、SQLFlow
上QQ阅读APP看书,第一时间看更新

2.3 生存分析在二手车定价案例中的应用

前文详细阐述了生存分析的理论,本节将基于2.1节提到的二手车定价案例,带领大家通过Python代码实操完成生存分析及结果解读。在开始实操之前,我们先对二手车定价案例涉及的具体问题及数据进行界定,并对操作流程做简要介绍。

1. 问题聚焦

我们以帕萨特二手车定价为例,以90天为一个销售周期,如果二手车超过90天仍未销售出去,则车辆的收益为0,成本为90天消耗的仓储及维护总成本。在2.1节推导出的利润公式中,所有车辆购入成本Cp固定不变,为常数,为了简化问题,此处不考虑购入成本,我们只需要求得如下公式的最大值:

000

2. 数据获取

假设我们从平台上获得的数据信息如表2-1所示。

表2-1 二手车定价案例可获取的数据信息

000

3. 操作流程

下面围绕上述信息展开案例实操,操作流程分为以下几步。

  • 了解软件包及数据要求:介绍软件包和数据格式,并对样例数据进行必要的处理。
  • 掌握生存分析基础操作:绘制二手车销售生存曲线,使用对数秩检验判断不同生存曲线是否存在显著差异。
  • 掌握Cox比例风险模型操作及解读:通过Cox比例风险模型对变量如何影响二手车销售生存曲线展开分析及解读,并对个体维度的生存曲线进行预测。
  • Cox比例风险模型应用于最优价格求解:基于前三步的输出结果,结合利润公式求解不同定价下的利润水平,寻找最优价格。

2.3.1 软件包、数据格式和数据读入

1. 软件包

目前,Python中有支持生存分析的软件包Liflines和scikit-survival可供使用,其中Liflines对分析友好,可以支持生存函数的绘制、对数秩检验及Cox模型拟合,本节将通过Python代码实现基于Liflines包的生存分析。

2. 数据格式

生存分析的数据格式以研究对象为单位,每行包括生存时间(删失前观测生存时间),是否发生终点事件的标记及生存概率影响因子。在二手车定价案例中,原始数据以每一个待出售车辆为单位,每一行代表一个车辆的数据信息,信息可以分为以下两类。

  • 车辆出售情况:包括车辆是否发生终点事件(是否售出)和生存时间(在售时长)。
  • 车辆公开信息(影响因子):包括车辆颜色、行驶里程、车辆照片、用户访问次数和车辆出售价格。

原始数据中各变量的类型及描述如表2-2所示。

表2-2 二手车定价案例数据各变量说明

000

3. 数据读入及处理

数据读入及处理如代码清单2-1、代码清单2-2所示。

代码清单2-1 引入软件包

##**软件包引入**重要的软件包的介绍在代码备注中
import pandas as pd      ##引入基础分析软件包pandas
import numpy as np       ##引入基础分析软件包numpy
import matplotlib.pyplot as plt     ##引入绘图软件包
from lifelines import KaplanMeierFitter       ##引入生存分析包-KM生存曲线
from lifelines.statistics import logrank_test    ##引入生存分析包-logrank检验
from lifelines import NelsonAalenFitter       ##引入生存分析包-风险曲线
from lifelines import CoxPHFitter     ##引入生存分析包-Cox模型

代码清单2-2 读入原始数据

##**数据读入**
data_survival = pd.read_csv('input_survival_v2.csv', sep=',', encoding = 'GBK', index_col=False)
data_survival = data_survival[(data_survival['Publish_period']<=90)].reset_index(drop=True)
data_survival = data_survival.drop(['Departure_Date','End_Date','Car_id'],axis=1) ##删除不需要的字段
data_survival.head()

原始数据展示如图2-7所示。

000

图2-7 二手车定价案例原始数据格式

其中,Color为字符串,属于离散变量,需要对该字段进行处理,如代码清单2-3所示。

代码清单2-3 对离散变量进行处理

##**对离散变量进行处理**
df = pd.get_dummies(data_survival, drop_first=True, columns=['Color']) 
df.head()

处理后的数据如图2-8所示。

000

图2-8 二手车定价案例处理后的数据格式

2.3.2 绘制二手车销售生存曲线及差异对比

1. 绘制整体生存曲线

数据处理完毕后,我们绘制KM曲线。首先基于全量数据绘制二手车销售情况的生存曲线,如代码清单2-4所示。

代码清单2-4 绘制二手车销售情况随时间变化的整体生存曲线

##**绘制二手车销售情况随时间变化的整体生存曲线
kmf = KaplanMeierFitter()
T = df["Publish_period"]  ##T代表生存时长
E = df["is_sold"]         ##E代表关注事件(终点事件)
kmf.fit(T, event_observed=E,label='Survival Curve')

#kmf.plot(show_censors=True,ci_show=False) ##绘图,展示删失数据
kmf.plot()
plt.xlim()
plt.ylabel("est. probability of survival $\hat{S}(t)$")
plt.xlabel("day $t$")

绘制结果如图2-9所示。

000

图2-9 二手车销售生存曲线

如图2-9可知,50%生存概率对应天数在20天左右。

2. 绘制不同访问次数车辆的生存曲线并对比

前面绘制的是整体生存曲线,但在分析中,更需要对不同生存曲线进行绘制及对比,接下来我们绘制两条生存曲线,实现不同访问次数的车辆销售生存曲线对比,如代码清单2-5所示。

代码清单2-5 绘制不同访问次数的二手车销售情况随时间变化的生存曲线

##绘制不同访问次数的二手车销售情况随时间变化的生存曲线
avg_inquires=np.mean(df['N_Inquires'])

df_less_inquires=df[(df['N_Inquires']<avg_inquires)]
df_more_inquires=df[(df['N_Inquires']>avg_inquires)]

ax = plt.subplot(111)
kmf=KaplanMeierFitter()

kmf.fit(df_less_inquires['Publish_period'], 
        event_observed=df_less_inquires['is_sold'], 
        label="less_inquires") 

ax = kmf.plot(ax=ax) 
kmf.fit(df_more_inquires['Publish_period'], 
        event_observed=df_more_inquires['is_sold'], 
        label="more_inquires") 

ax = kmf.plot(ax=ax) 

plt.title("survival curves of two different type");

绘制结果如图2-10所示。

000

图2-10 不同访问次数二手车生存曲线

对两条曲线进行观察后发现,访问次数更少的车辆生存曲线更靠左,说明在相同时间下,访问次数少的车辆出售的概率更高,接下来我们通过对数秩检验对曲线差异显著性进行检验,如代码清单2-6所示。

代码清单2-6 验证不同访问次数二手车销售生存曲线是否有显著差异

##**验证不同访问次数的二手车销售生存曲线是否有显著差异
T1 = df_less_inquires["Publish_period"]
E1 = df_less_inquires["is_sold"]

T2 = df_more_inquires["Publish_period"]
E2 = df_more_inquires["is_sold"]

results = logrank_test(T1, T2, 
                       event_observed_A=E1, 
                       event_observed_B=E2)
results.print_summary()

检验结果如图2-11所示。

000

图2-11 不同访问次数二手车生存曲线差异检验

图2-11所示的结果显示P值小于0.05,可以认为两条曲线存在显著差异,那么我们可以直接得出访问越少,销售速度越快的结论吗?答案是否定的,我们在分析过程中需要警惕一点,那就是相关并不代表因果,出现这一现象有可能是因为我们只考虑访问数量一个因素,而这些访问少的车辆恰好价格更低或者行驶里程更短。因此,对于没有控制其他因素下的单一因素对比需要谨慎对待。

2.3.3 二手车销售生存概率影响因素分析及个体预测

1. 影响因素分析

接下来我们引入Cox比例风险回归模型,对车辆颜色、行驶里程、车辆照片、用户访问次数、车辆出售价格这5个变量进行分析,找出对生存函数有显著影响变量,如代码清单2-7所示。

代码清单2-7 引入Cox模型对特征进行解释

##**引入Cox模型对特征进行解释
cph = CoxPHFitter()
cph.fit(df, duration_col='Publish_period', event_col='is_sold',show_progress=True,step_size=0.5)
cph.print_summary() 
cph.plot() ##图形对特征进行解释

模型结果如图2-12所示。

000

图2-12 模型拟合结果(各特征系数及显著性)

由模型结果可见,5个变量对结果均有显著影响(P值均小于0.05),对于系数的解读如下。

  • 连续变量:行驶里程、照片数量、访问次数及价格的系数为负,代表均为保护因素,对销售速度有负向影响。控制其他因素后可以看到,访问次数依然有显著影响(一般情况下需要先对变量的相关性进行检验并剔除高度相关变量,此处在数据采样阶段已进行过相关性检验,因此分析过程中不再重复操作)。
  • 分类变量:以黑色为基准,棕色的系数为负,表示保护因素,说明棕色车对销售速度相对黑色车更差,而白色、银色车的销售速度优于黑色,其中白色最优。

模型中Concordance是解释模型整体效果的参数,与一般模型中的AUC含义类似,结果显示模型的Concordance为0.69,表现尚可。

2. 个体生存概率预测

在模型拟合之后,可以实现对个体生存概率的预测,我们采用Cox模型拟合结果对个体进行预测并随机抽取两个样本(车辆ID为24及11166)展示生存概率预测曲线,如代码清单2-8所示。

代码清单2-8 对个体生存函数进行预测

##**对个体生存函数进行预测
X = df.drop(['Publish_period','is_sold'],axis=1)  ##筛选特征集合,剔除销售时间与事件结局数据
surv_hat=cph.predict_survival_function(X)
## 抽取任意两个样本,观察预测结果
surv_hat[24].plot(label='24')
surv_hat[11166].plot(label='11166')
plt.legend()

两个样本的预测生存曲线绘制如图2-13所示。

000

图2-13 两个样本的预测生存曲线

以50%生存概率对应生存时间(中位生存时间)为预测生存时间,图2-13所示二者的预测生存时间分别为62天和41天。

2.3.4 基于Cox风险比例模型的最优价格求解

最优价格分析

在操作之前,我们首先来看一下最优价格的分析流程,如图2-14所示。

000

图2-14 二手车最优价格分析流程

基于分析流程,我们在目前发布的最低价与最高价之间创建等差序列,选定10个价格作为预选价格,并在其中选出利润最大时对应价格,如代码清单2-9所示。

代码清单2-9 基于预测生存函数寻找最优价格策略

##**基于预测生存函数寻找最优价格策略
## 第1步:创建获取预测在售天数函数
def predict_day(surv_hat):
    days = np.zeros(surv_hat.shape[1])
    prob = np.zeros(surv_hat.shape[1])
    j = surv_hat.shape[1]
    for i in range(1,surv_hat.shape[1]):
        prob[i-1] = surv_hat[surv_hat[i-1] >= 0.5][i-1].min()
        prob[j-1] = surv_hat[surv_hat[j-1] >= 0.5][j-1].min()
        days[i-1] = surv_hat[surv_hat[i-1] == prob[i-1]].index.values.min()
        days[j-1] = surv_hat[surv_hat[j-1] == prob[j-1]].index.values.min()
        ##以预测半衰期作为预测在售天数
    return prob,days   

## 第2步:创建90天是否卖出函数
def is_sold(data):
    y = np.zeros(data.shape[0])   
    for i in range(1,data.shape[0]):
        if data[i-1]>=0.6:
            y[i-1]=0
        else:
            y[i-1]=1
    return y

## 第3步:创建利润函数
def profit(data, predict_days,sold_tag):
    d = list(predict_days) 
    y = list(sold_tag)
    revenue = np.sum(data['Price']*y)
    cost = np.sum(1000*d)
    profit = revenue - cost    
    return profit
    
## 第4步:计算不同价格下的利润
min_price = df['Price'].values.min()
max_price = df['Price'].values.max()
sp_price = np.linspace(min_price, max_price, 10) ##选定十个预选价格
X = df.drop(['Publish_period','is_sold'],axis=1)

profit_list = []
price_list = list(sp_price)
   
for p in price_list:
    X['Price'] = p        
    surv_hat = cph.predict_survival_function(X)        
    prob_result,days_result = predict_day(surv_hat)
    sold_result = is_sold(prob_result)
    profit_result = profit(X,days_result,sold_result)
    profit_list.append(profit_result)

profit_res = pd.DataFrame({'price': price_list, 'profit': profit_list}) 

得出结果如图2-15所示。

000

图2-15 价格与利润对应关系图

由此可见,在目前价格水平下,收益处于单调递增阶段,即售价越高利润越高,目前最优价格处于价格上限21万。

至此,整个分析实操就结束了,我们通过生存分析实现了最优价格的筛选,在固定分析框架后,我们将代码整理打包,就能达到自动化定价的目的。需要说明的是,这一分析数据仅为样例数据,不代表真实情况,因此结果与现实世界相比的合理性不是最重要的,掌握分析方法并能够有效使用实操才是本节学习的关键。