如何解决使用pandas滚动相关性时出现的不一致结果?

9

首先声明,为了重现问题,我需要大量数据,这也是问题的一部分,因为我无法预测什么时候会出现异常。无论如何,数据太大(约13k行,2列),无法在问题中粘贴,我已在帖子末尾添加了pastebin链接


我最近几天在使用pandas.core.window.rolling.Rolling.corr时遇到了一个奇怪的问题。我有一个数据集,想要计算滚动相关性。以下是问题描述:
在计算两列(ab)的滚动相关性(窗口大小为100)时:一些索引(例如12981)会返回接近0的值(约为1e-10的数量级),但实际上应该返回naninf(因为其中一列的所有值都是常数)。然而,如果我只计算该索引的独立相关性(即包括该索引的最后100行数据),或者对较少的行进行滚动计算(如300或1000而不是13k),就能得到正确的结果(即naninf)。

期望结果:

>>> df = pd.read_csv('sample_corr_data.csv') # link at the end,  ## columns = ['a', 'b']
>>> df.a.tail(100).value_counts()

 0.000000    86
-0.000029     3
 0.000029     3
-0.000029     2
 0.000029     2
-0.000029     2
 0.000029     2
Name: a, dtype: int64

>>> df.b.tail(100).value_counts()     # all 100 values are same
 
6.0    100
Name: b, dtype: int64

>>> df.a.tail(100).corr(df.b.tail(100))
nan                                      # expected, because column 'b' has same value throughout

# Made sure of this using,
# 1. np.corrcoef, because pandas uses this internally to calculate pearson moments
>>> np.corrcoef(df.a.tail(100), df.b.tail(100))[0, 1]
nan

# 2. using custom function
>>> def pearson(a, b):
        n = a.size
        num = n*np.nansum(a*b) - np.nansum(a)*np.nansum(b)
        den = (n*np.nansum((a**2)) - np.nansum(a)**2)*(n*np.nansum(b**2) - np.nansum(b)**2)
        return num/np.sqrt(den) if den * np.isfinite(den*num) else np.nan

>>> pearson(df.a.tail(100), df.b.tail(100))
nan

现在,现实情况是:
>>> df.a.rolling(100).corr(df.b).tail(3)
 
12979    7.761921e-07
12980    5.460717e-07
12981    2.755881e-10                    # This should have been NaN/inf !!

## Furthermore!!

>>> debug = df.tail(300)
>>> debug.a.rolling(100).corr(debug.b).tail(3)

12979    7.761921e-07
12980    5.460717e-07
12981            -inf                    # Got -inf, fine
dtype: float64

>>> debug = df.tail(3000)
>>> debug.a.rolling(100).corr(debug.b).tail(3)
 
12979    7.761921e-07
12980    5.460717e-07
12981             inf                     # Got +inf, still acceptable
dtype: float64

这将持续到 9369 行:

>>> debug = df.tail(9369)
>>> debug.a.rolling(100).corr(debug.b).tail(3)

12979    7.761921e-07
12980    5.460717e-07
12981             inf
dtype: float64

# then
>>> debug = df.tail(9370)
>>> debug.a.rolling(100).corr(debug.b).tail(3)

12979    7.761921e-07
12980    5.460717e-07
12981    4.719615e-10                    # SPOOKY ACTION IN DISTANCE!!!
dtype: float64

>>> debug = df.tail(10000)
>>> debug.a.rolling(100).corr(debug.b).tail(3)
 
12979    7.761921e-07
12980    5.460717e-07
12981    1.198994e-10                    # SPOOKY ACTION IN DISTANCE!!!    
dtype: float64

当前解决方案

>>> df.a.rolling(100).apply(lambda x: x.corr(df.b.reindex(x.index))).tail(3)   # PREDICTABLY, VERY SLOW!

12979    7.761921e-07
12980    5.460717e-07
12981             NaN
Name: a, dtype: float64

# again this checks out using other methods,
>>> df.a.rolling(100).apply(lambda x: np.corrcoef(x, df.b.reindex(x.index))[0, 1]).tail(3)
 
12979    7.761921e-07
12980    5.460717e-07
12981             NaN
Name: a, dtype: float64

>>> df.a.rolling(100).apply(lambda x: pearson(x, df.b.reindex(x.index))).tail(3)

12979    7.761921e-07
12980    5.460717e-07
12981             NaN
Name: a, dtype: float64

据我了解,series.rolling(n).corr(other_series)的结果应该与以下内容相匹配:
>>> def rolling_corr(series, other_series, n=100):
        return pd.Series(
                    [np.nan]*(n-1) + [series[i-n: i].corr(other_series[i-n:i]) 
                    for i in range (n, series.size+1)]
        )

>>> rolling_corr(df.a, df.b).tail(3)

12979    7.761921e-07
12980    5.460717e-07
12981             NaN

起初,我认为这是浮点算术问题(因为最初,在某些情况下,通过将列“a”四舍五入到5位小数或转换为float32可以解决此问题),但在这种情况下,无论使用多少样本都会出现。因此,可能存在一些与滚动有关的问题,或者至少滚动会根据数据大小引起浮点问题。我检查了rolling.corr的源代码,但找不到任何可以解释这种不一致性的东西。现在我很担心,有多少过去的代码受到这个问题的困扰。
这是什么原因?如何解决?如果这是因为pandas更注重速度而不是准确性(如here所建议的),那么这是否意味着我永远无法可靠地在大样本上使用pandas.rolling操作?我该如何知道超出哪个大小会出现这种不一致性?

样本相关数据:https://pastebin.com/jXXHSv3r

