Comment multiplier par 300 la vitesse d’itération de grandes quantités de données (avec Numba) ?
L’un des défis auxquels on est confronté lors d’un backtesting est de devoir itérer à travers des lignes. Contrairement à d’autres opérations où la vectorisation est une possibilité et apporte des gains de vitesse significatifs, on est limité lors du backtesting car on a typiquement besoin des valeurs de la ligne précédente pour calculer la ligne suivante. Cela vous oblige à itérer dans les données, ce qui, comme nous le savons, est incroyablement lent.
Ensemble nous allons voir comment itérer sur un jeu de données qui contient environ 15 ans de données sur les options. Pour ce faire nous allons explorer et comparer trois façons différentes d’effectuer une itération sur ce jeu de données.
Numba est la plus grande invention sur terre. Si vous pouvez utiliser des tableaux et numpy pour effectuer votre itération, utilisez numba.
import plotly.express as px import pandas as pd import numpy as np import numba long_data = pd.read_csv("long.csv") long_data["Open"] = long_data["Open"].str.replace(' Il n'y a pas grand-chose à voir ici - il s'agit simplement de nettoyer un peu les données en supprimant les signes de dollar et en convertissant tout en valeurs flottantes. La principale chose que nous testerons sera d'itérer sur la trame de données et les transactions ouvertes et fermées, en calculant le PnL et le changement résultant de la taille du compte. La taille du compte détermine alors la taille du lot de la transaction suivante - la vectorisation n'est donc pas possible puisque le PnL de chaque transaction ultérieure dépend de la taille du lot qui dépend de la modification de la taille du compte de la transaction précédente.
Méthode 1 :Itertuples
Une recherche rapide sur StackExchange montre que `itertuples` est la méthode qui est généralement utilisée lorsqu'on pose une question sur l'itération dans un cadre de données. Cette méthode est plus rapide que `iterrows` car sous le capot, pandas convertit chaque ligne en un tuple au lieu d'une série pandas, ce qui rend l'accès aux données beaucoup plus rapide. J'ai également utilisé une astuce que j'ai lue quelque part qui était de mettre `name=None` afin de créer des tuples sans nom (ce qui a été montré pour fournir un gain de vitesse).
def via_iter(long_data): acc_size = 100000 comm = 1.20 pcr = 0.27 tgt_return = 0.20 size = [acc_size] pnl = [(long_data["Open"][0]+long_data["Close"][0])*100*(acc_size * tgt_return / pcr / 252 / 100 / long_data["Open"][0]) - (comm*2)] for row in long_data[1:].itertuples(name=None): size.append(size[row[0]-1]+pnl[row[0]-1]) pnl.append((long_data.at[row[0]-1, 'Open']+long_data.at[row[0]-1, 'Close'])*100*(size[row[0]] * tgt_return / pcr / 252 / 100 / long_data.at[row[0]-1, 'Open']) - (comm*2)) return pnl, size
Méthode 2 : Itérer sur des tableaux
Ici, nous nous débarrassons de l’encombrant dataframe de pandas et utilisons à la place un tableau numpy. Malheureusement, en raison de la manière dont je devais effectuer le backtest, la vectorisation n’était pas une option et il n’y avait pas non plus d’ufuncs numpy que je pouvais utiliser. La bonne vieille boucle for devra faire l’affaire.
def arr_no_numba(calc_data): acc_size = 100000 comm = 1.20 pcr = 0.27 tgt_return = 0.20 calc_data[0][2] = acc_size calc_data[0][3] = (calc_data[0][0]+calc_data[0][1])*100*(acc_size * tgt_return / pcr / 252 / 100 / calc_data[0][0]) - (comm*2) for idx in range(1, len(calc_data)): calc_data[idx][2] = calc_data[idx-1][2] + calc_data[idx-1][3] calc_data[idx][3] = (calc_data[idx-1][0]+calc_data[idx-1][1])*100*(calc_data[idx][2] * tgt_return / pcr / 252 / 100 / calc_data[idx-1][0]) - (comm*2) return calc_data
Method 3: Numba
Enfin, on ajoute numba à la boucle for du tableau précédent. Numba accélère le code Python en traduisant essentiellement certains codes Python et Numpy en code machine ( !!) au moment de l’exécution. Cela lui permet d’atteindre des vitesses significativement plus élevées. En prime, il est très simple à utiliser – il suffit d’ajouter le décorateur @numba.jit
et le tour est joué !
import numba @numba.jit(nopython=True) def via_numba(calc_data): acc_size = 100000 comm = 1.20 pcr = 0.27 tgt_return = 0.20 calc_data[0][2] = acc_size calc_data[0][3] = (calc_data[0][0]+calc_data[0][1])*100*(acc_size * tgt_return / pcr / 252 / 100 / calc_data[0][0]) - (comm*2) for idx in range(1, len(calc_data)): calc_data[idx][2] = calc_data[idx-1][2] + calc_data[idx-1][3] calc_data[idx][3] = (calc_data[idx-1][0]+calc_data[idx-1][1])*100*(calc_data[idx][2] * tgt_return / pcr / 252 / 100 / calc_data[idx-1][0]) - (comm*2) return calc_data
Tester le temps
Pour utiliser les fonctions array et Numba, on a dû convertir le dataframe en array.
arr = long_data[["Open","Close"]].astype(float).to_numpy() input_arr = np.zeros((arr.shape[0],arr.shape[1]+2), dtype=float) input_arr[:,:-2] = arr
Ensuite on lance un `timeit` sur chacune des fonctions. Notez que l’on a éxecuté que 100 boucles pour les fonctions non-numériques parce que 1000 boucles prenaient beaucoup trop de temps…
iter_time = %timeit -o -n 100 via_iter(long_data) arr_time = %timeit -o -n 100 arr_no_numba(input_arr) numba_time = %timeit -o -n 1000 via_numba(input_arr)
39.6 ms ± 937 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) 11 ms ± 213 µs per loop (mean ± std. dev. of 7 runs, 100 loops each) L'exécution la plus lente a pris 4,44 fois plus de temps que la plus rapide. Cela pourrait signifier qu'un résultat intermédiaire est mis en cache. 112 µs ± 83.7 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
Les résultats étaient assez clairs étant donné que les unités étaient complètement différentes pour les trois tests. Voici quelques graphiques pour montrer les vitesses brutes :
import plotly.express as px px.bar(y=[iter_time.average*1000, arr_time.average*1000, numba_time.average*1000], x = ["Itertuples", "Numpy Array", "Numba"], labels={"x":"Iteration Method", "y":" Time (en millisecondes)"})
Et un graphique beaucoup plus clair montrant le gain de vitesse obtenu en utilisant Numba :
px.bar(y=[iter_time.average/numba_time.average, arr_time.average/numba_time.average], x = ["Numba > Itertuples", "Numba > Numpy Array"], labels={"x":"Iteration Method", "y":" Speed Gain"})
Conclusion
Numba semble sortir la concurrence de l’eau – ainsi, lorsque vous avez besoin d’itérer à travers un cadre de données dans un contexte de backtesting où vous ne pouvez pas vectoriser, utilisez des tableaux numpy, puis ajoutez le décorateur `@numba.jit` et vous passerez à travers ces itérations.
Merci pour votre lecture ! Si vous souhaitez lire nos prochains articles autour de la Data Science, vous pouvez nous suivre sur Facebook, LinkedIn et Twitter pour être notifié lorsqu’un nouvel article est publié !
Cet article a été inspiré par : A 300x speed boost when iterating data? Yes please! de Zachary Lim
,””).astype(float)
long_data[“Close”] = long_data[“Close”].str.replace(‘Il n’y a pas grand-chose à voir ici – il s’agit simplement de nettoyer un peu les données en supprimant les signes de dollar et en convertissant tout en valeurs flottantes.
La principale chose que nous testerons sera d’itérer sur la trame de données et les transactions ouvertes et fermées, en calculant le PnL et le changement résultant de la taille du compte. La taille du compte détermine alors la taille du lot de la transaction suivante – la vectorisation n’est donc pas possible puisque le PnL de chaque transaction ultérieure dépend de la taille du lot qui dépend de la modification de la taille du compte de la transaction précédente.
Méthode 1 :Itertuples
Une recherche rapide sur StackExchange montre que `itertuples` est la méthode qui est généralement utilisée lorsqu’on pose une question sur l’itération dans un cadre de données. Cette méthode est plus rapide que `iterrows` car sous le capot, pandas convertit chaque ligne en un tuple au lieu d’une série pandas, ce qui rend l’accès aux données beaucoup plus rapide. J’ai également utilisé une astuce que j’ai lue quelque part qui était de mettre `name=None` afin de créer des tuples sans nom (ce qui a été montré pour fournir un gain de vitesse).
Méthode 2 : Itérer sur des tableaux
Ici, nous nous débarrassons de l’encombrant dataframe de pandas et utilisons à la place un tableau numpy. Malheureusement, en raison de la manière dont je devais effectuer le backtest, la vectorisation n’était pas une option et il n’y avait pas non plus d’ufuncs numpy que je pouvais utiliser. La bonne vieille boucle for devra faire l’affaire.
Method 3: Numba
Enfin, on ajoute numba à la boucle for du tableau précédent. Numba accélère le code Python en traduisant essentiellement certains codes Python et Numpy en code machine ( !!) au moment de l’exécution. Cela lui permet d’atteindre des vitesses significativement plus élevées. En prime, il est très simple à utiliser – il suffit d’ajouter le décorateur @numba.jit
et le tour est joué !
Tester le temps
Pour utiliser les fonctions array et Numba, on a dû convertir le dataframe en array.
Ensuite on lance un `timeit` sur chacune des fonctions. Notez que l’on a éxecuté que 100 boucles pour les fonctions non-numériques parce que 1000 boucles prenaient beaucoup trop de temps…
Les résultats étaient assez clairs étant donné que les unités étaient complètement différentes pour les trois tests. Voici quelques graphiques pour montrer les vitesses brutes :
Et un graphique beaucoup plus clair montrant le gain de vitesse obtenu en utilisant Numba :
Conclusion
Numba semble sortir la concurrence de l’eau – ainsi, lorsque vous avez besoin d’itérer à travers un cadre de données dans un contexte de backtesting où vous ne pouvez pas vectoriser, utilisez des tableaux numpy, puis ajoutez le décorateur `@numba.jit` et vous passerez à travers ces itérations.
Merci pour votre lecture ! Si vous souhaitez lire nos prochains articles autour de la Data Science, vous pouvez nous suivre sur Facebook, LinkedIn et Twitter pour être notifié lorsqu’un nouvel article est publié !
Cet article a été inspiré par : A 300x speed boost when iterating data? Yes please! de Zachary Lim
,””).astype(float)
Il n’y a pas grand-chose à voir ici – il s’agit simplement de nettoyer un peu les données en supprimant les signes de dollar et en convertissant tout en valeurs flottantes.
La principale chose que nous testerons sera d’itérer sur la trame de données et les transactions ouvertes et fermées, en calculant le PnL et le changement résultant de la taille du compte. La taille du compte détermine alors la taille du lot de la transaction suivante – la vectorisation n’est donc pas possible puisque le PnL de chaque transaction ultérieure dépend de la taille du lot qui dépend de la modification de la taille du compte de la transaction précédente.
Méthode 1 :Itertuples
Une recherche rapide sur StackExchange montre que `itertuples` est la méthode qui est généralement utilisée lorsqu’on pose une question sur l’itération dans un cadre de données. Cette méthode est plus rapide que `iterrows` car sous le capot, pandas convertit chaque ligne en un tuple au lieu d’une série pandas, ce qui rend l’accès aux données beaucoup plus rapide. J’ai également utilisé une astuce que j’ai lue quelque part qui était de mettre `name=None` afin de créer des tuples sans nom (ce qui a été montré pour fournir un gain de vitesse).
Méthode 2 : Itérer sur des tableaux
Ici, nous nous débarrassons de l’encombrant dataframe de pandas et utilisons à la place un tableau numpy. Malheureusement, en raison de la manière dont je devais effectuer le backtest, la vectorisation n’était pas une option et il n’y avait pas non plus d’ufuncs numpy que je pouvais utiliser. La bonne vieille boucle for devra faire l’affaire.
Method 3: Numba
Enfin, on ajoute numba à la boucle for du tableau précédent. Numba accélère le code Python en traduisant essentiellement certains codes Python et Numpy en code machine ( !!) au moment de l’exécution. Cela lui permet d’atteindre des vitesses significativement plus élevées. En prime, il est très simple à utiliser – il suffit d’ajouter le décorateur @numba.jit
et le tour est joué !
Tester le temps
Pour utiliser les fonctions array et Numba, on a dû convertir le dataframe en array.
Ensuite on lance un `timeit` sur chacune des fonctions. Notez que l’on a éxecuté que 100 boucles pour les fonctions non-numériques parce que 1000 boucles prenaient beaucoup trop de temps…
Les résultats étaient assez clairs étant donné que les unités étaient complètement différentes pour les trois tests. Voici quelques graphiques pour montrer les vitesses brutes :
Et un graphique beaucoup plus clair montrant le gain de vitesse obtenu en utilisant Numba :
Conclusion
Numba semble sortir la concurrence de l’eau – ainsi, lorsque vous avez besoin d’itérer à travers un cadre de données dans un contexte de backtesting où vous ne pouvez pas vectoriser, utilisez des tableaux numpy, puis ajoutez le décorateur `@numba.jit` et vous passerez à travers ces itérations.
Merci pour votre lecture ! Si vous souhaitez lire nos prochains articles autour de la Data Science, vous pouvez nous suivre sur Facebook, LinkedIn et Twitter pour être notifié lorsqu’un nouvel article est publié !
Cet article a été inspiré par : A 300x speed boost when iterating data? Yes please! de Zachary Lim