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))
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]
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
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]
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)
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
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)
calc_beta(df[['Market', 's0000']])
0.0020118230147777435
REMARQUE:
Les calculs sont les mêmes
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:
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.