web-dev-qa-db-fra.com

Pandas calcul de corrélation de fenêtre en expansion / déroulante avec valeur de p

Supposons que j'ai un DataFrame, sur lequel je veux calculer les corrélations Pearson de roulement ou d'extension entre deux colonnes

import numpy as np
import pandas as pd
import scipy.stats as st


df = pd.DataFrame({'x': np.random.Rand(10000), 'y': np.random.Rand(10000)})

Avec la fonctionnalité pandas intégrée, il est très rapide de calculer cela

expanding_corr = df['x'].expanding(50).corr(df['y'])
rolling_corr = df['x'].rolling(50).corr(df['y'])

Cependant, si je souhaite obtenir les valeurs p associées à ces corrélations, le mieux que je puisse faire est de définir une fonction de roulement personnalisée et de passer apply à groupby objet

def custom_roll(df, w, **kwargs):

    v = df.values
    d0, d1 = v.shape
    s0, s1 = v.strides
    a = np.lib.stride_tricks.as_strided(v, (d0 - (w - 1), w, d1), (s0, s0, s1))
    rolled_df = pd.concat({
        row: pd.DataFrame(values, columns=df.columns)
        for row, values in Zip(df.index[(w-1):], a)
    })
    return rolled_df.groupby(level=0, **kwargs)

c_df = custom_roll(df, 50).apply(lambda df: st.pearsonr(df['x'], df['y']))

c_df contient maintenant les corrélations appropriées et surtout leurs valeurs de p associées.

Cependant, cette méthode est extrêmement lente par rapport à la méthode intégrée pandas, ce qui signifie qu'elle n'est pas appropriée, car je calcule pratiquement ces corrélations des milliers de fois au cours d'un processus d'optimisation. De plus, je ne sais pas comment étendre le custom_roll fonction pour travailler pour agrandir les fenêtres.

Quelqu'un peut-il m'orienter dans le sens de tirer parti de numpy pour obtenir les valeurs de p sur des fenêtres en expansion à des vitesses vectorisées?

14
M Horsley

Je ne pouvais pas penser à un moyen intelligent de le faire dans pandas en utilisant rolling directement, mais notez que vous pouvez calculer la valeur de p en fonction du coefficient de corrélation.

Le coefficient de corrélation de Pearson suit la distribution t de Student et vous pouvez obtenir la valeur p en la branchant au cdf défini par la fonction bêta incomplète, scipy.special.betainc. Cela semble compliqué mais peut être fait en quelques lignes de code. Vous trouverez ci-dessous une fonction qui calcule la valeur de p en fonction du coefficient de corrélation corr et de la taille de l'échantillon n. Il est en fait basé sur implémentation de scipy que vous avez utilisé.

import pandas as pd
from scipy.special import betainc

def pvalue(corr, n=50):
    df = n - 2
    t_squared = corr**2 * (df / ((1.0 - corr) * (1.0 + corr)))
    prob = betainc(0.5*df, 0.5, df/(df+t_squared))
    return prob

Vous pouvez ensuite appliquer cette fonction aux valeurs de corrélation que vous avez déjà.

rolling_corr = df['x'].rolling(50).corr(df['y'])
pvalue(rolling_corr)

Ce n'est peut-être pas la solution numpy vectorisée parfaite, mais devrait être des dizaines de fois plus rapide que de calculer les corrélations encore et encore.

5
AlCorreia

Approche n ° 1

corr2_coeff_rowwise indique comment effectuer une corrélation élément par élément entre les lignes. Nous pourrions le réduire à un cas de corrélation par élément entre deux colonnes. Donc, nous finirions avec une boucle qui utilise corr2_coeff_rowwise. Ensuite, nous essayons de le vectoriser et voyons qu'il y a des morceaux qui pourraient être vectorisés:

  1. Obtenir des valeurs moyennes avec mean. Cela pourrait être vectorisé en utilisant un filtre uniforme.
  2. La prochaine étape consistait à obtenir les différences entre ces valeurs moyennes par rapport aux éléments glissants des tableaux d'entrée. Pour porter sur un fichier vectorisé, nous utiliserions broadcasting.

Le reste reste le même pour obtenir la première des deux sorties de pearsonr.

Pour obtenir la deuxième sortie, nous revenons à source code . Cela devrait être simple étant donné le premier coefficient de sortie.

Donc, avec ceux à l'esprit, nous nous retrouverions avec quelque chose comme ça -

