基于经典A / B测试和自举方法的相食性计算

本文讨论了一种基于经典A / B测试的移动应用程序食人化计算方法。 在这种情况下,与禁用广告的组中的目标操作相比,作为广告来源(Direct,Criteo,AdWords UAC等)的重新分配过程的一部分,应考虑并评估目标操作。

本文概述了比较独立样本的经典方法,并提供了简要的理论基础,并描述了所使用的库,包括 简要描述了bootstrap方法的本质及其在FaceBook Bootstrapped库中的实现,以及在应用这些技术时在实践中出现的问题以及如何解决这些问题。

为了保持保密协议,对证据进行了混淆或没有提供。

将来,我计划在出现​​新事实时对本文进行补充和稍加修改,因此可以将此版本视为第一个版本。 我将不胜感激这些评论和评论。

引言


食人化是指将流量完整且针对性地从一个渠道流向另一个渠道的过程。

营销人员通常将此指标用作计算CPA的附加K系数:计算出的CPA乘以1 +K。 在这种情况下,CPA表示吸引流量的总成本/直接通过货币化的目标操作数(即带来实际利润-例如定向呼叫和/或间接货币化-例如增加广告数据库的数量,增加受众等)。

当免费渠道(例如,来自自然SERP的访问,对供我们免费使用的网站上的链接的点击)被蚕食以付费(直接,Adwords代替有机广告,在社交网络供稿中投放广告而不是点击广告)时,分组等等),这会带来财务损失的风险,因此了解同化率非常重要。

在我们的案例中,任务是通过从Criteo广告网络的转换来计算“有机”转换到应用程序的吞噬率。 监视是设备或用户uid(GAID / ADVID和IDFA)。

实验准备


您可以通过将AdJust分析系统界面中的用户分为几组来为实验准备受众,以将分别从GAS或ADVID和IDFA中看到广告的用户与从特定广告网络中看到广告的用户(对照组)隔离开来。 (AdJust提供了Audience Builder API)。 然后,在对照样本中,您可以在实验中研究的广告网络中包含一个广告活动。

我从我自己身上注意到,从直观上看,在这种情况下,以下实验的实施将更胜任:选择四个小组-从所有渠道(1)重新定向残疾的人作为实验组,以及仅使用Criteo(2)启用了重新定向; 仅使用Criteo(3)禁用了重定向功能的用户,打开了所有重定向功能(4)的用户。 然后,有可能计算出(1)/(2),它已经收到了Criteo网络进行同种化的广告活动的实际价值,从而实现了向应用程序的“有机”过渡;以及(3)/(4),已经在“自然”环境中获得了Criteo的同质化(因为Criteo,显然也可以蚕食其他付费频道)。 应该对其他广告网络重复相同的实验,以了解它们各自的影响; 在理想的世界中,调查占总流量最大份额的所有主要付费来源之间的相互吞没关系会很不错,但是这会花费很多时间(从开发的角度准备实验和评估结果),这会导致对不合理细致的批评。

实际上,我们的实验是在条件(3)和(4)下进行的,样品的比例为10%至90%,实验进行了2周。

数据准备和验证


在开始任何研究之前,重要的一步是胜任的预培训和数据清理。

应该注意的是,实际上,实验期间的活动设备比完整初始样本中的设备少2倍(分别为对照组和实验组的42.5%和50%),这可以通过数据的性质来解释:

  1. 首先(这是关键原因),在Adjust中用于重新定位的选择包含曾经安装过该应用程序的所有设备的标识符,即不再使用的设备以及已经与该应用程序一起使用的设备的标识符已删除
  2. 其次,在实验过程中不必所有设备都已登录到应用程序。

但是,我们根据完整样本中的数据计算了同类相食。 就我个人而言,这种计算的正确性似乎还没有定论点-一般而言,我认为清除所有卸载了该应用程序且未使用相应标签安装该应用程序的人以及那些未登录该应用程序超过一年的人更为正确-在这段时间内,用户可以更换设备; 减号-通过这种方式,在实验中,如果我们在Criteo网络上向他们展示广告,则那些未切换到该应用程序但可以执行该操作的用户可以从选择中删除。 我要指出,在一个美好的世界中,所有这些被迫忽视和假设都应分别进行调查和核实,但我们生活在一个快速而毛茸茸的世界中。

