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?
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.
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:
mean
. Cela pourrait être vectorisé en utilisant un filtre uniforme.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
.