web-dev-qa-db-fra.com

Appliquer parallèlement après groupe de pandas

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)
39
Ivan

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)
69
Ivan

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().

35
Pietro Battiston

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. 

10
JD Long

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. 

3
spring

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. 

0
Jinhua Wang