web-dev-qa-db-fra.com

Calcul efficace de la bêta des stocks de pandas Python sur de nombreux cadres de données

J'ai beaucoup (4000+) CSV de données de stock (Date, Ouvert, Haut, Bas, Fermé) que j'importe dans des cadres de données Pandas individuels pour effectuer une analyse. Je suis nouveau sur Python et je souhaite calculer une version bêta glissante sur 12 mois pour chaque action. J'ai trouvé un poste permettant de calculer la version bêta roulante ( Les pandas Python calculent la version bêta du matériel roulant à l’aide de s’appliquer à un objet groupby de manière vectorielle ). mon code ci-dessous prend plus de 2,5 heures! Étant donné que je peux exécuter exactement les mêmes calculs dans les tables SQL en moins de 3 minutes, cela est trop lent.

Comment puis-je améliorer les performances de mon code ci-dessous pour qu'elles correspondent à celles de SQL? Je comprends que Pandas/python a cette capacité. Ma méthode actuelle effectue une boucle sur chaque ligne, ce qui, je le sais, ralentit les performances, mais je ne connais aucune méthode globale permettant d'effectuer un calcul bêta à fenêtre glissante sur un cadre de données.

Remarque: les 2 premières étapes du chargement des CSV dans des cadres de données individuels et du calcul des rendements quotidiens ne prennent que 20 secondes environ. Toutes mes images CSV sont stockées dans le dictionnaire appelé "FilesLoaded" avec des noms tels que "XAO".

Votre aide serait très appréciée! Je vous remercie :)

import pandas as pd, numpy as np
import datetime
import ntpath
pd.set_option('precision',10)  #Set the Decimal Point precision to DISPLAY
start_time=datetime.datetime.now()

MarketIndex = 'XAO'
period = 250
MinBetaPeriod = period
# ***********************************************************************************************
# CALC RETURNS 
# ***********************************************************************************************
for File in FilesLoaded:
    FilesLoaded[File]['Return'] = FilesLoaded[File]['Close'].pct_change()
# ***********************************************************************************************
# CALC BETA
# ***********************************************************************************************
def calc_beta(df):
    np_array = df.values
    m = np_array[:,0] # market returns are column zero from numpy array
    s = np_array[:,1] # stock returns are column one from numpy array
    covariance = np.cov(s,m) # Calculate covariance between stock and market
    beta = covariance[0,1]/covariance[1,1]
    return beta

#Build Custom "Rolling_Apply" function
def rolling_apply(df, period, func, min_periods=None):
    if min_periods is None:
        min_periods = period
    result = pd.Series(np.nan, index=df.index)
    for i in range(1, len(df)+1):
        sub_df = df.iloc[max(i-period, 0):i,:]
        if len(sub_df) >= min_periods:  
            idx = sub_df.index[-1]
            result[idx] = func(sub_df)
    return result

#Create empty BETA dataframe with same index as RETURNS dataframe
df_join = pd.DataFrame(index=FilesLoaded[MarketIndex].index)    
df_join['market'] = FilesLoaded[MarketIndex]['Return']
df_join['stock'] = np.nan

for File in FilesLoaded:
    df_join['stock'].update(FilesLoaded[File]['Return'])
    df_join  = df_join.replace(np.inf, np.nan) #get rid of infinite values "inf" (SQL won't take "Inf")
    df_join  = df_join.replace(-np.inf, np.nan)#get rid of infinite values "inf" (SQL won't take "Inf")
    df_join  = df_join.fillna(0) #get rid of the NaNs in the return data
    FilesLoaded[File]['Beta'] = rolling_apply(df_join[['market','stock']], period, calc_beta, min_periods = MinBetaPeriod)

# ***********************************************************************************************
# CLEAN-UP
# ***********************************************************************************************
print('Run-time: {0}'.format(datetime.datetime.now() - start_time))
12
cwse

Générer des données de stock aléatoires
20 ans de données mensuelles pour 4 000 actions

