安斯库姆四重奏:可视化的力量与统计错觉
引言:数值的欺骗性
在数据科学和统计学领域,初学者往往容易陷入一种误区:过度迷信数值指标(Metrics),而忽视数据的分布形态。
我们习惯于拿到数据后立即计算均值、方差、相关系数,甚至直接套用回归模型。毕竟,”数值计算是精确的,图表用处不大”(Numerical calculations are exact, but graphs are rough)曾是早期统计学界的一种流行观点。
为了反驳这一观点,1973年,统计学家弗朗西斯·安斯库姆(Francis Anscombe)构造了四组神奇的数据集,即著名的安斯库姆四重奏(Anscombe’s Quartet)。这四组数据在统计特性上几乎完全一致,但在图形展示上却有着天壤之别。
它告诉我们一个深刻的道理:在进行任何复杂的统计推断之前,进行可视化的探索性数据分析(EDA)不仅是辅助手段,更是保证推断有效性的必要前提。
安斯库姆四重奏数据集
让我们先来看看这四组数据。它们由四对 $(x, y)$ 变量组成:
| 观测值 | $x_1$ | $y_1$ | $x_2$ | $y_2$ | $x_3$ | $y_3$ | $x_4$ | $y_4$ |
|---|---|---|---|---|---|---|---|---|
| 1 | 10.0 | 8.04 | 10.0 | 9.14 | 10.0 | 7.46 | 8.0 | 6.58 |
| 2 | 8.0 | 6.95 | 8.0 | 8.14 | 8.0 | 6.77 | 8.0 | 5.76 |
| 3 | 13.0 | 7.58 | 13.0 | 8.74 | 13.0 | 12.74 | 8.0 | 7.71 |
| 4 | 9.0 | 8.81 | 9.0 | 8.77 | 9.0 | 7.11 | 8.0 | 8.84 |
| 5 | 11.0 | 8.33 | 11.0 | 9.26 | 11.0 | 7.81 | 8.0 | 8.47 |
| 6 | 14.0 | 9.96 | 14.0 | 8.10 | 14.0 | 8.84 | 8.0 | 7.04 |
| 7 | 6.0 | 7.24 | 6.0 | 6.13 | 6.0 | 6.08 | 8.0 | 5.25 |
| 8 | 4.0 | 4.26 | 4.0 | 3.10 | 4.0 | 5.39 | 19.0 | 12.50 |
| 9 | 12.0 | 10.84 | 12.0 | 9.13 | 12.0 | 8.15 | 8.0 | 5.56 |
| 10 | 7.0 | 4.82 | 7.0 | 7.26 | 7.0 | 6.42 | 8.0 | 7.91 |
| 11 | 5.0 | 5.68 | 5.0 | 4.74 | 5.0 | 5.73 | 8.0 | 6.89 |
乍看之下,这仅仅是一堆杂乱的数字。但当我们开始计算统计量时,”魔术”发生了。
统计陷阱:完美的伪装
如果我们不画图,仅仅依赖常用的描述性统计量和线性回归模型来分析这四组数据,我们会得到惊人一致的结果。
1. 描述性统计量
对四组数据分别计算均值和方差,结果如下(精确到小数点后2-3位):
- 均值 (Mean):
- $E(x) = 9.00$ (四组完全相同)
- $E(y) = 7.50$ (四组完全相同)
- 样本方差 (Variance):
- $Var(x) = 11.00$ (四组完全相同)
- $Var(y) \approx 4.12$ (四组基本相同)
- 相关系数 (Correlation):
- $Corr(x, y) \approx 0.816$ (四组完全相同)
2. 回归分析
如果我们假设 $y$ 和 $x$ 之间存在线性关系,并使用最小二乘法(OLS)拟合线性回归模型 $y = \beta_0 + \beta_1 x + \epsilon$,我们会发现四组模型的参数惊人地一致:
- 截距 ($\beta_0$):约 3.00
- 斜率 ($\beta_1$):约 0.50
- 拟合优度 ($R^2$):约 0.67
结论:如果我们只看上面的数字,我们会自信地得出结论——这四组数据反映了同样的规律,$x$ 和 $y$ 之间存在显著的正相关线性关系,且模型拟合程度尚可。
然而,这个结论是大错特错的。
可视化的揭示:图表不会撒谎
当我们把这四组数据绘制成散点图(Scatter Plot)时,真相才浮出水面。
让我们逐一审视这四张图:
- Dataset I(左上): 这是我们理想中的线性关系。数据点均匀地分布在回归线两侧,随机误差看起来服从正态分布。对于这组数据,线性回归模型是恰当的。
- Dataset II(右上): 这是一个明显的非线性关系(看起来像倒置的抛物线)。数据之间有着极强的确定性关系,但强行使用线性模型(直线)去拟合曲线,导致我们完全误解了变量间的结构。$R^2=0.67$ 在这里毫无意义。
- Dataset III(左下): 这是一个强影响点(Outlier) 的典型案例。绝大多数数据点呈现极其完美的线性关系(相关系数几乎为1),但仅仅因为一个离群值的存在,拉低了整条回归线的斜率,降低了相关系数。如果没有可视化,我们无法发现这个异常点,从而无法剔除或单独分析它。
- Dataset IV(右下): 这是一个高杠杆点(High Leverage Point) 的极端案例。$x$ 值除了一个点外全部相同($x=8$)。线性关系的建立完全依赖于那一个极端的观测值。如果去掉这个点,$x$ 和 $y$ 之间根本不存在线性关系(因为 $x$ 是常数)。这说明模型非常脆弱,完全被单个数据点”绑架”了。
深入探讨:为什么可视化对统计推断至关重要?
安斯库姆四重奏不仅是一个有趣的数学游戏,它揭示了统计推断中潜在的巨大风险。
1. 验证模型假设 (Assumptions Checking)
经典的统计模型(如线性回归)通常建立在一系列严格的假设之上,例如:
- 线性性 (Linearity):自变量和因变量之间是线性关系。
- 同方差性 (Homoscedasticity):误差项的方差是恒定的。
- 正态性 (Normality):误差项服从正态分布。
- 独立性 (Independence):样本之间相互独立。
数值指标无法直接告诉你假设是否成立。例如在 Dataset II 中,数值告诉我们有线性相关性,但图形一眼就否定了”线性性”假设。只有通过残差图(Residual Plot) 等可视化手段,我们才能诊断模型是否适用。
2. 识别离群值与强影响点
如 Dataset III 和 IV 所示,单个离群值足以摧毁整个模型的统计特性。均值和方差对离群值非常敏感(非鲁棒统计量)。在盲目计算之前,通过箱线图(Boxplot) 或散点图识别并处理异常值,是保证推断稳健性的关键步骤。
3. “数据形态”优于”数据指标”
现代数据分析不仅关注中心趋势(均值),更关注数据的分布形态。偏度(Skewness)和峰度(Kurtosis)虽然能描述分布,但依然不如直方图(Histogram) 或 核密度估计(KDE) 直观。
结语
弗朗西斯·安斯库姆在 50 年前的告诫在今天的大数据时代依然震耳欲聋。随着自动化机器学习(AutoML)和黑盒模型的流行,我们越来越容易忽略对原始数据的观察。
不要相信没有图表的统计结果。 (Never trust summary statistics alone.)
在按下 “Run Model” 按钮之前,请先画个图。这不仅是良好的工作习惯,更是对数据真实性的尊重。
附录:Python 代码实现
如果你想亲自重现这四组神奇的数据和图表,可以使用以下 Python 代码:
import matplotlib.pyplot as plt
import numpy as np
# Anscombe's Quartet Data
x = [10, 8, 13, 9, 11, 14, 6, 4, 12, 7, 5]
y1 = [8.04, 6.95, 7.58, 8.81, 8.33, 9.96, 7.24, 4.26, 10.84, 4.82, 5.68]
y2 = [9.14, 8.14, 8.74, 8.77, 9.26, 8.10, 6.13, 3.10, 9.13, 7.26, 4.74]
y3 = [7.46, 6.77, 12.74, 7.11, 7.81, 8.84, 6.08, 5.39, 8.15, 6.42, 5.73]
x4 = [8, 8, 8, 8, 8, 8, 8, 19, 8, 8, 8]
y4 = [6.58, 5.76, 7.71, 8.84, 8.47, 7.04, 5.25, 12.50, 5.56, 7.91, 6.89]
datasets = [(x, y1), (x, y2), (x, y3), (x4, y4)]
fig, axes = plt.subplots(2, 2, figsize=(10, 8), sharex=True, sharey=True)
axes = axes.flatten()
for i, (xi, yi) in enumerate(datasets):
ax = axes[i]
# 绘制散点
ax.scatter(xi, yi, color='orange', edgecolor='k', s=60)
# 绘制回归线
m, b = np.polyfit(xi, yi, 1)
ax.plot(np.array([4, 19]), m * np.array([4, 19]) + b, color='blue', alpha=0.6)
ax.set_title(f'Dataset {i+1}')
ax.set_xlim(3, 20)
ax.set_ylim(3, 13)
plt.tight_layout()
plt.show()