干货 | 贝叶斯结构模型在全量营销效果评估的应用
作者简介
Yiwen,携程数据分析师,专注用户增长、因果推断、数据科学等领域。
一、背景
如何科学地推断某个产品策略对观测指标产生的效应非常重要,这能够帮助产品和运营更精准地得到该策略的价值,从而进行后续方向的迭代及调整。
在因果推断框架下,效果评估的黄金准则一定是“AB实验”,因为实验的分流被认为是完全随机且均匀的,在此基础上对比实验组与对照组的指标差异就可以体现某个干预带来的增量值。但是很多场景下,我们较难进行严格的AB实验,例如对于酒店的定价;现金奖励的发放等等,不适宜向不同人群展现不同的内容。对于这些问题,我们会采取因果推断的方法来进行策略的效果评估。
适用于有结构特征的时间序列数据 利用贝叶斯的思想来进行参数估计
(1) 称为观测方程,反映观测值与其背后隐藏状态的关系;(2) 称为状态方程,反映随时间推移各个状态之间的转换。;
Linear Local Trend(局部趋势):一定时间内的单调性(单调上升或下降) Seasonality(季节性因子):固定长度的变化,类似于一年四季的温度变化 Cyclical(周期性):类似季节性但波动时间不固定,波动频率也不固定的变化
图3-1:观测数据及其结构化元素。第一张图体现原数据的波动情况;第二张体现季节性因子的情况;第三张图体现局部趋势的情况。
如果希望在映射关系中加入协变量X,可以将(1)拓展为:
3.2 贝叶斯及MCMC(马尔可夫蒙特卡洛方法)
假设状态方程(2)中各个时刻的状态序列为
对θ设置先验分布 以及初始状态的分布 构造马尔科夫链,用MCMC方法得到 通过贝叶斯公式计算得到参数的后验分布
import tensorflow as tf
import tensorflow_probability as tfp
from causalimpact import CausalImpact
# 模型初始化 - 自定义时间序列数据:
def plot_time_series_components(ci):
component_dists = tfp.sts.decompose_by_component(ci.model, ci.observed_time_series, ci.model_samples)
num_components = len(component_dists)
mu, sig = ci.mu_sig if ci.mu_sig is not None else 0.0, 1.0
for i, (component, component_dist) in enumerate(component_dists.items()):
component_mean = component_dist.mean().numpy()
component_stddev = component_dist.stddev().numpy()
# 自定义观测方程以及真实值y:
def plot_forecast_components(ci):
component_forecasts = tfp.sts.decompose_forecast_by_component(ci.model, ci.posterior_dist, ci.model_samples)
num_components = len(component_forecasts)
mu, sig = ci.mu_sig if ci.mu_sig is not None else 0.0, 1.0
for i, (component, component_dist) in enumerate(component_forecasts.items()):
component_mean = component_dist.mean().numpy()
component_stddev = component_dist.stddev().numpy()
# 生成模拟数据,包括一个实验组数据(有干预)以及两条对照组数据(无干预)
observed_stddev, observed_initial = (tf.convert_to_tensor(value=1, dtype=tf.float32),tf.convert_to_tensor(value=0., dtype=tf.float32))
level_scale_prior = tfd.LogNormal(loc=tf.math.log(0.05 * observed_stddev), scale=1, name='level_scale_prior') # 设置先验分布
initial_state_prior = tfd.MultivariateNormalDiag(loc=observed_initial[..., tf.newaxis], scale_diag=(tf.abs(observed_initial) + observed_stddev)[..., tf.newaxis], name='initial_level_prior') # 设置先验分布
ll_ssm = tfp.sts.LocalLevelStateSpaceModel(100, initial_state_prior=initial_state_prior, level_scale=level_scale_prior.sample()) #训练时序模型
ll_ssm_sample = np.squeeze(ll_ssm.sample().numpy())
# 整合数据
x0 = 100 * np.random.rand(100) # 对照组1
x1 = 90 * np.random.rand(100) # 对照组2
y = 1.2 * x0 + 0.9 * x1 + ll_ssm_sample #生成真实值y
y[70:] += 10 #设置干预点
data = pd.DataFrame({'x0': x0, 'x1': x1, 'y': y}, columns=['y', 'x0', 'x1'])
# 调用模型:
pre_period = [0, 69] #设置干预前的时间窗口
post_period = [70, 99] #干预后的窗口
ci = CausalImpact(data, pre_period, post_period) #调用CausalImpact
# 对于causalImpact的使用我们核心需要填写三个参数:观测数据data、干预前的时间窗口、干预后的时间窗口。
# 输出结果:
ci.plot()
ci.summary()图4-3:展示CausalImpact输出的结果图,图1表示真实值与模型拟合值的曲线;图2表示每个时刻真实值与预测值的差异;图3表示真实值与预测值的累计差值。表3-1:展示CausalImpact输出的结果表格,量化效应值effect的估计及其置信区间,反映效应值是否具有显著性。107.71表示干预之后实际值的平均;96.25表示干预之后预测值的平均,3.28表示估计的标准差,[89.77,102.64]表示反事实估计的置信区间。11.46表示实际值与预测值的差距,[5.07,17.94]表示差值的置信区间,由于差距的置信区间在0的右侧,表示干预有显著的提升作用。
过程参数:我们可以使用Tensorflow中的Decomposition来查看时序模型中各个结构元素,包括周期性/季节性等等。
seasonal_decompose(data)
自定义参数:我们可以自定义参数的先验分布;迭代次数;周期性的时间窗口长度等等。往往参数调整会对结果输出有影响,例如正确的选取先验分布会让结果更准确;迭代次数更多能保证MCMC收敛更稳定(但也可能导致模型运行时间较长)等等。其中最重要的是对时间窗口长度的设置,需要正确地反映观测数据的周期性。如果是年维度数据以星期为周期则设置neasons=52;如果是天维度数据以小时为周期则设置neasons=24等等。
CausalImpact(..., model.args = list(niter = 20000, nseasons = 24))
自定义时序模型:causalImpact的包中默认使用BSTS模型进行训练,我们也可以改为其他的时序模型,但前提是需要对数据进行标准化。(如果使用默认的BSTS则不一定需要标准化)
from causalimpact.misc import standardize
normed_data, _ = standardize(data.astype(np.float32)) #标准化数据
obs_data = normed_data.iloc[:70, 0]
# 使用tfp中的其他模型来训练时序数据
linear_level = tfp.sts.LocalLinearTrend(observed_time_series=obs_data)
linear_reg = tfp.sts.LinearRegression(design_matrix=normed_data.iloc[:, 1:].values.reshape(-1, normed_data.shape[1] -1))
model = tfp.sts.Sum([linear_level, linear_reg], observed_time_series=obs_data)
# 将自义定时序模型代入CausalImpact包中
ci = CausalImpact(data, pre_period, post_period, model=model)
如果使用PSM,需要在大盘中寻找与推送人群相似但是没有被推送的用户作为对照组。但一般节假日推送时都会有兜底策略,几乎覆盖了95%以上的平台用户,较难从中找到符合条件但未被推送的人群来进行对照。 如果使用SCM,我们较难找到合适的对照组来合成。如评估度假BU的推送效果时,我们不太可能用火车、机票、酒店等各个产线合成一个“虚拟度假BU”,因为本身各个产线的用户需求就不同,使用这样合成的虚拟对照组来对比度假订单的转化率是不够科学的。 DID的方式也同理,我们很难找到一个满足平行趋势假设且业务场景相似的对照组来进行推送前后的对比。
综上所述,一些传统的因果推断方法纵使在技术上可行,在业务的解释性上也有所欠缺。而且,以上三种方式都没有考虑到用户购票行为的“时间周期性”。因此即使合成了对照组也不一定能够匹配到实验组真正的结构特点,进而导致效应值计算有偏。于是我们考虑首先验证用户购票的数据周期性;在定位到周期规律之后尝试使用BSTS模型结合CausalImpact来进行反事实值的预测。下文我们选择2022年端午的火车票营销推送场景进行实践。
我们以小时为周期窗口,通过简单的图像能够看出大盘的火车票下单人数确实随着时间推移呈现某种固定趋势。
考虑到端午作为节假日本身就有的自然流量增长,支付人数的提升不能完全归因于推送带来的,因此训练时序模型的时候,选取了19年-21年所有的端午数据(正端午前后10天)输入BSTS模型进行训练,得到端午这个窗口内的特有的结构状态,随后用这个结构化的模型来代入22年的端午数据,对2022年端午推送之后的转化人数做出预测。 最后使用真实的转化人数与预测人数作差体现本次营销推送的效果。
# 选取19-22年每年的端午窗口,按照小时划分,共960个数据点
y_hour=c(x1,x2,x3,x4)
x_time_hour=c(1:960)
data_hour <- cbind(y_hour, x_time_hour)
pre.period <- c(1, 808) # 2022年的推送发生在第808个时间点,故以此为干预节点。
post.period <- c(809, 960)
# nseasons=24, 迭代次数2000,fit the model
impact_hour <- CausalImpact(data_hour, pre.period, post.period, model.args = list(niter = 20000, nseasons = 24))
summary(impact_hour)
plot(impact_hour)
能够识别出数据背后的结构化特征,更好的做出预测; 利用了贝叶斯估计的思想,得到参数的后验分布情况,计算效应值时能够给出置信区间。但第(2)点对于BSTS模型是一把“双刃剑”,如果先验分布设置得不好,会影响MCMC的收敛速度和方向甚至最终的后验分布情况。因此对于先验分布的选取需谨慎。
【推荐阅读】
“携程技术”公众号
分享,交流,成长