dates = pd.date_range('1995-12-31', periods=480, freq='M', name='Date')
stoks = pd.Index(['s{:04d}'.format(i) for i in range(4000)])
df = pd.DataFrame(np.random.Rand(480, 4000), dates, stoks)

df.iloc[:5, :5]

 enter image description here


Fonction de rotation
Renvoie un objet groupby prêt à appliquer des fonctions personnalisées
Voir Source

def roll(df, w):
    # stack df.values w-times shifted once at each stack
    roll_array = np.dstack([df.values[i:i+w, :] for i in range(len(df.index) - w + 1)]).T
    # roll_array is now a 3-D array and can be read into
    # a pandas panel object
    panel = pd.Panel(roll_array, 
                     items=df.index[w-1:],
                     major_axis=df.columns,
                     minor_axis=pd.Index(range(w), name='roll'))
    # convert to dataframe and pivot + groupby
    # is now ready for any action normally performed
    # on a groupby object
    return panel.to_frame().unstack().T.groupby(level=0)

Fonction bêta
Utiliser une solution fermée de la régression OLS
Supposons que la colonne 0 représente le marché
Voir Source

def beta(df):
    # first column is the market
    X = df.values[:, [0]]
    # prepend a column of ones for the intercept
    X = np.concatenate([np.ones_like(X), X], axis=1)
    # matrix algebra
    b = np.linalg.pinv(X.T.dot(X)).dot(X.T).dot(df.values[:, 1:])
    return pd.Series(b[1], df.columns[1:], name='Beta')

Manifestation

rdf = roll(df, 12)
betas = rdf.apply(beta)

Timing

 enter image description here


Validation
Comparer les calculs avec OP

def calc_beta(df):
    np_array = df.values
    m = np_array[:,0] # market returns are column zero from numpy array
    s = np_array[:,1] # stock returns are column one from numpy array
    covariance = np.cov(s,m) # Calculate covariance between stock and market
    beta = covariance[0,1]/covariance[1,1]
    return beta

print(calc_beta(df.iloc[:12, :2]))

-0.311757542437

print(beta(df.iloc[:12, :2]))

s0001   -0.311758
Name: Beta, dtype: float64

Notez la première cellule
Est la même valeur que les calculs validés ci-dessus

betas = rdf.apply(beta)
betas.iloc[:5, :5]

 enter image description here


Réponse au commentaire
Exemple de travail complet avec plusieurs images simulées

num_sec_dfs = 4000

cols = ['Open', 'High', 'Low', 'Close']
dfs = {'s{:04d}'.format(i): pd.DataFrame(np.random.Rand(480, 4), dates, cols) for i in range(num_sec_dfs)}

market = pd.Series(np.random.Rand(480), dates, name='Market')

df = pd.concat([market] + [dfs[k].Close.rename(k) for k in dfs.keys()], axis=1).sort_index(1)

betas = roll(df.pct_change().dropna(), 12).apply(beta)

for c, col in betas.iteritems():
    dfs[c]['Beta'] = col

dfs['s0001'].head(20)

 enter image description here

9
piRSquared

Utiliser un générateur pour améliorer l'efficacité de la mémoire

Données simulées 

m, n = 480, 10000
dates = pd.date_range('1995-12-31', periods=m, freq='M', name='Date')
stocks = pd.Index(['s{:04d}'.format(i) for i in range(n)])
df = pd.DataFrame(np.random.Rand(m, n), dates, stocks)
market = pd.Series(np.random.Rand(m), dates, name='Market')
df = pd.concat([df, market], axis=1)

Calcul bêta 

def beta(df, market=None):
    # If the market values are not passed,
    # I'll assume they are located in a column
    # named 'Market'.  If not, this will fail.
    if market is None:
        market = df['Market']
        df = df.drop('Market', axis=1)
    X = market.values.reshape(-1, 1)
    X = np.concatenate([np.ones_like(X), X], axis=1)
    b = np.linalg.pinv(X.T.dot(X)).dot(X.T).dot(df.values)
    return pd.Series(b[1], df.columns, name=df.index[-1])