import scipy.special as special
from scipy.ndimage import uniform_filter

def sliding_corr1(a,b,W):
    # a,b are input arrays; W is window length

    am = uniform_filter(a.astype(float),W)
    bm = uniform_filter(b.astype(float),W)

    amc = am[W//2:-W//2+1]
    bmc = bm[W//2:-W//2+1]

    da = a[:,None]-amc
    db = b[:,None]-bmc

    # Get sliding mask of valid windows
    m,n = da.shape
    mask1 = np.arange(m)[:,None] >= np.arange(n)
    mask2 = np.arange(m)[:,None] < np.arange(n)+W
    mask = mask1 & mask2
    dam = (da*mask)
    dbm = (db*mask)

    ssAs = np.einsum('ij,ij->j',dam,dam)
    ssBs = np.einsum('ij,ij->j',dbm,dbm)
    D = np.einsum('ij,ij->j',dam,dbm)
    coeff = D/np.sqrt(ssAs*ssBs)

    n = W
    ab = n/2 - 1
    pval = 2*special.btdtr(ab, ab, 0.5*(1 - abs(np.float64(coeff))))
    return coeff,pval

Ainsi, pour obtenir la sortie finale des entrées de la série pandas -

out = sliding_corr1(df['x'].to_numpy(copy=False),df['y'].to_numpy(copy=False),50)

Approche n ° 2

Beaucoup similaire à Approach #1, mais nous utiliserons numba pour améliorer l'efficacité de la mémoire pour remplacer l'étape # 2 de l'approche précédente.

from numba import njit
import math

@njit(parallel=True)
def sliding_corr2_coeff(a,b,amc,bmc):
    L = len(a)-W+1
    out00 = np.empty(L)
    for i in range(L):
        out_a = 0
        out_b = 0
        out_D = 0
        for j in range(W):
            d_a = a[i+j]-amc[i]
            d_b = b[i+j]-bmc[i]
            out_D += d_a*d_b
            out_a += d_a**2
            out_b += d_b**2
        out00[i] = out_D/math.sqrt(out_a*out_b)
    return out00

def sliding_corr2(a,b,W):
    am = uniform_filter(a.astype(float),W)
    bm = uniform_filter(b.astype(float),W)

    amc = am[W//2:-W//2+1]
    bmc = bm[W//2:-W//2+1]

    coeff = sliding_corr2_coeff(a,b,amc,bmc)

    ab = W/2 - 1
    pval = 2*special.btdtr(ab, ab, 0.5*(1 - abs(np.float64(coeff))))
    return coeff,pval

Approche n ° 3

Très similaire à la précédente, sauf que nous poussons tous les coefficients à numba -

@njit(parallel=True)
def sliding_corr3_coeff(a,b,W):
    L = len(a)-W+1
    out00 = np.empty(L)
    for i in range(L):
        a_mean = 0.0
        b_mean = 0.0
        for j in range(W):
            a_mean += a[i+j]
            b_mean += b[i+j]
        a_mean /= W
        b_mean /= W

        out_a = 0
        out_b = 0
        out_D = 0
        for j in range(W):
            d_a = a[i+j]-a_mean
            d_b = b[i+j]-b_mean
            out_D += d_a*d_b
            out_a += d_a*d_a
            out_b += d_b*d_b
        out00[i] = out_D/math.sqrt(out_a*out_b)
    return out00

def sliding_corr3(a,b,W):    
    coeff = sliding_corr3_coeff(a,b,W)
    ab = W/2 - 1
    pval = 2*special.btdtr(ab, ab, 0.5*(1 - np.abs(coeff)))
    return coeff,pval

Horaires -

In [181]: df = pd.DataFrame({'x': np.random.Rand(10000), 'y': np.random.Rand(10000)})

In [182]: %timeit sliding_corr2(df['x'].to_numpy(copy=False),df['y'].to_numpy(copy=False),50)
100 loops, best of 3: 5.05 ms per loop

In [183]: %timeit sliding_corr3(df['x'].to_numpy(copy=False),df['y'].to_numpy(copy=False),50)
100 loops, best of 3: 5.51 ms per loop

Remarque :

  • sliding_corr1 semble prendre beaucoup de temps sur cet ensemble de données et probablement à cause de la mémoire requise par son étape # 2.

  • Le goulot d'étranglement après avoir utilisé les fonctions numba, puis transféré vers le calcul de p-val avec special.btdtr.

3
Divakar