在我们的情况下,检查以下几点很重要:

  1. 我们检查初始样本中的交集-实验和对照。 在正确实施的实验中,不应存在此类交集,但在我们的情况下,对照中的实验样品有多个重复项。 在我们的情况下,这些重复项在实验中涉及的设备总量中所占的份额很小;因此,我们忽略了这种情况。 如果重复次数大于1%,则应认为该实验是不正确的,并且应在事先清理完重复样本后再进行第二次实验。
  2. 我们检查实验中的数据是否确实受到了影响-实验样本中的重定位应该已被禁用(至少在正确设置的实验中使用Criteo-从所有渠道都已禁用),因此,在使用Criteo重定位时,有必要检查实验中是否没有DeviceID。 在我们的案例中,实验组的DeviceID仍然可以重新定位,但不到1%,可以忽略不计。

直接评估实验


我们将考虑以下目标指标的变化:绝对-通话次数,相对-控件(在Criteo网络上看到广告)和实验性(广告被禁用)组中每个用户的通话次数。 在下面的代码中,变量数据引用了pandas.DataFrame结构,该结构由实验或控制样本的结果形成。

有用于评估无关样本中值差异的统计显着性的参数方法和非参数方法。 参数评估标准可提供更高的准确性,但在其应用方面存在局限性-特别是,主要条件之一是样本中观测值的测量值应呈正态分布。

1.研究样本中值的正态分布


第一步是使用标准检验-在sklearn.stats库中实施的Kolmogorov-Smirnov和Shapiro-Wilks标准以及Bartlett检验,检查现有样本的值分布类型和方差是否相等,p值= 0.05:

#    : def norm_test(df, pvalue = 0.05, test_name = 'kstest'): if test_name == 'kstest': st = stats.kstest(df, 'norm') if test_name == 'shapiro': st = stats.shapiro(df) sys.stdout.write('According to {} {} is {}normal\n'.format(test_name, df.name, {True:'NOT ', False:''}[st[1] < pvalue])) #    : def barlett_test(df1, df2, pvalue = 0.05): st = stats.bartlett(df1, df2) sys.stdout.write('Variances of {} and {} is {}equals\n'.format(df1.name, df2.name, {True:'NOT ', False:''}[st[1] < pvalue])) 

