web-dev-qa-db-ja.com

グループ化されたpandas DataFrameに効率的に関数を適用する

非常に大きな(混合データ型の)DataFrameのグループに関数を適用する必要があり、複数のコアを利用したい場合があります。

グループからイテレータを作成してマルチプロセッシングモジュールを使用できますが、すべてのグループと関数の結果をプロセス間のメッセージングのためにピクルする必要があるため、効率的ではありません。

酸洗いを回避したり、DataFrameのコピーを完全に回避したりする方法はありますか?マルチプロセッシングモジュールの共有メモリ関数はnumpy配列に制限されているようです。他のオプションはありますか?

88
user2303

上記のコメントから、これはpandasで計画されているようです(興味深い外観の rosettaプロジェクト もありました)。

ただし、すべての並列機能がpandasに組み込まれるまで、 pandas + OpenMP を使用してcythonに効率的でメモリをコピーしない並列拡張を直接書き込むのは非常に簡単であることに気付きましたC++。

並列groupby-sumを作成する短い例を次に示します。この使用法は次のようなものです。

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)

スレッドが完了すると、関数は(異なる範囲からの)すべての結果を1つの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
12
Ami Tavory