比拟一个变量在不同组中的散布是数据迷信中的一个常见问题。当咱们想要评估一项策略(用户体验性能、广告流动、药物等)的因果效应时,因果推断的黄金规范便是随机对照试验,也就是所谓的A /B测试。在实践中,咱们为钻研抉择一个样本,并将其随机分为对照组(control group)和实验组(treatment group)比拟两组之间的后果。随机化确保了两组之间的惟一差别,这样咱们就能够将后果差别归因于试验成果。
因为是随机的所以两组个体不会齐全的雷同(identical)。然而有时候,它们在总体体现时甚至不是“类似”的(similar)。例如,咱们可能在一个群体中有更多的男性,或者年长的人,等等。(咱们通常称这些特色为协变量或控制变量)。当这种状况产生时,就不能再确定后果的差别只是因为试验得来的。因而,随机化后,查看所有察看变量是否在组间均衡,是否没有零碎差别是十分重要的。
在这篇文章中,咱们将看到比拟两个(或更多)散布的不同办法,并评估它们差别的量级和重要性。咱们将思考两种不同的办法,可视化和统计。这两种办法能够为咱们的提供直觉和谨严的数值来疾速评估和摸索差别,然而须要留神的是,这些办法很难辨别这些差别是系统性的还是因为噪声造成的。
假如咱们须要对一组人进行试验并且曾经将他们随机分为实验组和对照组。咱们心愿它们尽可能具备可比性,以便将两组之间的任何差别仅归因于试验成果。咱们还将实验组分为不同的组,以测验不同试验办法的成果(例如,同一种药物的轻微变动)。
对于这个例子,我模仿了1000集体的数据集,咱们察看他们的一组特色。我从src导入了数据生成过程dgp_rnd_assignment()。DGP和src.utils中的一些绘图函数和库。
from src.utils import *from src.dgp import dgp_rnd_assignmentdf = dgp_rnd_assignment().generate_data()df.head()
咱们有1000集体的信息,察看他们的性别、年龄和周支出。每个人要么被调配到4个不同的实验组要么被调配到对照组。
2组数据比照-可视化
让咱们从最简略的开始:咱们想要比拟整个实验组和对照组的支出调配。咱们首先摸索可视化办法,而后是统计办法。第一种办法的长处是能够应用咱们的直觉进行判断,第二种办法的长处是应用数字判断更加的谨严。
对于大多数可视化,这里将应用Python的seaborn库。
箱线图
第一种视觉办法是箱线图。箱线图是汇总统计和数据可视化之间的良好折衷。框的核心代表中位数,而边框别离代表第1(Q1)和第3四分位数(Q3)。扩大线延长到框外超过四分位距 (Q3 - Q1) 1.5 倍的第一个数据点。落在扩大线之外的点是独自绘制的,通常会被认为是异样值。
因而,箱线图提供了汇总统计数据(方框和扩大线)和间接数据可视化(异样值)。
sns.boxplot(data=df, x='Group', y='Income');plt.title("Boxplot");
实验组的支出调配更加扩散:橙色盒子更大,它的扩大线覆盖范围更广。然而箱线图的问题是它暗藏了数据的形态,它通知咱们一些汇总的统计数据,但没有显示理论的数据分布。
直方图
绘制分布图最直观的办法是直方图。直方图将数据分组到等同宽的容器(bin)中,并绘制出每个容器中的察看数据的数量。
sns.histplot(data=df, x='Income', hue='Group', bins=50);plt.title("Histogram");
直方图也存在一些问题
- 因为两组的察看次数不同,因而两个直方图不具备可比性
- bin的数量是任意的
咱们能够应用 stat 选项来绘制密度而不是计数来解决第一个问题,并将 common_norm 设置为 False 别离对每个直方图进行归一化。
sns.histplot(data=df, x='Income', hue='Group', bins=50, stat='density', common_norm=False);plt.title("Density Histogram");
这样这两个直方图就具备可比性!
然而一个重要的问题依然存在:bin的大小是任意的。在极其非凡的状况下,如果咱们将数据更少分组,最终会失去最多只有一个察看值的 bin,如果咱们将数据分组更多,咱们最终会只失去一个 bin。在这两种状况下,咱们都无奈判断。这是一个经典的偏差-方差衡量的问题。
核密度
一种可能的解决方案是应用核密度函数,该函数尝试应用核密度估计 (KDE) 用连续函数迫近直方图。
sns.kdeplot(x='Income', data=df, hue='Group', common_norm=False);plt.title("Kernel Density Function");
从图中能够看到,支出核密度仿佛在实验组中具备更高的方差,然而各组的平均值却是类似的。核密度估计的问题在于它有点像一个黑匣子,可能会覆盖数据的相干特色。
累积散布
两种散布更通明的示意是它们的累积散布函数(Cumulative Distribution Function)。在 x 轴(支出)的每个点,咱们绘制具备相等或更低值的数据点的百分比。累积散布函数的次要长处是
- 不须要做出任何的抉择(例如bin的数量)
- 不须要执行任何近似(例如应用 KDE),图形代表所有数据点
sns.histplot(x='Income', data=df, hue='Group', bins=len(df), stat="density", element="step", fill=False, cumulative=True, common_norm=False);plt.title("Cumulative distribution function");
咱们应该如何解释这张图?
- 因为这两条线在0.5 (y轴)处或多或少穿插,这意味着它们的中值是类似的
- 因为橙色线在右边的蓝线之上,在左边的蓝线之下,这意味着实验组的散布是fatter tails(肥尾)
QQ图
一种相干的办法是 QQ 图,其中 q 代表分位数。QQ 图绘制了两个散布的分位数。如果散布雷同应该失去一条 45 度线。
Python 中没有原生的 QQ 图性能,而 statsmodels 包提供了 qqplot 性能,但相当麻烦。因而,咱们将手动实现。
首先,咱们须要应用 percentile 函数计算两组的四分位数。
income = df['Income'].valuesincome_t = df.loc[df.Group=='treatment', 'Income'].valuesincome_c = df.loc[df.Group=='control', 'Income'].valuesdf_pct = pd.DataFrame()df_pct['q_treatment'] = np.percentile(income_t, range(100))df_pct['q_control'] = np.percentile(income_c, range(100))
当初咱们能够绘制两个分位数散布,加上 45 度线,代表基准完满拟合。
plt.figure(figsize=(8, 8))plt.scatter(x='q_control', y='q_treatment', data=df_pct, label='Actual fit');sns.lineplot(x='q_control', y='q_control', data=df_pct, color='r', label='Line of perfect fit');plt.xlabel('Quantile of income, control group')plt.ylabel('Quantile of income, treatment group')plt.legend()plt.title("QQ plot");
QQ 图在累积分布图方面提供了十分类似的信息:实验组的支出具备雷同的中位数(线在核心穿插)但尾部更宽(点在右边的线以下,左边的线以上)。
2组数据比照-统计学办法
到目前为止,咱们曾经看到了不同的办法来可视化散布之间的差别。可视化的次要长处是直观:咱们能够察看差别并直观地评估它们。
然而,咱们可能想要更加严格,并尝试评估散布之间差别的统计显着性,即 答复“察看到的差别是系统性的还是因为采样噪声?”的问题。
咱们当初将剖析不同的测验办法以辨别两个散布。
T测验
第一个也是最常见的是学生 t 测验。T 测验通常用于比拟均值。咱们要测验两组的支出调配均值是否雷同。两均值比拟测验的测验统计量由下式给出:
其中 x 是样本均值,s 是样本标准差。在较温和的条件下,测验统计量作为学生 t 散布渐近散布。
咱们应用 scipy 中的 ttest_ind 函数来执行 t 测验。该函数返回测验统计量和隐含的 p 值。
from scipy.stats import ttest_indstat, p_value = ttest_ind(income_c, income_t)print(f"t-test: statistic={stat:.4f}, p-value={p_value:.4f}")t-test: statistic=-1.5549, p-value=0.1203
测验的p值为0.12,因而咱们不回绝实验组和对照组平均值无差别的零假如。
标准化平均差 (SMD)
一般来说,当咱们进行随机对照试验或 A/B 测试时,最好对实验组和对照组中所有变量的均值差别进行测验。
然而,因为 t 测验统计量的分母取决于样本量,因而 t 测验的 p 值难以跨钻研进行比拟。所以咱们可能在一个差别十分小但样本量很大的试验中取得显着的后果,而在差别很大但样本量小的试验中咱们可能会取得不显着的后果。
解决这个问题的一种解决方案是标准化平均差 (SMD)。顾名思义,这不是一个适当的统计量,而只是一个标准化的差别,能够计算为:
通常,低于0.1的值被认为是一个“小”的差别。
最将实验组和对照组的所有变量的平均值以及两者之间的间隔度量(t 测验或 SMD)收集到一个称为平衡表的表中。能够应用causalml库中的create_table_one函数来生成它。正如该函数的名称所显示的那样,在执行A/B测试时,平衡表应该是你心愿看到的的第一个表。
from causalml.match import create_table_onedf['treatment'] = df['Group']=='treatment'create_table_one(df, 'treatment', ['Gender', 'Age', 'Income'])
在前两列中,咱们能够看到实验组和对照组的不同变量的平均值,括号中是标准误差。在最初一列中,SMD 的值示意所有变量的标准化差别均大于 0.1,这表明两组可能不同。
Mann–Whitney U测验
另一种测验是 Mann-Whitney U 测验,它比拟两个散布的中位数。该测验的原假如是两组具备雷同的散布,而备择假如是一组比另一组具备更大(或更小)的值。
与下面咱们看到的其余测验不同,Mann-Whitney U 测验对异样值不可知的。
测验过程如下。
- 合并所有数据点并对它们进行排名(按升序或降序排列)
- 计算 U = R - n(n + 1)/2,其中 R 是第一组数据点的秩和,n 是第一组数据点的数量。
- 相似地计算第二组的 U。
- 测验统计量由 stat = min(U, U) 给出。
在两个散布之间没有零碎等级差别的原假如下(即雷同的中位数),测验统计量是渐近正态分布的,具备已知的均值和方差。
计算 R 和 U 背地的实践如下:如果第一个样本中的值都大于第二个样本中的值,则 R = n(n + 1)/2 并且作为后果,U 1 将为零(可达到的最小值)。否则如果两个样本类似,U1 和 U2 将十分靠近 n1 n2 / 2(可达到的最大值)。
咱们应用 scipy 的 mannwhitneyu 函数。
from scipy.stats import mannwhitneyustat, p_value = mannwhitneyu(income_t, income_c)print(f" Mann–Whitney U Test: statistic={stat:.4f}, p-value={p_value:.4f}")Mann–Whitney U Test: statistic=106371.5000, p-value=0.6012
咱们失去的p值为0.6,这意味着咱们不回绝实验组和对照组的中位数没有差别的零假如。
置换测验
一种非参数代替办法是置换测验。在原假如下,两个散布应该是雷同的,因而打乱组标签不应该显着扭转任何统计数据。
能够抉择任何统计数据并查看其在原始样本中的值如何与其在组标签排列中的散布进行比拟。例如应用实验组和对照组之间样本均值的差别作为测验统计。
sample_stat = np.mean(income_t) - np.mean(income_c)stats = np.zeros(1000)for k in range(1000): labels = np.random.permutation((df['Group'] == 'treatment').values) stats[k] = np.mean(income[labels]) - np.mean(income[labels==False])p_value = np.mean(stats > sample_stat)print(f"Permutation test: p-value={p_value:.4f}")Permutation test: p-value=0.0530
置换测验失去的 p 值为 0.053,这意味着在 5% 的程度上对原假如的弱不回绝。那么应该如何解释 p 值?这意味着数据中均值的差别大于置换样本中均值差别的 1–0.0560 = 94.4%。
咱们能够通过绘制测验统计在排列中的散布与其样本值的散布来可视化。
plt.hist(stats, label='Permutation Statistics', bins=30);plt.axvline(x=sample_stat, c='r', ls='--', label='Sample Statistic');plt.legend();plt.xlabel('Income difference between treatment and control group')plt.title('Permutation Test');
正如咱们所看到的,样本统计量绝对于置换样本中的值是相当极其的,但并不过分。
卡方测验
卡方测验是一种十分弱小的测验,次要用于测验频率差别。
卡方测验最鲜为人知的利用之一是测验两个散布之间的相似性。这个想法是对两组的察看后果进行分类。如果两个散布雷同,咱们会冀望每个 bin 中的察看频率雷同。这里重要的一点是须要在每个 bin 中进行足够的察看,以使测验无效。
生成与对照组中支出散布的非常位数绝对应的bin,而后如果两个散布雷同,我计算实验组中每个bin中的预期察看数。
# Init dataframedf_bins = pd.DataFrame()# Generate bins from control group_, bins = pd.qcut(income_c, q=10, retbins=True)df_bins['bin'] = pd.cut(income_c, bins=bins).value_counts().index# Apply bins to both groupsdf_bins['income_c_observed'] = pd.cut(income_c, bins=bins).value_counts().valuesdf_bins['income_t_observed'] = pd.cut(income_t, bins=bins).value_counts().values# Compute expected frequency in the treatment groupdf_bins['income_t_expected'] = df_bins['income_c_observed'] / np.sum(df_bins['income_c_observed']) * np.sum(df_bins['income_t_observed'])df_bins
当初能够通过比拟解决组中跨bin的预期 (E) 和察看 (O) 察看数来执行测验。测验统计量由下式给出
其中 bin 由 i 索引,O 是 bin i 中察看到的数据点数,E 是 bin i 中的预期的数据点数。因为咱们应用对照组中支出散布的非常位数生成了 bin,因而咱们预计解决组中每个 bin 的察看数在各个 bin 之间是雷同的。测验统计量渐近散布为卡方散布。
为了计算测验统计量和测验的 p 值,咱们应用 scipy 的卡方函数。
from scipy.stats import chisquarestat, p_value = chisquare(df_bins['income_t_observed'], df_bins['income_t_expected'])print(f"Chi-squared Test: statistic={stat:.4f}, p-value={p_value:.4f}")Chi-squared Test: statistic=32.1432, p-value=0.0002
与下面介绍的所有其余测验不同,卡方测验强烈回绝两个散布雷同的原假如。这是为什么?
起因在于这两个散布具备类似的核心但尾部不同,并且卡方测验测试了整个散布的相似性,而不仅仅是核心,就像咱们在之前的测验中所做的那样。
这个后果讲述了一个警示:在从 p 值得出自觉论断之前,理解理论测验的内容十分重要!
Kolmogorov-Smirnov 测验
Kolmogorov-Smirnov测验的思维是比拟两组的累积散布。特地是,Kolmogorov-Smirnov 测验统计量是两个累积散布之间的最大相对差。
其中 F 和 F 是两个累积散布函数,x 是根底变量的值。Kolmogorov-Smirnov 测验统计量的渐近散布是 Kolmogorov 散布。
为了更好地了解,让咱们绘制累积散布函数和测验统计量。首先计算累积散布函数。
df_ks = pd.DataFrame()df_ks['Income'] = np.sort(df['Income'].unique())df_ks['F_control'] = df_ks['Income'].apply(lambda x: np.mean(income_c<=x))df_ks['F_treatment'] = df_ks['Income'].apply(lambda x: np.mean(income_t<=x))df_ks.head()
当初须要找到累积散布函数之间的相对间隔最大的点。
k = np.argmax( np.abs(df_ks['F_control'] - df_ks['F_treatment']))ks_stat = np.abs(df_ks['F_treatment'][k] - df_ks['F_control'][k])
能够通过绘制两个累积散布函数和测验统计量的值来可视化测验统计量的值。
y = (df_ks['F_treatment'][k] + df_ks['F_control'][k])/2plt.plot('Income', 'F_control', data=df_ks, label='Control')plt.plot('Income', 'F_treatment', data=df_ks, label='Treatment')plt.errorbar(x=df_ks['Income'][k], y=y, yerr=ks_stat/2, color='k', capsize=5, mew=3, label=f"Test statistic: {ks_stat:.4f}")plt.legend(loc='center right');plt.title("Kolmogorov-Smirnov Test");
从图中咱们能够看出,测验统计量的值对应于支出~650 时的两个累积散布之间的间隔。对于该支出值在两组之间存在最大的不均衡。
咱们能够应用 scipy 中的 kstest 函数执行实测验。
from scipy.stats import ksteststat, p_value = kstest(income_t, income_c)print(f" Kolmogorov-Smirnov Test: statistic={stat:.4f}, p-value={p_value:.4f}")Kolmogorov-Smirnov Test: statistic=0.0974, p-value=0.0355
p 值低于 5%:咱们以 95% 的置信度回绝两个散布雷同的原假如。
多组数据比照-可视化
到目前为止,咱们只思考了两组的状况,然而如果咱们有多个组呢?咱们在下面看到的一些办法能够很好地扩大,而另一些则不能。
作为一个示例,咱们当初将查看不同实验组的支出调配是否雷同。
箱线图
当咱们有多组时,箱线图能够很好地扩大,因为咱们能够并排搁置不同的框。
sns.boxplot(x='Arm', y='Income', data=df.sort_values('Arm'));plt.title("Boxplot, multiple groups");
从图中能够看出,不同实验组的支出调配不同,编号越高的组平均收入越高。
提琴图
联合汇总统计和核密度估计的箱线图的一个十分好的扩大是小提琴图。小提琴图沿 y 轴显示不同的密度,因而它们不会重叠。默认状况下,它还在外面增加了一个微型箱线图。
sns.violinplot(x='Arm', y='Income', data=df.sort_values('Arm'));plt.title("Violin Plot, multiple groups");
小提琴图表明不同实验组的支出不同。
山脊图
山脊图沿 x 轴绘制了多个核密度散布,它比小提琴图更直观。在 matplotlib 和 seaborn 中都没有默认的山脊线图。素以须要joypy包。
from joypy import joyplotjoyplot(df, by='Arm', column='Income', colormap=sns.color_palette("crest", as_cmap=True));plt.xlabel('Income');plt.title("Ridgeline Plot, multiple groups");
山脊图表明,编号越高的实验组支出越高。从这个图中也更容易了解散布的不同形态。
多组数据比照-统计学办法
最初,让咱们思考比拟多个组的假设检验。为了简略起见,咱们将集中探讨最罕用的一个:f测验。
F测验
对于多个组最风行的测验办法是 F 测验。F 测验比拟不同组间变量的方差。这种剖析也称为方差分析。
F 测验统计量由下式给出
其中 G 是组数,N 是察看数,x 是总体平均值,xg 是组 g 内的平均值。在组独立性的原假如下,f 统计量是 F 散布的。
from scipy.stats import f_onewayincome_groups = [df.loc[df['Arm']==arm, 'Income'].values for arm in df['Arm'].dropna().unique()]stat, p_value = f_oneway(*income_groups)print(f"F Test: statistic={stat:.4f}, p-value={p_value:.4f}")F Test: statistic=9.0911, p-value=0.0000
测验 p 值基本上为零,这意味着强烈回绝实验组之间支出调配没有差别的原假如。
总结
在这篇文章中,咱们看到了很多不同的办法来比拟两个或多个散布,无论是在可视化上还是在统计上。这是许多应用程序中的次要问题,尤其是在因果推断中,咱们须要使随机化使实验组和对照组尽可能具备可比性。
咱们还看到了不同的办法如何实用于不同的状况。视觉办法十分直观,但统计办法对于决策至关重要,因为咱们须要可能评估差别的幅度和统计意义。
本文代码:
https://avoid.overfit.cn/post/883ffe04b1e340c8a5fd2a7ea6364cbf
作者:Matteo Courthoud