DATA NOTEBOOK阅读地图
9个主要章节
1段代码示例
5组图片与图表

先确认变量、单位与假设,再核对手算、图表和解释范围。

章节速览9 节 · 展开选择

一列数据的波动可以用方差描述;两列数据是否一起偏高、一起偏低,则需要保留它们的配对关系。把延迟与请求大小分别统计后再拼接均值,并不能恢复它们共同变化的信息。

这篇接续Welford在线方差,把状态扩展到两个变量。目标是:每次接收一对记录,用固定数量的状态维护两个均值、两个中心平方和及一个中心积,并能合并互不重复的分块。 本文只讨论等权、有限实数、成对完整观测的累计统计;所有算例均为合成数据。

先算清楚相同中心,才能讨论共同波动

先核对每对记录的身份、单位和两个均值,再区分中心积、协方差与相关系数。

再合并块内关系之外,还有块间差异

用四对记录看见:两个块的相关系数都为−1,合起来却可以是0.6。

先定义配对单位与统计口径

设记录为(xᵢ,yᵢ),i=1,…,n。两个均值分别记作μₓ、μᵧ。维护Sₓₓ=Σ(xᵢ−μₓ)²、Sᵧᵧ=Σ(yᵢ−μᵧ)²,以及中心积C=Σ(xᵢ−μₓ)(yᵢ−μᵧ)。C会带符号,不能像平方和那样要求非负。

如果x是请求大小KB,y是延迟ms,C与协方差的单位都是KB·ms;Sₓₓ与Sᵧᵧ的单位分别为KB²和ms²。相关系数把这两种尺度除掉,因此没有单位。

公式1|中心积、协方差与Pearson相关系数
描述协方差:covₙ = C / n,n ≥ 1
样本协方差:sₓᵧ = C / (n − 1),n ≥ 2
r = C / (√Sₓₓ · √Sᵧᵧ),n ≥ 2 且 Sₓₓ、Sᵧᵧ > 0

共同使用n或n−1作方差/协方差分母时,相关系数中的分母会约去。样本协方差的无偏性质还需要独立同分布、有限二阶矩等抽样条件;除以n−1不会修复选择偏差。接口口径参见文献1。

n、μₓ、μᵧ
有效配对数量和两列均值。两列必须来自同一批配对,不能分别丢弃缺失后按剩余位置拼接。
Sₓₓ、Sᵧᵧ
各列围绕当前均值的平方和。任一列恒定时,相应平方和为0,相关系数未定义。
C
围绕同一批记录各自均值的中心积之和。正负偏差的乘积决定贡献方向,单独的x·y大小不等于贡献。

在精确算术下,柯西—施瓦茨不等式给出C²≤SₓₓSᵧᵧ,所以r落在[−1,1]。单条记录的描述协方差为0,但相关系数仍未定义:没有波动可以用来归一化。报告“r=0”会把未定义误写成没有线性关系。

配对身份比公式先行。例如先按大小排序x,再按延迟排序y,会制造原本不存在的一一对应。异步传感器必须先定义时间对齐与容忍范围;这个统计器不会替你判断两条读数是否应该配成一对。

为什么不直接累计xy后相减

恒等式C=Σxᵢyᵢ−nμₓμᵧ在精确算术下成立。但两个变量都有很大的基数、共同波动很小时,右侧是两个大数相减。输入转换和累计乘法的误差可能已经大于最终差值。

例如把正文四对小整数同时平移到10¹²附近,真正的C仍为3。中心化更新把计算放在相对均值的增量上,避免显式累加约10²⁴量级的乘积再抵消;它仍使用有限精度浮点数,不能保证任意范围或顺序下逐位相同。若整数转换成float时已丢掉低位,后续算法也无法恢复。

从均值移动推导一次更新

已有n≥1对记录,新来(x,y)。令δₓ=x−μₓ、δᵧ=y−μᵧ,新的数量n′=n+1,均值分别增加δₓ/n′和δᵧ/n′。

对旧记录,新的中心积展开为[(xᵢ−μₓ)−δₓ/n′][(yᵢ−μᵧ)−δᵧ/n′]。两组旧偏差之和都为0,因此交叉项消失,只留下nδₓδᵧ/n′²。新记录相对新均值的偏差分别是nδₓ/n′和nδᵧ/n′,再加入它的贡献,共增加nδₓδᵧ/n′。

公式2|用旧偏差与新均值更新中心量
n′ = n + 1;μₓ′ = μₓ + δₓ/n′;μᵧ′ = μᵧ + δᵧ/n′
Sₓₓ′ = Sₓₓ + δₓ(x − μₓ′)
Sᵧᵧ′ = Sᵧᵧ + δᵧ(y − μᵧ′)
C′ = C + δₓ(y − μᵧ′) = C + δₓδᵧ·n/n′

