J'ai utilisé rosetta.parallel.pandas_easy pour paralléliser, appliquer après groupe, par exemple:
from rosetta.parallel.pandas_easy import groupby_to_series_to_frame
df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
groupby_to_series_to_frame(df, np.mean, n_jobs=8, use_apply=True, by=df.index)
Cependant, quelqu'un a-t-il trouvé le moyen de mettre en parallèle une fonction qui renvoie un cadre de données? Ce code échoue pour rosetta, comme prévu.
def tmpFunc(df):
df['c'] = df.a + df.b
return df
df.groupby(df.index).apply(tmpFunc)
groupby_to_series_to_frame(df, tmpFunc, n_jobs=1, use_apply=True, by=df.index)
Cela semble fonctionner, même si cela devrait vraiment être intégré aux pandas
import pandas as pd
from joblib import Parallel, delayed
import multiprocessing
def tmpFunc(df):
df['c'] = df.a + df.b
return df
def applyParallel(dfGrouped, func):
retLst = Parallel(n_jobs=multiprocessing.cpu_count())(delayed(func)(group) for name, group in dfGrouped)
return pd.concat(retLst)
if __== '__main__':
df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
print 'parallel version: '
print applyParallel(df.groupby(df.index), tmpFunc)
print 'regular version: '
print df.groupby(df.index).apply(tmpFunc)
print 'ideal version (does not work): '
print df.groupby(df.index).applyParallel(tmpFunc)
La réponse d'Ivan est excellente, mais il semble que cela puisse être légèrement simplifié, éliminant également la nécessité de dépendre de joblib:
from multiprocessing import Pool, cpu_count
def applyParallel(dfGrouped, func):
with Pool(cpu_count()) as p:
ret_list = p.map(func, [group for name, group in dfGrouped])
return pandas.concat(ret_list)
Au fait: cela ne peut pas remplacer any groupby.apply (), mais cela couvrira les cas typiques: par ex. il devrait couvrir les cas 2 et 3 de la documentation , tandis que vous devriez obtenir le comportement du cas 1 en donnant l’argument axis=1
à l’appel final pandas.concat()
.
J'ai un bidouillage que j'utilise pour obtenir la parallélisation dans les pandas. Je décompose mon bloc de données en morceaux, je place chaque morceau dans l'élément d'une liste, puis j'utilise les bits parallèles d'ipython pour appliquer en parallèle la liste des cadres de données. Puis je remets la liste en place en utilisant la fonction pandas concat
.
Ce n'est pas généralement applicable, cependant. Cela fonctionne pour moi parce que la fonction que je veux appliquer à chaque bloc de la trame de données prend environ une minute. Et la séparation et la compilation de mes données ne prend pas beaucoup de temps. Donc, ceci est clairement un kludge. Cela dit, voici un exemple. J'utilise Ipython notebook pour que vous voyiez %%time
magic dans mon code:
## make some example data
import pandas as pd
np.random.seed(1)
n=10000
df = pd.DataFrame({'mygroup' : np.random.randint(1000, size=n),
'data' : np.random.Rand(n)})
grouped = df.groupby('mygroup')
Pour cet exemple, je vais créer des «morceaux» en fonction du groupe ci-dessus, mais cela ne doit pas nécessairement correspondre à la façon dont les données sont fragmentées. Bien que ce soit un modèle assez commun.
dflist = []
for name, group in grouped:
dflist.append(group)
mettre en place les bits parallèles
from IPython.parallel import Client
rc = Client()
lview = rc.load_balanced_view()
lview.block = True
écrire une fonction idiote à appliquer à nos données
def myFunc(inDf):
inDf['newCol'] = inDf.data ** 10
return inDf
exécutons maintenant le code en série puis en parallèle. série en premier:
%%time
serial_list = map(myFunc, dflist)
CPU times: user 14 s, sys: 19.9 ms, total: 14 s
Wall time: 14 s
maintenant parallèle
%%time
parallel_list = lview.map(myFunc, dflist)
CPU times: user 1.46 s, sys: 86.9 ms, total: 1.54 s
Wall time: 1.56 s
alors il ne faut que quelques ms pour les fusionner en une seule base de données
%%time
combinedDf = pd.concat(parallel_list)
CPU times: user 296 ms, sys: 5.27 ms, total: 301 ms
Wall time: 300 ms
J'utilise 6 moteurs IPython sur mon MacBook, mais vous pouvez voir qu'il réduit le temps d'exécution à 14 secondes au lieu de 2 secondes.
Pour les simulations stochastiques très longues, je peux utiliser le backend AWS en activant un cluster avec StarCluster . La plupart du temps, cependant, je parallélise juste sur 8 processeurs sur mon MBP.
Un court commentaire pour accompagner la réponse de JD Long. J'ai constaté que si le nombre de groupes est très important (par exemple, des centaines de milliers) et que votre fonction apply fait quelque chose d'assez simple et rapide, divisez ensuite votre trame de données en blocs et affectez chaque bloc à un travailleur pour effectuer une tâche. groupby-apply (en série) peut être beaucoup plus rapide que de faire un groupe parallèle-apply et que les travailleurs lisent une file d'attente contenant une multitude de groupes. Exemple:
import pandas as pd
import numpy as np
import time
from concurrent.futures import ProcessPoolExecutor, as_completed
nrows = 15000
np.random.seed(1980)
df = pd.DataFrame({'a': np.random.permutation(np.arange(nrows))})
Donc, notre dataframe ressemble à:
a
0 3425
1 1016
2 8141
3 9263
4 8018
Notez que la colonne 'a' contient plusieurs groupes (pensez aux identifiants client):
len(df.a.unique())
15000
Une fonction pour opérer sur nos groupes:
def f1(group):
time.sleep(0.0001)
return group
Démarrer une piscine:
ppe = ProcessPoolExecutor(12)
futures = []
results = []
Faites un groupe parallèle en appliquant:
%%time
for name, group in df.groupby('a'):
p = ppe.submit(f1, group)
futures.append(p)
for future in as_completed(futures):
r = future.result()
results.append(r)
df_output = pd.concat(results)
del ppe
CPU times: user 18.8 s, sys: 2.15 s, total: 21 s
Wall time: 17.9 s
Ajoutons maintenant une colonne qui partitionne le df en beaucoup moins de groupes:
df['b'] = np.random.randint(0, 12, nrows)
Maintenant, au lieu de 15000 groupes, il n'y en a que 12:
len(df.b.unique())
12
Nous allons partitionner notre df et faire un groupby-apply sur chaque morceau.
ppe = ProcessPoolExecutor(12)
Wrapper fun:
def f2(df):
df.groupby('a').apply(f1)
return df
Envoyez chaque morceau à opérer en série:
%%time
for i in df.b.unique():
p = ppe.submit(f2, df[df.b==i])
futures.append(p)
for future in as_completed(futures):
r = future.result()
results.append(r)
df_output = pd.concat(results)
CPU times: user 11.4 s, sys: 176 ms, total: 11.5 s
Wall time: 12.4 s
Notez que le temps passé par groupe n'a pas changé. Ce qui a plutôt changé, c'est la longueur de la file d'attente à partir de laquelle les travailleurs ont lu. Je soupçonne que ce qui se passe, c’est que les travailleurs ne peuvent pas accéder simultanément à la mémoire partagée et reviennent constamment pour lire la file d’attente. Ils se marchent ainsi les uns sur les autres. Avec de plus gros morceaux à opérer, les ouvriers reviennent moins souvent et ainsi ce problème est amélioré et l'exécution globale est plus rapide.
Personnellement, je recommanderais d'utiliser dask, par ce fil .
Comme @chrisb l'a fait remarquer, le multitraitement avec des pandas en python peut créer une surcharge inutile. Il est également possible que not fonctionne aussi bien que le multithreading ou même comme un seul thread.
Dask est créé spécifiquement pour la multiprocession.