web-dev-qa-db-fra.com

Pandas df.iterrow () parallélisation

Je voudrais paralléliser le code suivant:

for row in df.iterrow():
    idx = row[0]
    k = row[1]['Chromosome']
    start,end = row[1]['Bin'].split('-')

    sequence = sequence_from_coordinates(k,1,start,end) #slow download form http

    df.set_value(idx,'GC%',gc_content(sequence,percent=False,verbose=False))
    df.set_value(idx,'G4 repeats', sum([len(list(i)) for i in g4_scanner(sequence)]))
    df.set_value(idx,'max flexibility',max([item[1] for item in dna_flex(sequence,verbose=False)]))

J'ai essayé d'utiliser multiprocessing.Pool() car chaque ligne peut être traitée indépendamment, mais je ne vois pas comment partager le DataFrame. Je ne suis pas sûr non plus que ce soit la meilleure approche pour faire la parallélisation avec les pandas. De l'aide?

12
alec_djinn

Comme @Khris l'a dit dans son commentaire, vous devez diviser votre cadre de données en quelques gros morceaux et effectuer une itération parallèle sur chaque morceau. Vous pouvez scinder arbitrairement le cadre de données en fragments de taille aléatoire, mais il est plus logique de diviser le cadre de données en fragments de taille égale en fonction du nombre de processus que vous envisagez d'utiliser. Heureusement, quelqu'un d'autre a déjà compris comment faire cette partie pour nous:

# don't forget to import
import pandas as pd
import multiprocessing

# create as many processes as there are CPUs on your machine
num_processes = multiprocessing.cpu_count()

# calculate the chunk size as an integer
chunk_size = int(df.shape[0]/num_processes)

# this solution was reworked from the above link.
# will work even if the length of the dataframe is not evenly divisible by num_processes
chunks = [df.ix[df.index[i:i + chunk_size]] for i in range(0, df.shape[0], chunk_size)]

Cela crée une liste contenant notre image de données en morceaux. Nous devons maintenant le transférer dans notre pool avec une fonction permettant de manipuler les données.

def func(d):
   # let's create a function that squares every value in the dataframe
   return d * d

# create our pool with `num_processes` processes
pool = multiprocessing.Pool(processes=num_processes)

# apply our function to each chunk in the list
result = pool.map(func, chunks)

À ce stade, result sera une liste contenant chaque bloc après sa manipulation. Dans ce cas, toutes les valeurs ont été mises au carré. Le problème est maintenant que la base de données d'origine n'a pas été modifiée. Nous devons donc remplacer toutes ses valeurs existantes par les résultats de notre pool.

for i in range(len(result)):
   # since result[i] is just a dataframe
   # we can reassign the original dataframe based on the index of each chunk
   df.ix[result[i].index] = result[i]

Maintenant, ma fonction de manipuler mon cadre de données est vectorisée et aurait probablement été plus rapide si je l'avais simplement appliquée à l'ensemble de mon cadre de données au lieu de la scinder en gros morceaux. Cependant, dans votre cas, votre fonction itérerait sur chaque ligne de chaque bloc, puis renverrait le bloc. Cela vous permet de traiter des lignes num_process à la fois.

def func(d):
   for row in d.iterrow():
      idx = row[0]
      k = row[1]['Chromosome']
      start,end = row[1]['Bin'].split('-')

      sequence = sequence_from_coordinates(k,1,start,end) #slow download form http
      d.set_value(idx,'GC%',gc_content(sequence,percent=False,verbose=False))
      d.set_value(idx,'G4 repeats', sum([len(list(i)) for i in g4_scanner(sequence)]))
      d.set_value(idx,'max flexibility',max([item[1] for item in dna_flex(sequence,verbose=False)]))
   # return the chunk!
   return d

Ensuite, vous réaffectez les valeurs dans la trame de données d'origine et vous avez parallélisé ce processus.

Combien de processus dois-je utiliser?

Votre performance optimale dépendra de la réponse à cette question. Alors que "TOUS LES PROCESSUS !!!!" est une réponse, une meilleure réponse est beaucoup plus nuancée. Après un certain point, lancer plus de processus sur un problème crée en réalité plus de temps système qu'il n'en vaut. Ceci est connu sous le nom de loi d'Amdahl . Encore une fois, nous sommes chanceux que d’autres aient déjà abordé cette question pour nous: 

  1. Limite du processus de pool du multitraitement Python } _
  2. Combien de processus dois-je exécuter en parallèle?

Une bonne valeur par défaut consiste à utiliser multiprocessing.cpu_count(), qui correspond au comportement par défaut de multiprocessing.Pool. Selon la documentation "Si process est Aucun, le nombre renvoyé par cpu_count () est utilisé." C'est pourquoi j'ai défini num_processes au début sur multiprocessing.cpu_count(). De cette façon, si vous passez à une machine plus robuste, vous en retirerez les avantages sans avoir à changer directement la variable num_processes.

30
TheF1rstPancake

Un moyen plus rapide (environ 10% dans mon cas):

Principales différences par rapport à la réponse acceptée: Utilisez pd.concat et np.array_split pour scinder et joindre le bloc de données.

import multiprocessing
import numpy as np


def parallelize_dataframe(df, func):
    num_cores = multiprocessing.cpu_count()-1  #leave one free to not freeze machine
    num_partitions = num_cores #number of partitions to split dataframe
    df_split = np.array_split(df, num_partitions)
    pool = multiprocessing.Pool(num_cores)
    df = pd.concat(pool.map(func, df_split))
    pool.close()
    pool.join()
    return df

func est la fonction à laquelle vous souhaitez appliquer df. Utilisez partial(func, arg=arg_val) pour plus d'un argument. 

10
ic_fl2

Pensez à utiliser dask.dataframe, par exemple indiqué dans cet exemple pour une question similaire: https://stackoverflow.com/a/53923034/4340584

import dask.dataframe as ddf
df_dask = ddf.from_pandas(df, npartitions=4)   # where the number of partitions is the number of cores you want to use
df_dask['output'] = df_dask.apply(lambda x: your_function(x), meta=('str')).compute(scheduler='multiprocessing')
1
Robert