J'ai souvent besoin d'appliquer une fonction aux groupes d'un très grand DataFrame
(de types de données mixtes) et souhaitez tirer parti de plusieurs cœurs.
Je peux créer un itérateur à partir des groupes et utiliser le module multiprocessionnant, mais il n'est pas efficace car chaque groupe et les résultats de la fonction doivent être marinés pour la messagerie entre les processus.
Y a-t-il un moyen d'éviter le décapage ou même d'éviter la copie du DataFrame
complètement? On dirait que les fonctions de mémoire partagées des modules multiprocessionnaires sont limitées aux tableaux numpy
. Il y a-t-il des alternatives?
Des commentaires ci-dessus, il semble que cela soit prévu pour pandas
quelque temps (il existe également un projet intéressant rosetta
projet que je viens de remarquer).
Cependant, jusqu'à ce que chaque fonctionnalité parallèle soit intégrée à pandas
_, j'ai remarqué qu'il est très facile d'écrire des augmentations parallèles efficaces et non à la copie de mémoire à pandas
directement en utilisant cython
+ openmp et c ++.
Voici un court exemple d'écriture d'une somme parallèle à la somme, dont la consommation est quelque chose comme ceci:
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)
et la sortie est:
sum
key
0 6
1 11
2 4
Remarque sans doute, la fonctionnalité de cet exemple simple fera finalement partie de pandas
. Certaines choses, cependant, seront plus naturelles à paralléliser en C++ pendant un certain temps, et il est important de savoir à quel point il est facile de le combiner dans pandas
.
Pour ce faire, j'ai écrit une simple extension de fichier à source unique dont le code suit.
Il commence par certaines importations et définitions de type
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
Le C++ unordered_map
Type est pour la somme de sommation par un seul thread, et le vector
est destiné à la somme de tous les threads.
Maintenant à la fonction sum
. Il démarre avec Vues de mémoire tapé pour un accès rapide:
def sum(crit, vals):
cdef int64_t[:] crit_view = crit.values
cdef int64_t[:] vals_view = vals.values
La fonction se poursuit en divisant les semi-égales aux fils (ici codé en dur à 4) et que chaque thread somme les entrées dans sa plage:
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)
Lorsque les threads sont terminés, la fonction fusionne tous les résultats (à partir des différentes gammes) en un seul 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)
Tout ce qui reste est de créer un DataFrame
et renvoyer les résultats:
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