fonction de défilement
Cela retourne un générateur et sera beaucoup plus efficace en mémoire

def roll(df, w):
    for i in range(df.shape[0] - w + 1):
        yield pd.DataFrame(df.values[i:i+w, :], df.index[i:i+w], df.columns)

Mettre tous ensemble 

betas = pd.concat([beta(sdf) for sdf in roll(df.pct_change().dropna(), 12)], axis=1).T

Validation

OP beta calc

def calc_beta(df):
    np_array = df.values
    m = np_array[:,0] # market returns are column zero from numpy array
    s = np_array[:,1] # stock returns are column one from numpy array
    covariance = np.cov(s,m) # Calculate covariance between stock and market
    beta = covariance[0,1]/covariance[1,1]
    return beta

Configuration de l'expérience

m, n = 12, 2
dates = pd.date_range('1995-12-31', periods=m, freq='M', name='Date')

cols = ['Open', 'High', 'Low', 'Close']
dfs = {'s{:04d}'.format(i): pd.DataFrame(np.random.Rand(m, 4), dates, cols) for i in range(n)}

market = pd.Series(np.random.Rand(m), dates, name='Market')

df = pd.concat([market] + [dfs[k].Close.rename(k) for k in dfs.keys()], axis=1).sort_index(1)

betas = pd.concat([beta(sdf) for sdf in roll(df.pct_change().dropna(), 12)], axis=1).T

for c, col in betas.iteritems():
    dfs[c]['Beta'] = col

dfs['s0000'].head(20)

 enter image description here

calc_beta(df[['Market', 's0000']])

0.0020118230147777435

REMARQUE:
Les calculs sont les mêmes

4
piRSquared

Bien que l'efficacité de la subdivision du jeu de données d'entrée en fenêtres glissantes soit importante pour l'optimisation des calculs globaux, les performances du calcul bêta lui-même peuvent également être considérablement améliorées.

Les éléments suivants optimisent uniquement la subdivision du jeu de données en fenêtres glissantes:

def numpy_betas(x_name, window, returns_data, intercept=True):
    if intercept:
        ones = numpy.ones(window)

    def lstsq_beta(window_data):
        x_data = numpy.vstack([window_data[x_name], ones]).T if intercept else window_data[[x_name]]
        beta_arr, residuals, rank, s = numpy.linalg.lstsq(x_data, window_data)
        return beta_arr[0]

    indices = [int(x) for x in numpy.arange(0, returns_data.shape[0] - window + 1, 1)]
    return DataFrame(
        data=[lstsq_beta(returns_data.iloc[i:(i + window)]) for i in indices]
        , columns=list(returns_data.columns)
        , index=returns_data.index[window - 1::1]
    )

Ce qui suit optimise également le calcul de la bêta elle-même:

def custom_betas(x_name, window, returns_data):
    window_inv = 1.0 / window
    x_sum = returns_data[x_name].rolling(window, min_periods=window).sum()
    y_sum = returns_data.rolling(window, min_periods=window).sum()
    xy_sum = returns_data.mul(returns_data[x_name], axis=0).rolling(window, min_periods=window).sum()
    xx_sum = numpy.square(returns_data[x_name]).rolling(window, min_periods=window).sum()
    xy_cov = xy_sum - window_inv * y_sum.mul(x_sum, axis=0)
    x_var = xx_sum - window_inv * numpy.square(x_sum)
    betas = xy_cov.divide(x_var, axis=0)[window - 1:]
    betas.columns.name = None
    return betas

En comparant les performances de deux calculs différents, vous pouvez constater que, lorsque la fenêtre utilisée dans le calcul bêta augmente, la seconde méthode surpasse de manière spectaculaire la première:  enter image description here

En comparant les performances à celles de l'implémentation de @ piRSquared, la méthode personnalisée nécessite environ 350 millisecondes à évaluer, contre plus de 2 secondes.

0
mcguip