web-dev-qa-db-fra.com

Comment paralléliser apply () sur Pandas Dataframes utilisant tous les cœurs d'une machine?

Depuis août 2017, Pandas DataFame.apply () est malheureusement toujours limité au travail sur un seul cœur, ce qui signifie qu'une machine multicœur va gaspiller la majorité de ses ressources informatiques. heure à laquelle vous exécutez df.apply(myfunc, axis=1).

Comment pouvez-vous utiliser tous vos cœurs pour exécuter une application sur un cadre de données en parallèle?

57
Roko Mijic

Le moyen le plus simple est d'utiliser map_partitions de Dask . Vous avez besoin de ces importations (vous aurez besoin de pip install dask):

import pandas as pd
import dask.dataframe as dd
from dask.multiprocessing import get

et la syntaxe est

data = <your_pandas_dataframe>
ddata = dd.from_pandas(data, npartitions=30)

def myfunc(x,y,z, ...): return <whatever>

res = ddata.map_partitions(lambda df: df.apply((lambda row: myfunc(*row)), axis=1)).compute(get=get)  

(Je pense que 30 est un nombre approprié de partitions si vous avez 16 cœurs). Juste pour être complet, j'ai chronométré la différence sur ma machine (16 cœurs):

data = pd.DataFrame()
data['col1'] = np.random.normal(size = 1500000)
data['col2'] = np.random.normal(size = 1500000)

ddata = dd.from_pandas(data, npartitions=30)
def myfunc(x,y): return y*(x**2+1)
def apply_myfunc_to_DF(df): return df.apply((lambda row: myfunc(*row)), axis=1)
def pandas_apply(): return apply_myfunc_to_DF(data)
def dask_apply(): return ddata.map_partitions(apply_myfunc_to_DF).compute(get=get)  
def vectorized(): return myfunc(data['col1'], data['col2']  )

t_pds = timeit.Timer(lambda: pandas_apply())
print(t_pds.timeit(number=1))

28.16970546543598

t_dsk = timeit.Timer(lambda: dask_apply())
print(t_dsk.timeit(number=1))

2.708152851089835

t_vec = timeit.Timer(lambda: vectorized())
print(t_vec.timeit(number=1))

0.010668013244867325

Donner un facteur de 10 accélération à partir de pandas s'applique à dask s’applique aux partitions. Bien sûr, si vous avez une fonction que vous pouvez vectoriser, vous devriez - dans ce cas, la fonction (y*(x**2+1)) est vectorisée de manière triviale, mais de nombreuses choses sont impossibles à vectoriser.

70
Roko Mijic

Vous pouvez utiliser le package swifter :

pip install swifter

Il fonctionne comme un plugin pour les pandas, vous permettant de réutiliser la fonction apply:

import swifter

def some_function(data):
    return data * 10

data['out'] = data['in'].swifter.apply(some_function)

Il trouvera automatiquement le moyen le plus efficace de paralléliser la fonction, qu'elle soit vectorisée (comme dans l'exemple ci-dessus) ou non.

Autres exemples et un comparaison des performances sont disponibles sur GitHub. Notez que le package est en cours de développement, l’API peut donc changer.

39
slhck

vous pouvez plutôt essayer pandarallel: un outil simple et efficace pour paralléliser vos opérations pandas sur tous vos processeurs (Sous Linux et macOS)

  • La parallélisation a un coût (lancement de nouveaux processus, envoi de données via une mémoire partagée, etc.), la parallélisation n’est efficace que si le nombre de calculs à paralléliser est suffisamment élevé. Pour une très petite quantité de données, l’utilisation de la parallezation n’en vaut pas toujours la peine.
  • Les fonctions appliquées ne doivent PAS être des fonctions lambda.
from pandarallel import pandarallel
from math import sin

pandarallel.initialize()

# FORBIDDEN
df.parallel_apply(lambda x: sin(x**2), axis=1)

# ALLOWED
def func(x):
    return sin(x**2)

df.parallel_apply(func, axis=1)

voir https://github.com/nalepae/pandarallel

10
G_KOBELIEF

Si vous voulez rester en python natif:

import multiprocessing as mp

pool = mp.Pool(mp.cpu_count())
df['newcol'] = pool.map(f, df['col'])
pool.terminate()
pool.join()

appliquera la fonction f de manière parallèle à la colonne col du cadre de données df

2
Olivier_Cruchant

Voici un exemple de transformateur de base Sklearn, dans lequel pandas apply est parallélisé

import multiprocessing as mp
from sklearn.base import TransformerMixin, BaseEstimator

class ParllelTransformer(BaseEstimator, TransformerMixin):
    def __init__(self,
                 n_jobs=1):
        """
        n_jobs - parallel jobs to run
        """
        self.variety = variety
        self.user_abbrevs = user_abbrevs
        self.n_jobs = n_jobs
    def fit(self, X, y=None):
        return self
    def transform(self, X, *_):
        X_copy = X.copy()
        cores = mp.cpu_count()
        partitions = 1

        if self.n_jobs <= -1:
            partitions = cores
        Elif self.n_jobs <= 0:
            partitions = 1
        else:
            partitions = min(self.n_jobs, cores)

        if partitions == 1:
            # transform sequentially
            return X_copy.apply(self._transform_one)

        # splitting data into batches
        data_split = np.array_split(X_copy, partitions)

        pool = mp.Pool(cores)

        # Here reduce function - concationation of transformed batches
        data = pd.concat(
            pool.map(self._preprocess_part, data_split)
        )

        pool.close()
        pool.join()
        return data
    def _transform_part(self, df_part):
        return df_part.apply(self._transform_one)
    def _transform_one(self, line):
        # some kind of transformations here
        return line

pour plus d'informations, voir https://towardsdatascience.com/4-easy-steps-to-improve-your-machine-learning-code-performance-88a0b0eeffa8

1
Maxim Balatsko