精确算术下成立;C也可对称地写成C+δᵧ(x−μₓ′)。这里的第二个偏差必须使用新均值。空状态直接建立n=1、两个均值为新观测、三个中心量为0。

四对记录的手算与图解

取(1,2)、(2,1)、(3,4)、(4,3),逐次纳入得到:

纳入到第几对 μₓ μᵧ Sₓₓ Sᵧᵧ C
1 1 2 0 0 0
2 3/2 3/2 1/2 1/2 −1/2
3 2 7/3 2 14/3 2
4 5/2 5/2 5 5 3

第二步δₓ=1、δᵧ=−1,C增加−1/2。第三步δₓ=3/2、δᵧ=5/2,增加(3/2)(5/2)×2/3=5/2,得到C=2。最后一步增加1,最终描述协方差为3/4,样本协方差为1,r=3/5=0.6。

四对观测围绕均值2.5与2.5,中心积贡献分别为0.75、0.75、0.75、0.75,总和C为3,两个中心平方和均为5
图1|原创合成散点与中心积核算。四个贡献均相对最终均值计算;不是四次在线增量,也不是因果效应。查看原图
自检1:把y的顺序倒过来,协方差还必须保持吗?

不必须。倒序破坏了原始配对。两列各自的均值与方差都不变,但这组数据的C从3变为−3,相关系数从0.6变为−0.6;说明单列摘要不足以决定成对关系。

分块合并时,为什么不能平均相关系数

设两个非空块A、B,数量分别n_A、n_B,均值差Δₓ=μₓ,B−μₓ,A、Δᵧ=μᵧ,B−μᵧ,A。总均值为数量加权均值。将每块的观测偏差拆成“相对块均值”和“块均值相对总均值”,块内偏差和为0,留下块内中心积与块间修正:

公式3|互不重复分块的中心量合并
n = n_A + n_B;w = n_A·n_B/n
μₓ = μₓ,A + Δₓ·n_B/n;μᵧ = μᵧ,A + Δᵧ·n_B/n
Sₓₓ = Sₓₓ,A + Sₓₓ,B + Δₓ²w
Sᵧᵧ = Sᵧᵧ,A + Sᵧᵧ,B + Δᵧ²w
C = C_A + C_B + ΔₓΔᵧw

任一块为空时采用另一块。ΔₓΔᵧw的单位与C一致;其符号由两个块均值移动的方向决定。分块背景参见文献2;这里从中心化展开独立推导。

A取前两对,B取后两对。两个块的Sₓₓ、Sᵧᵧ都为1/2,C都为−1/2,所以各自r=−1。块均值却从(1.5,1.5)共同移动到(3.5,3.5),Δₓ=Δᵧ=2、w=1,跨块贡献为4。合并C=−1/2−1/2+4=3,相关系数变成0.6。

A块与B块的中心积均为负0.5,均值差两个分量均为2,跨块修正为4,合并中心积为3,相关系数0.6
图2|原创分块账本。块内相关与总体相关回答不同范围的问题;不要把相关系数的平均值当作总体相关。查看原图

这不是公式相互矛盾,而是总体同时包含块内和块间变化。若分块对应不同人群或时间段,要同时保留分组结果并解释组合目的;可接着读辛普森悖论与标准化。相关符号反转并不自动说明哪一种分组代表因果机制。

代码1:标准库不可变累计状态

代码1|Python 3.11及以上标准库:保存为online_cov.py运行。仅使用默认空状态,再通过add和merge生成状态;不支持手工伪造内部字段、权重、缺失补值或删点。每次add返回新对象,调用时必须接回返回值。转换为float后计算,None表示样本不足或常量列导致未定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
from dataclasses import dataclass
import math


def finite_number(value):
if type(value) not in (int, float):
raise TypeError("expected int or float, excluding bool")
try:
value = float(value)
except OverflowError as exc:
raise ValueError("input outside float range") from exc
if not math.isfinite(value):
raise ValueError("expected finite input")
return value


@dataclass(frozen=True)
class OnlineCov:
n: int = 0
mx: float = 0.0
my: float = 0.0
xx: float = 0.0
yy: float = 0.0
c: float = 0.0

@staticmethod
def _new(n, mx, my, xx, yy, c):
if not all(math.isfinite(v) for v in (mx, my, xx, yy, c)):
raise ArithmeticError("state outside float range")
if xx < 0 or yy < 0:
raise ArithmeticError("negative centered square sum")
return OnlineCov(n, mx, my, xx, yy, c)

def add(self, x, y):
x, y = finite_number(x), finite_number(y)
if self.n == 0:
return self._new(1, x, y, 0.0, 0.0, 0.0)
n = self.n + 1
dx, dy = x - self.mx, y - self.my
mx, my = self.mx + dx / n, self.my + dy / n
return self._new(n, mx, my,
self.xx + dx * (x - mx),
self.yy + dy * (y - my),
self.c + dx * (y - my))

