Applicazione efficiente di una funzione a un DataFrame panda raggruppato in parallelo


89

Spesso ho bisogno di applicare una funzione ai gruppi di un molto grande DataFrame(di tipi di dati misti) e vorrei sfruttare più core.

Posso creare un iteratore dai gruppi e utilizzare il modulo multiprocessing, ma non è efficiente perché ogni gruppo e i risultati della funzione devono essere selezionati per la messaggistica tra i processi.

Esiste un modo per evitare il decapaggio o addirittura evitare la copia del DataFrametutto? Sembra che le funzioni di memoria condivisa dei moduli multiprocessing siano limitate agli numpyarray. Ci sono altre opzioni?


Per quanto ne so, non c'è modo di condividere oggetti arbitrari. Mi chiedo se il decapaggio richieda molto più tempo rispetto al guadagno con il multiprocessing. Forse dovresti cercare la possibilità di creare pacchetti di lavoro più grandi per ogni processo per ridurre il tempo di decapaggio relativo. Un'altra possibilità potrebbe essere quella di utilizzare il multiprocessing quando crei i gruppi.
Sebastian Werk,

3
Faccio qualcosa del genere ma usando UWSGI, Flask e preforking: carico il dataframe pandas in un processo, lo forco x volte (rendendolo un oggetto di memoria condivisa) e poi chiamo quei processi da un altro processo python dove concatizzo i risultati. atm Uso JSON come processo di comunicazione, ma questo sta arrivando (ma ancora altamente sperimentale): pandas.pydata.org/pandas-docs/dev/io.html#msgpack-experimental
Carst

A proposito, hai mai guardato HDF5 con il chunking? (HDF5 non è salvo per la scrittura simultanea, ma puoi anche salvare su file separati e alla fine concatenare cose)
Carst

7
questo sarà mirato a 0,14, vedere questo problema: github.com/pydata/pandas/issues/5751
Jeff

4
@Jeff è stato spinto a 0,15 = (
pyCthon

Risposte:


12

Dai commenti sopra, sembra che questo sia pianificato da pandastempo (c'è anche un rosettaprogetto dall'aspetto interessante che ho appena notato).

Tuttavia, fino a quando non viene incorporata ogni funzionalità parallela pandas, ho notato che è molto facile scrivere aumenti paralleli efficienti e non copianti in memoria pandasutilizzando direttamente cython+ OpenMP e C ++.

Ecco un breve esempio di scrittura di un groupby-sum parallelo, il cui utilizzo è qualcosa del genere:

import pandas as pd
import para_group_demo

df = pd.DataFrame({'a': [1, 2, 1, 2, 1, 1, 0], 'b': range(7)})
print para_group_demo.sum(df.a, df.b)

e l'output è:

     sum
key     
0      6
1      11
2      4

Nota Senza dubbio, la funzionalità di questo semplice esempio finirà per far parte di pandas. Alcune cose, tuttavia, saranno più naturali da parallelizzare in C ++ per qualche tempo ed è importante essere consapevoli di quanto sia facile combinarle in pandas.


Per fare questo, ho scritto una semplice estensione di file a sorgente singola il cui codice segue.

Inizia con alcune importazioni e definizioni di tipo

from libc.stdint cimport int64_t, uint64_t
from libcpp.vector cimport vector
from libcpp.unordered_map cimport unordered_map

cimport cython
from cython.operator cimport dereference as deref, preincrement as inc
from cython.parallel import prange

import pandas as pd

ctypedef unordered_map[int64_t, uint64_t] counts_t
ctypedef unordered_map[int64_t, uint64_t].iterator counts_it_t
ctypedef vector[counts_t] counts_vec_t

Il unordered_maptipo C ++ è per la somma da un singolo thread e vectorper la somma da tutti i thread.

Ora alla funzione sum. Inizia con le visualizzazioni della memoria digitata per un accesso rapido:

def sum(crit, vals):
    cdef int64_t[:] crit_view = crit.values
    cdef int64_t[:] vals_view = vals.values

La funzione continua dividendo il semi-equamente per i thread (qui hardcoded a 4) e facendo in modo che ogni thread somma le voci nel suo intervallo:

    cdef uint64_t num_threads = 4
    cdef uint64_t l = len(crit)
    cdef uint64_t s = l / num_threads + 1
    cdef uint64_t i, j, e
    cdef counts_vec_t counts
    counts = counts_vec_t(num_threads)
    counts.resize(num_threads)
    with cython.boundscheck(False):
        for i in prange(num_threads, nogil=True): 
            j = i * s
            e = j + s
            if e > l:
                e = l
            while j < e:
                counts[i][crit_view[j]] += vals_view[j]
                inc(j)

Quando i thread sono stati completati, la funzione unisce tutti i risultati (dai diversi intervalli) in un unico unordered_map:

    cdef counts_t total
    cdef counts_it_t it, e_it
    for i in range(num_threads):
        it = counts[i].begin()
        e_it = counts[i].end()
        while it != e_it:
            total[deref(it).first] += deref(it).second
            inc(it)        

Non resta che creare un DataFramee restituire i risultati:

    key, sum_ = [], []
    it = total.begin()
    e_it = total.end()
    while it != e_it:
        key.append(deref(it).first)
        sum_.append(deref(it).second)
        inc(it)

    df = pd.DataFrame({'key': key, 'sum': sum_})
    df.set_index('key', inplace=True)
    return df
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.