两个pandas数据框之间的快速斯皮尔曼相关性

3

我想将spearman相关性应用于两个具有相同列数的pandas数据帧(每对行的相关性)。

我的目标是计算每一对行(r,s)之间的spearman相关性分布,其中r是来自第一个数据帧的行,s是来自第二个数据帧的行。

我知道以前已经回答了类似的问题(请参见这里)。然而,这个问题不同,因为我想比较第一个数据框中的每一行与第二个数据框中的所有行。此外,由于我的数据规模,这是计算密集型的,并且需要几个小时的时间。我想并行化它,并可能重写它以加快速度。

我尝试使用numba,但不幸的是它失败了(类似于这个问题),因为它似乎无法识别scipy spearmanr。我的代码如下:

def corr(a, b):
    dist = []
    for i in range(a.shape[0]):
        for j in range(b.shape[0]):
            dist += [spearmanr(a.iloc[i, :], b.iloc[j, :])[0]]
    return dist
3个回答

5

新答案

from numba import njit
import pandas as pd
import numpy as np

@njit
def mean1(a):
  n = len(a)
  b = np.empty(n)
  for i in range(n):
    b[i] = a[i].mean()
  return b

@njit
def std1(a):
  n = len(a)
  b = np.empty(n)
  for i in range(n):
    b[i] = a[i].std()
  return b

@njit
def c(a, b):
    ''' Correlation '''
    n, k = a.shape
    m, k = b.shape

    mu_a = mean1(a)
    mu_b = mean1(b)
    sig_a = std1(a)
    sig_b = std1(b)

    out = np.empty((n, m))

    for i in range(n):
        for j in range(m):
            out[i, j] = (a[i] - mu_a[i]) @ (b[j] - mu_b[j]) / k / sig_a[i] / sig_b[j]

    return out

r = df_test.rank(1).values
df_test.T.corr('spearman') == c(r, r)

旧答案

做斯皮尔曼等级相关性只是对等级进行相关性分析。

等级

我们可以利用 argsort 来获得等级。虽然 argsort 的排名可以获得等级,但我们可以通过切片赋值将自己限制在一个排序中。

def rank(a):
  i, j = np.meshgrid(*map(np.arange, a.shape), indexing='ij')

  s = a.argsort(1)
  out = np.empty_like(s)
  out[i, s] = j

  return out

相关性

在相关排名的情况下,均值和标准差都由数组的第二个维度的大小预先确定。

你可以不使用numba来实现相同的功能,但我假设你想要使用它。

from numba import njit

@njit
def c(a, b):
  n, k = a.shape
  m, k = b.shape

  mu = (k - 1) / 2
  sig = ((k - 1) * (k + 1) / 12) ** .5

  out = np.empty((n, m))

  a = a - mu
  b = b - mu

  for i in range(n):
    for j in range(m):
      out[i, j] = a[i] @ b[j] / k / sig ** 2

  return out

为了后人,我们可以完全避免内部循环,但这可能会导致内存问题。
@njit
def c1(a, b):
  n, k = a.shape
  m, k = b.shape

  mu = (k - 1) / 2
  sig = ((k - 1) * (k + 1) / 12) ** .5

  a = a - mu
  b = b - mu

  return a @ b.T / k / sig ** 2

Demonstration

np.random.seed([3, 1415])

a = np.random.randn(2, 10)
b = np.random.randn(2, 10)

rank_a = rank(a)
rank_b = rank(b)

c(rank_a, rank_b)

array([[0.32121212, 0.01818182],
       [0.13939394, 0.55151515]])

如果你正在使用 DataFrame

da = pd.DataFrame(a)
db = pd.DataFrame(b)

pd.DataFrame(c(rank(da.values), rank(db.values)), da.index, db.index)


          0         1
0  0.321212  0.018182
1  0.139394  0.551515

验证

我们可以使用 pandas.DataFrame.corr 快速进行验证。

pd.DataFrame(a.T).corr('spearman') == c(rank_a, rank_a)

      0     1
0  True  True
1  True  True

是的,抱歉-评论空间非常短。这是一个 pastebin 链接:https://pastebin.com/M5mFz8FJ - gc5
我得到了全零。你能否编辑那个粘贴板,包括一个[mcve],也就是包括数据。这样,我就可以直接从粘贴板复制并运行它。 - piRSquared
我无法从我的位置访问Dropbox。我是指在您的粘贴板中提供一小部分数据,以便我们可以确保在调试时使用相同的数据。 - piRSquared
啊!我现在明白了。这是一个大问题。我没有预料到会有稀疏类型数组。也就是说,我没想到会有许多零值。我需要重新考虑一下。 - piRSquared
让我们在聊天中继续这个讨论 - gc5
显示剩余5条评论