def merge(self, other):
if not isinstance(other, OnlineCov):
raise TypeError("expected OnlineCov")
if self.n == 0:
return other
if other.n == 0:
return self
n = self.n + other.n
ratio = other.n / n
weight = self.n * ratio
dx, dy = other.mx - self.mx, other.my - self.my
return self._new(n, self.mx + dx * ratio,
self.my + dy * ratio,
self.xx + other.xx + dx * dx * weight,
self.yy + other.yy + dy * dy * weight,
self.c + other.c + dx * dy * weight)

def covariance(self, sample=True):
denominator = self.n - 1 if sample else self.n
return self.c / denominator if denominator > 0 else None

def correlation(self):
if self.n < 2 or self.xx == 0 or self.yy == 0:
return None
r = (self.c / math.sqrt(self.xx)) / math.sqrt(self.yy)
if not math.isfinite(r) or abs(r) > 1 + 1e-12:
raise ArithmeticError("correlation outside roundoff tolerance")
return max(-1.0, min(1.0, r))


if __name__ == "__main__":
pairs = [(1, 2), (2, 1), (3, 4), (4, 3)]
state = OnlineCov()
for x, y in pairs:
state = state.add(x, y)
print(state.n, state.mx, state.my)
print(state.covariance(False), state.covariance(), round(state.correlation(), 12))

预期两行输出为4 2.5 2.5和0.75 1.0 0.6。入口仅对显示的r舍入到12位小数;内部计算与验证仍使用原始浮点结果。单条更新与两块合并都是O(1)时间、O(1)额外状态;读取n条数据总计O(n),返回新对象的旧状态若被外部一直保存,外部历史列表仍会占O(n)空间。

相关系数顺次除以两个平方根,避免先计算xx·yy而发生不必要的乘积溢出。最后的截断只容忍约10⁻¹²的越界,不是修复任意错误协方差的办法。极大跨度、极小变化、输入下溢或长期累计仍可能超出本实现能力;严格精度需求应重新设计缩放、数据表示和误差预算。

缺失、常量与非线性关系的边界

自检2:一列恒定时,相关系数是不是0?

不是。恒定列的中心平方和为0,归一化分母为0,代码返回None。协方差可以为0,但不能据此写出一个已定义的相关系数。

自检3:r=0是否证明两个变量互不相关、可以独立处理?

这里只说明当前样本的线性中心积为0。例如x取−2、−1、0、1、2,y=x²,中心积为0,但y完全由x决定。统计独立比零线性相关强得多,也不能从一个样本值直接推出总体结论。

自检4:把同一块合并两次,能靠状态发现吗?

不能。摘要没有分块身份,也不知道记录范围。重复合并会重复计数;生产管道需要额外的分块标识、幂等提交或去重设计。不可变对象保证一次操作不留下半截状态,不保证恰好处理一次。

如果丢弃缺失,必须按整对记录执行;只丢x中的缺失再与原y压缩配对,会改变问题。扩展到多列时,逐列两两删除缺失还可能让各协方差项来自不同样本集合,不能直接当作同一批完整记录的协方差矩阵。本文的拒绝策略宁可明确报错,也不隐式改变统计人群。

x改用另一正比例单位,协方差随尺度改变,而r不变;若只把一列乘以负数,r改变符号。对x、y分别平移,精确算术下中心量不变。这些是核对程序的性质,不是对实际测量误差或因果关系的保证。

如何复核实现与继续阅读

核对不能只拿两个复用相同递推式的函数比较。本文验证程序从正文提取原代码,实际运行入口,再用高精度两遍中心化计算独立参考,覆盖随机配对、不同分块、置换、平移/缩放、常量、空块、逆序配对以及非法输入;精度按输入范围评估,不把高精度参考的位数说成浮点实现精度。

本篇解决的是摘要怎样取得。继续阅读PCA理解多个特征的投影,或位置—速度卡尔曼观察交叉协方差怎样参与状态修正;相关分析的报告还应说明样本来源、时间范围、缺失处理和分组依据。

参考文献与对应位置

  1. Python 3.11官方文档:statistics.covariance与statistics.correlation。 协方差、相关系数。用于核对样本协方差口径、等长要求与常量列边界;本文None返回约定不同于标准库抛异常。返回公式1。
  2. Philippe Pébay. Formulas for Robust, One-Pass Parallel Computation of Covariances and Arbitrary-Order Statistical Moments. SAND2008-6212, 2008. DOI:10.2172/1028931. Sandia官方摘要与报告元信息。核对增量与并行协方差的研究范围;未声称取得当前访问失败的全文,本篇代数推导独立展开。返回公式3。