另外,为了对结果进行视觉评估,可以使用直方图功能。

 data_agg = data.groupby(['bucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}).fillna(0) data_conv = data_agg['calls_auto']/data_agg['device_id'] data_conv.hist(bins=20) 

图片

您可以按以下方式读取直方图:样本中有10次转换为0.08,1-0.14。 这并没有说明作为任何转换指标的观察对象的设备数量。

在我们的情况下,样本中参数值的绝对值和相对值(设备调用次数)的分布都是不正常的。
在这种情况下,您可以使用标准sklearn.stats库中实现的非参数Wilcoxon检验,也可以尝试将样本中的值分布恢复为正常形式并应用其中一个参数标准-学生的aka t检验或Shapiro-Wilks检验。

2.将样本中的值的分布减少为标准形式的方法


2.1。 子桶

使分布恢复正常的一种方法是子桶方法。 它的本质很简单,并且以下数学论据是理论基础:根据经典的中心极限定理,均值的分布趋于正态-n个独立的均匀分布的随机变量之和具有接近于正态的分布,并且等效地,前n个独立的均匀分布的随机变量的样本均值的分布数量趋于正常。 因此,我们可以将现有存储桶拆分为子存储桶y,因此,取每个存储桶的子存储桶y的平均值,可以得到接近正态的分布:

 #   subbucket' data['subbucket'] = data['device_id'].apply(lambda x: randint(0,1000)) # Variant 1 data['subbucket'] = data['device_id'].apply(lambda x: hash(x)%1000) # Variant 2 

拆分有很多选择,这都取决于开发人员的想象力和道德原则-您可以采取诚实的随机选择,也可以使用原始存储桶中的哈希,从而考虑了在方案中发布它的机制。

但是,实际上,从几十个代码启动中,我们只收到一次正态分布,也就是说,这种方法既不能保证也不稳定。

另外,目标操作和用户与子存储桶中的操作和用户总数之比可能与初始backets不一致,因此您必须首先检查该比例是否得到维持。

 data[data['calls'] > 0].device_id.nunique()/data.device_id.nunique() # Total buckets = data.groupby(['bucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}) buckets[buckets['calls'] > 0].device_id.nunique()/buckets.device_id.nunique() # Buckets subbuckets = data.groupby(['subbucket']).aggregate({'device_id': 'nunique', 'calls': 'sum'}) subbuckets[subbuckets['calls'] > 0].device_id.nunique()/subbuckets.device_id.nunique() # Subbuckets 

在进行此类验证的过程中,我们发现未保留子存储桶相对于原始选择的转换率。 由于我们需要额外保证输出样本和源样本中的呼叫份额比率的一致性,因此我们使用类平衡并增加权重,以便按子组分别选择数据:与具有目标操作的观察分开,与与没有目标操作的观察按正确的比例分开进行选择。 此外,在我们的案例中,样本分布不均; 从直觉上看,平均值似乎不应改变,但是样本的不均匀性如何影响方差从色散公式中并不明显。 为了阐明样本大小的差异是否会影响结果,使用Xi平方标准-如果检测到统计学上显着的差异,则将对较大的数据帧进行较小的采样:

 def class_arrays_balancer(df1, df2, target = 'calls', pvalue=0.05): df1_target_size = len(df1[df1[target] > 0]) print(df1.columns.to_list()) df2_target_size = len(df2[df2[target] > 0]) total_target_size = df1_target_size + df2_target_size chi2_target, pvalue_target, dof_target, expected_target = chi2_contingency([[df1_target_size, total_target_size], [df2_target_size, total_target_size]]) df1_other_size = len(df1[df1[target] == 0]) df2_other_size = len(df1[df1[target] == 0]) total_other_size = df1_other_size + df2_other_size chi2_other, pvalue_other, dof_other, expected_other = chi2_contingency([[df1_other_size, total_other_size], [df2_other_size, total_other_size]]) df1_target, df2_target, df1_other, df2_other = None, None, None, None if pvalue_target < pvalue: sample_size = min([df1_target_size, df2_target_size]) df1_rnd_indx = np.random.choice(df1_target_size, size=sample_size, replace=False) df2_rnd_indx = np.random.choice(df2_target_size, size=sample_size, replace=False) df1_target = pd.DataFrame((np.asarray(df1[df1[target] == 1])[df1_rnd_indx]).tolist(), columns = df1.columns.tolist()) df2_target = pd.DataFrame((np.asarray(df2[df2[target] == 1])[df2_rnd_indx]).tolist(), columns = df2.columns.tolist()) if p_value_other < pvalue: sample_size = min([df1_other_size, df2_other_size]) df1_rnd_indx = np.random.choice(df1_other_size, size=sample_size, replace=False) df2_rnd_indx = np.random.choice(df2_other_size, size=sample_size, replace=False) df1_other = pd.DataFrame((np.asarray(df1[df1[target] == 0])[df1_rnd_indx]).tolist(), columns = df1.columns.tolist()) df2_other = pd.DataFrame((np.asarray(df2[df2[target] == 0])[df2_rnd_indx]).tolist(), columns = df2.columns.tolist()) df1 = pd.concat([df1_target, df1_other]) df2 = pd.concat([df2_target, df2_other]) return df1, df2 exp_classes, control_classes = class_arrays_balancer(data_exp, data_control) 

在输出中,我们获得大小平衡且与初始转化率保持一致的数据,即已正常分布的研究指标(针对子桶的平均值计算得出),既可以从视觉上看到,也可以通过应用我们已知的测试标准得出的结果来查看正态性(p值> = 0.05)。 例如,对于相对指标:

 data_conv = (data[data['calls'] > 0].groupby(['subbucket']).calls.sum()*1.0/data.groupby(['subbucket']).device_id.nunique()) data_conv.hist(bins = 50) 

现在,可以将t检验应用于子存储桶的平均值(因此,不是device_id,不是设备,而是作为观察值的子存储桶)。

在确定这些变化在统计上是有意义的之后,我们可以怀着明确的良心做所有我们开始做的事情-计算自噬:

 (data_exp.groupby(['subbucket']).calls.avg() - data_cntrl.groupby(['subbucket']).calls.avg() )/ data_exp.groupby(['subbucket']).calls.avg() 

分母应该是没有广告的流量,即实验性流量。

3.引导方法


bootstrap方法是sub-bucket方法的扩展,代表了它的更高级和改进的版本; 在Facebook Bootstrapped库中可以找到该方法在Python中的软件实现。
简而言之,bootstrap的概念可以描述如下:一种方法无非就是以类似于子桶方法的方式随机生成但可能重复的方式生成样本的构造方法。 我们可以说来自一般人群的安置(如果可以打电话给原始样本)。 在输出中,由每个生成的子样本的平均值形成平均值(或中位数,数量等)。

FaceBook Bootstrap库的主要方法
 bootstrap() 
-实施形成子样本的机制; 默认情况下返回下限(5%)和上限(95%); 要返回此范围内的离散分布,必须设置参数return_distribution = True (它由generate_distributions()帮助函数生成 )。

您可以使用num_iterations参数指定迭代次数,在该参数中将生成子样本,并指定每次迭代的eration_batch_size子样本数。 在generate_distributions()的输出中,将生成一个样本,其大小等于迭代次数num_iterations ,其元素将是每次迭代中计算出的eration_batch_size样本值的平均值。 在大量样本的情况下,数据可能不再适合内存,因此在这种情况下,建议减小eration_batch_size的值。

示例 :让原始样本为2,000,000; num_iterations = 10,000, iteration_batch_size =300。然后,在10,000个迭代中的每个迭代中,将在内存中存储2,000,000个项目的300个列表。

该函数还允许在多个处理器内核,多个线程上进行并行计算,并使用num_threads参数设置所需的数字。

 bootstrap_ab() 

执行与上述bootstrap()函数相同的所有操作,但是,此外,还通过stat_func中指定的方法对平均值进行汇总-从num_iterations的值开始 。 接下来,计算在compare_func参数中指定的度量,并估计统计显着性。

 compare_functions 

-一类功能,提供用于形成评估指标的工具:
 compare_functions.difference() compare_functions.percent_change() compare_functions.ratio() compare_functions.percent_difference() # difference = (test_stat - ctrl_stat) # percent_change = (test_stat - ctrl_stat) * 100.0 / ctrl_stat # ratio = test_stat / ctrl_stat # percent_difference = (test_stat - ctrl_stat) / ((test_stat + ctrl_stat) / 2.0) * 100.0 

 stats_functions 
-一类函数,可从中选择所研究度量的聚合方法:
 stats_functions.mean stats_functions.sum stats_functions.median stats_functions.std 

作为stat_func,您还可以使用自定义的用户定义函数,例如:

 def test_func(test_stat, ctrl_stat): return (test_stat - ctrl_stat)/test_stat bs.bootstrap_ab(test.values, control.values, stats_functions.mean, test_func, num_iterations=5000, alpha=0.05, iteration_batch_size=100, scale_test_by=1, num_threads=4) 

实际上, (test_stat-ctrl_stat)/ test_stat是计算自相残杀的公式。

另外,或者为了实际实验的目的,您可以首先使用bootstrap()获得分布,使用t检验检查目标指标差异的统计显着性,然后对其进行必要的处理。
使用此方法如何获得“质量”正态分布的示例:



可以在存储库页面上找到更详细的文档。

目前,这就是我想要(或设法)谈论的一切。 我试图简要但清楚地描述所使用的方法及其实现过程。 方法可能需要调整,因此,我将感谢您的反馈和评论。

我也要感谢我的同事在准备这项工作中的帮助。 如果文章获得了主要的正面反馈,我将在此指出其名字或昵称(经事先同意)。

祝福大家! :)

PS 亲爱的冠军频道 ,评估A / B测试结果的任务是数据科学中最重要的任务之一,因为在没有A / B的情况下,没有一个新的ML模型在生产中启动。 也许是时候组织一场竞赛来开发一种评估A / B测试结果的系统了吗? :)

Source: https://habr.com/ru/post/zh-CN451488/


All Articles