Ефективне паралельне застосування функції до згрупованих панд DataFrame


89

Мені часто потрібно застосовувати функцію до груп дуже великих DataFrame(змішаних типів даних), і я хотів би скористатися перевагами декількох ядер.

Я можу створити ітератор з груп і використовувати багатопроцесорний модуль, але це не ефективно, оскільки кожна група та результати функції повинні бути замариновані для обміну повідомленнями між процесами.

Чи є спосіб уникнути травлення або навіть уникнути копіювання DataFrameповністю? Схоже, функції спільної пам'яті багатопроцесорних модулів обмежені numpyмасивами. Чи є інші варіанти?


Наскільки мені відомо, немає можливості ділитися довільними об’єктами. Мені цікаво, якщо травлення займає набагато більше часу, ніж виграш від багатопроцесорної обробки. Можливо, вам слід шукати можливість створити більші робочі пакети для кожного процесу, щоб зменшити відносний час травлення. Іншою можливістю було б використання багатопроцесорної обробки під час створення груп.
Себастьян Верк,

3
Я роблю щось подібне, але за допомогою UWSGI, Flask та попереднього форкінгу: я завантажую кадр даних pandas у процес, розгалужую його x разів (роблячи його об'єктом спільної пам'яті), а потім викликаю ці процеси з іншого процесу python, де я конкатую результати. Банкомат Я використовую JSON як процес спілкування, але це наближається (але все ще дуже експериментально): pandas.pydata.org/pandas-docs/dev/io.html#msgpack-experimental
Carst

До речі, ти коли-небудь дивився на HDF5 з кусками? (HDF5 не зберігається для одночасного запису, але ви також можете зберігати для окремих файлів і, зрештою,
об'єднуйте

7
це буде націлено на 0,14, див. цю проблему: github.com/pydata/pandas/issues/5751
Джефф

4
@Jeff був відсунутий до 0,15 = (
pyCthon

Відповіді:


12

З коментарів вище, здається, це планується на pandasякийсь час (є також цікавий на вигляд rosettaпроект, який я щойно помітив).

Однак, поки не включена кожна паралельна функціональність pandas, я помітив, що дуже легко писати ефективні паралельні доповнення, що не копіюють пам'ять, pandasбезпосередньо за допомогою cython+ OpenMP і C ++.

Ось короткий приклад написання паралельної групи за допомогою суми, використання якої приблизно таке:

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)

і вихідний результат:

     sum
key     
0      6
1      11
2      4

Зауважте, без сумніву, функціональність цього простого прикладу з часом стане частиною pandas. Однак деякі речі буде природнішим розпаралелювати в C ++ протягом деякого часу, і важливо знати, наскільки легко це поєднати pandas.


Для цього я написав просте розширення файлу з одним джерелом, код якого слід.

Починається з деяких імпорту та визначень типів

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

Тип C ++ unordered_mapпризначений для підсумовування за одним потоком, а тип vector- для підсумовування за всіма потоками.

Тепер до функції sum. Починається з набраних переглядів пам'яті для швидкого доступу:

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

Функція триває, діливши напіврівно на потоки (тут жорстко закодовані до 4), і кожен потік підсумовує записи в своєму діапазоні:

    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)

Після завершення потоків функція об’єднує всі результати (з різних діапазонів) в один 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)        

Залишилося лише створити DataFrameта повернути результати:

    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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.