测试环境

  • Windows 10,python 3.9.1,pandas 1.2.2,(IPython 7.20)
  • Windows 10,python 3.8.2,pandas 1.0.5,(IPython 7.19)
  • Ubuntu 20.04,python 3.7.7,pandas 1.0.5,(GCC 7.3.0,标准REPL)
  • CentOS Linux 7(核心),Python 2.7.5,pandas 0.23.4,(IPython 5.8.0)

注意:不同的操作系统在所述索引处返回不同的值,但都是有限且接近于0


我认为问题出在corr中,因为它涉及到计算中的std。NumPy中有类似的问题: https://dev59.com/ab3pa4cB1Zd3GeqPYB9G https://github.com/numpy/numpy/issues/9631 - Quant Christo
@QuantChristo 但为什么这个行为不一致呢?就像我展示的那样,滚动相关性可以工作到9369行。手动应用相关性可以得出正确的结果。这种不一致性让我很困扰。 - Sayandip Dutta
根据文档(以及源代码),您对滚动窗口的预期行为是正确的。我手动尝试了数据帧尾部的_get_corr(a,b)函数;它确实返回nan...所以也许这与窗口或“flex_binary_moment”有关(我对此一无所知)。 - tgrandje
@tgrandje 我已经检查了flex_binary_moment,它只是在ab都是序列时对给定的函数进行应用。对于corr,该函数是_get_corr(a, b)。它使用了cov、var和std。问题可能出现在这些函数中,但我认为这不是问题,因为它们分别返回正确的结果,你可能已经看到了。 - Sayandip Dutta
我的猜测是与Rolling背后的C代码有关,但我需要一个明确的答案,否则我将无法再次使用'rolling.corr'。而其他选择非常缓慢。因此,在使用自定义函数之前,我需要确定没有其他选择。 - Sayandip Dutta
@tgrandje 我创建了一个聊天室:https://chat.stackoverflow.com/rooms/230066/rolling-corr,如果你愿意,我们可以在那里讨论你的答案。 - Sayandip Dutta
1个回答

3

如果您在Pearson公式中用滚动求和取代总和,会发生什么?


def rolling_pearson(a, b, n):
    a_sum = a.rolling(n).sum()
    b_sum = b.rolling(n).sum()
    ab_sum = (a*b).rolling(n).sum()
    aa_sum = (a**2).rolling(n).sum()
    bb_sum = (b**2).rolling(n).sum();
    
    num = n * ab_sum - a_sum * b_sum;
    den = (n*aa_sum - a_sum**2) * (n * bb_sum - b_sum**2)
    return num / den**(0.5)

rolling_pearson(df.a, df.b, 100)


             ...     
12977    1.109077e-06
12978    9.555249e-07
12979    7.761921e-07
12980    5.460717e-07
12981             inf
Length: 12982, dtype: float64

这是为什么

为了回答这个问题,我需要检查实现方式。因为确实最后100个样本的 b 的方差为零,滚动相关性计算方法为 a.cov(b) / (a.var() * b.var())**0.5

经过一番搜索,我在这里找到了滚动方差实现 here,他们使用的方法是Welford在线算法。 这个算法非常不错,因为您可以仅使用一个乘法(与累计和方法相同)添加一个样本,并且可以使用单个整数除法进行计算。 在此将其重写为Python。

def welford_add(existingAggregate, newValue):
    if pd.isna(newValue):
        return s
    (count, mean, M2) = existingAggregate
    count += 1
    delta = newValue - mean
    mean += delta / count
    delta2 = newValue - mean
    M2 += delta * delta2
    return (count, mean, M2)
def welford_remove(existingAggregate, newValue):
    if pd.isna(newValue):
        return s
    (count, mean, M2) = existingAggregate
    count -= 1
    delta = newValue - mean
    mean -= delta / count
    delta2 = newValue - mean
    M2 -= delta * delta2
    return (count, mean, M2)
def finalize(existingAggregate):
    (count, mean, M2) = existingAggregate
    (mean, variance, sampleVariance) = (mean, 
            M2 / count if count > 0 else None, 
            M2 / (count - 1) if count > 1 else None)
    return (mean, variance, sampleVariance)

在Pandas的实现中,他们提到了Kahan求和算法,这对于加法运算的精度很重要,但结果并没有因此得到改善(我没有检查它是否被正确实现)。
应用Welford算法,n=100
s = (0,0,0)
for i in range(len(df.b)):
    if i >= n:
        s = welford_remove(s, df.b[i-n])
    s = welford_add(s, df.b[i])
finalize(s)

它给出
(6.000000000000152, 4.7853099260919405e-12, 4.8336463899918594e-12)

df.b.rolling(100).var()则给出

0                 NaN
1                 NaN
2                 NaN
3                 NaN
4                 NaN
             ...     
12977    6.206061e-01
12978    4.703030e-01
12979    3.167677e-01
12980    1.600000e-01
12981    6.487273e-12
Name: b, Length: 12982, dtype: float64

误差略高于Welford方法的直接应用,为6.4e-12,而该方法给出的误差为4.83e-12

另一方面,对于最后一个条目,(df.b**2).rolling(n).sum()-df.b.rolling(n).sum()**2/n 的结果为0.0。

0          NaN
1          NaN
2          NaN
3          NaN
4          NaN
         ...  
12977    61.44
12978    46.56
12979    31.36
12980    15.84
12981     0.00
Name: b, Length: 12982, dtype: float64

我希望这个解释是令人满意的 :)

这样做更快,但我主要想知道为什么不一致会首先出现。 - Sayandip Dutta

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接