2
以下是使用行为基础、未编译版本的 scipy.stats.spearmanr 的示例,它在大型数据集上仅需 ~5% 的时间,并展示了其生成相同结果的示例:
import numpy as np

import pandas as pd


def spearman_row(x, y):

    x = np.asarray(x)
    y = np.asarray(y)

    rx = rankdata_average(x)
    ry = rankdata_average(y)

    # print(rx)
    # print(ry)

    return compute_corr(rx, ry)

def compute_corr(x, y):

    # Thanks to https://github.com/dengemann

    def ss(a, axis):
        return np.sum(a * a, axis=axis)

    x = np.asarray(x)
    y = np.asarray(y)

    mx = x.mean(axis=-1)
    my = y.mean(axis=-1)

    xm, ym = x - mx[..., None], y - my[..., None]

    r_num = np.add.reduce(xm * ym, axis=-1)
    r_den = np.sqrt(ss(xm, axis=-1) * ss(ym, axis=-1))

    with np.errstate(divide='ignore', invalid="ignore"):

        r = r_num / r_den

    return r


def rankdata_average(data):

    """Row-based rankdata using method=mean"""

    dc = np.asarray(data).copy()
    sorter = np.apply_along_axis(np.argsort, 1, data)

    inv = np.empty(data.shape, np.intp)

    ranks = np.tile(np.arange(data.shape[1]), (len(data), 1))

    np.put_along_axis(inv, sorter, ranks, axis=1)

    dc = np.take_along_axis(dc, sorter, 1)

    res = np.apply_along_axis(lambda r: r[1:] != r[:-1], 1, dc)

    obs = np.column_stack([np.ones(len(res), dtype=bool), res])

    dense = np.take_along_axis(np.apply_along_axis(np.cumsum, 1, obs), inv, 1)

    len_r = obs.shape[1]

    nonzero = np.count_nonzero(obs, axis=1)
    obs = pd.DataFrame(obs)
    nonzero = pd.Series(nonzero)
    dense = pd.DataFrame(dense)

    ranks = []
    for _nonzero, nzdf in obs.groupby(nonzero, sort=False):

        nz = np.apply_along_axis(lambda r: np.nonzero(r)[0], 1, nzdf)

        _count = np.column_stack([nz, np.ones(len(nz)) * len_r])
        _dense = dense.reindex(nzdf.index).values

        _result = 0.5 * (np.take_along_axis(_count, _dense, 1) + np.take_along_axis(_count, _dense - 1, 1) + 1)

        result = pd.DataFrame(_result, index=nzdf.index)
        ranks.append(result)

    final = pd.concat(ranks).sort_index()

    return final


if __name__ == "__main__":

    from scipy.stats import rankdata, spearmanr
    from time import time

    np.random.seed(0)

    size = int(1e5), 5
    d1 = np.random.randint(5, size=size)
    d2 = np.random.randint(5, size=size)

    start = time()
    actual = spearman_row(d1, d2)
    end = time()
    print("actual", actual)
    print("rowbased took", end - start)

    start = time()
    expected = []
    for i in range(len(d1)):
        expected.append(spearmanr(d1[i], d2[i]).correlation)
    end = time()
    print("scipy took", end - start)

    expected = np.array(expected)

    print("largest diff", pd.Series(expected - actual).abs().max())

它会打印出:

rowbased took 3.6308434009552
scipy took 53.552557945251465
largest diff 2.220446049250313e-16 

0
Pandas有一个支持spearman的corr函数。它适用于列,因此我们可以只需转置数据框即可。
我们将df1附加到df2并通过迭代每一行来计算相关性。
len_df1 = df1.shape[0]
df2_index = df2.index.values.tolist()


df = df2.append(df1).reset_index(drop=True).T
values = {i: [df.iloc[:,df2_index+[i]].corr(method='spearman').values] for i in range(len_df1)}

我认为在使用df2.index时存在问题,因为它由字符串组成,而在最后两个语句中,我认为它是列的排名(由于reset_index())。感谢建议,但我正在努力从这里开始构建。 - gc5
我不知道数据框长什么样,但是在df2_index赋值之前你需要先使用reset_index()一次,这样应该就可以了。 - Raunaq Jain

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