web-dev-qa-db-ja.com

pandas groupbyの後に適用を並列化します

私はrosetta.parallel.pandas_easyを使用して、グループ化後に適用を並列化しました。例:

from rosetta.parallel.pandas_easy import groupby_to_series_to_frame
df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
groupby_to_series_to_frame(df, np.mean, n_jobs=8, use_apply=True, by=df.index)

しかし、誰かがデータフレームを返す関数を並列化する方法を見つけましたか?予想どおり、このコードはrosettaで失敗します。

def tmpFunc(df):
    df['c'] = df.a + df.b
    return df

df.groupby(df.index).apply(tmpFunc)
groupby_to_series_to_frame(df, tmpFunc, n_jobs=1, use_apply=True, by=df.index)
44
Ivan

これは実際に機能しているようですが、実際にはパンダに組み込む必要があります

import pandas as pd
from joblib import Parallel, delayed
import multiprocessing

def tmpFunc(df):
    df['c'] = df.a + df.b
    return df

def applyParallel(dfGrouped, func):
    retLst = Parallel(n_jobs=multiprocessing.cpu_count())(delayed(func)(group) for name, group in dfGrouped)
    return pd.concat(retLst)

if __== '__main__':
    df = pd.DataFrame({'a': [6, 2, 2], 'b': [4, 5, 6]},index= ['g1', 'g1', 'g2'])
    print 'parallel version: '
    print applyParallel(df.groupby(df.index), tmpFunc)

    print 'regular version: '
    print df.groupby(df.index).apply(tmpFunc)

    print 'ideal version (does not work): '
    print df.groupby(df.index).applyParallel(tmpFunc)
84
Ivan

Ivanの答えは素晴らしいですが、それはわずかに単純化できるように見え、joblibに依存する必要もなくなります。

_from multiprocessing import Pool, cpu_count

def applyParallel(dfGrouped, func):
    with Pool(cpu_count()) as p:
        ret_list = p.map(func, [group for name, group in dfGrouped])
    return pandas.concat(ret_list)
_

ちなみに、これはanygroupby.apply()を置き換えることはできませんが、典型的なケースをカバーします:ケース2と3をカバーする必要があります ドキュメント内 、最後のpandas.concat()呼び出しに引数_axis=1_を与えることでケース1の動作を取得する必要があります。

41

パンダで並列化を行うために使用するハックがあります。データフレームをチャンクに分割し、各チャンクをリストの要素に入れてから、ipythonのパラレルビットを使用してデータフレームのリストに並列適用します。次に、pandas concat関数を使用してリストを元に戻します。

ただし、これは一般的には適用されません。データフレームの各チャンクに適用したい関数には約1分かかるので、私にとってはうまくいきます。そして、データを引き離してまとめるのにそれほど時間はかかりません。だから、これは明らかにクラッジです。とはいえ、ここに例があります。 Ipythonノートブックを使用しているので、%%time私のコードの魔法:

## make some example data
import pandas as pd

np.random.seed(1)
n=10000
df = pd.DataFrame({'mygroup' : np.random.randint(1000, size=n), 
                   'data' : np.random.Rand(n)})
grouped = df.groupby('mygroup')

この例では、上記のgroupbyに基づいて「チャンク」を作成しますが、これはデータをチャンクする方法である必要はありません。それはかなり一般的なパターンですが。

dflist = []
for name, group in grouped:
    dflist.append(group)

パラレルビットを設定します

from IPython.parallel import Client
rc = Client()
lview = rc.load_balanced_view()
lview.block = True

データに適用する愚かな関数を書く

def myFunc(inDf):
    inDf['newCol'] = inDf.data ** 10
    return inDf

次に、コードをシリアルで実行し、次にパラレルで実行します。シリアル優先:

%%time
serial_list = map(myFunc, dflist)
CPU times: user 14 s, sys: 19.9 ms, total: 14 s
Wall time: 14 s

今並行

%%time
parallel_list = lview.map(myFunc, dflist)

CPU times: user 1.46 s, sys: 86.9 ms, total: 1.54 s
Wall time: 1.56 s

それらを1つのデータフレームにマージするのに数ミリ秒しかかかりません

%%time
combinedDf = pd.concat(parallel_list)
 CPU times: user 296 ms, sys: 5.27 ms, total: 301 ms
Wall time: 300 ms

MacBookで6つのIPythonエンジンを実行していますが、実行時間が14秒から2秒に短縮されていることがわかります。

本当に長時間実行される確率的シミュレーションの場合、 StarCluster でクラスターを起動することでAWSバックエンドを使用できます。しかし、ほとんどの場合、MBPの8つのCPUで並列化しています。

10
JD Long

JD Longの回答に付随する短いコメント。グループの数が非常に多く(数十万など)、適用関数がかなり単純かつ迅速に処理している場合、データフレームをチャンクに分割し、各チャンクをワーカーに割り当てて、 groupby-apply(シリアル)は、並列groupby-applyを実行し、多数のグループを含むキューからワーカーを読み取らせるよりもはるかに高速です。例:

import pandas as pd
import numpy as np
import time
from concurrent.futures import ProcessPoolExecutor, as_completed

nrows = 15000
np.random.seed(1980)
df = pd.DataFrame({'a': np.random.permutation(np.arange(nrows))})

したがって、データフレームは次のようになります。

    a
0   3425
1   1016
2   8141
3   9263
4   8018

列「a」には多くのグループがあることに注意してください(顧客IDを考えてください)。

len(df.a.unique())
15000

グループを操作する機能:

def f1(group):
    time.sleep(0.0001)
    return group

プールを開始します。

ppe = ProcessPoolExecutor(12)
futures = []
results = []

並列groupby-applyを実行します。

%%time

for name, group in df.groupby('a'):
    p = ppe.submit(f1, group)
    futures.append(p)

for future in as_completed(futures):
    r = future.result()
    results.append(r)

df_output = pd.concat(results)
del ppe

CPU times: user 18.8 s, sys: 2.15 s, total: 21 s
Wall time: 17.9 s

ここで、dfをより少ないグループに分割する列を追加しましょう。

df['b'] = np.random.randint(0, 12, nrows)

現在、15000のグループの代わりに12のみがあります。

len(df.b.unique())
12

Dfをパーティション分割し、各チャンクでgroupby-applyを実行します。

ppe = ProcessPoolExecutor(12)

ラッパーの楽しみ:

def f2(df):
    df.groupby('a').apply(f1)
    return df

順次操作する各チャンクを送信します。

%%time

for i in df.b.unique():
    p = ppe.submit(f2, df[df.b==i])
    futures.append(p)

for future in as_completed(futures):
    r = future.result()
    results.append(r)

df_output = pd.concat(results) 

CPU times: user 11.4 s, sys: 176 ms, total: 11.5 s
Wall time: 12.4 s

グループごとに費やす時間は変更されていないことに注意してください。むしろ、変更されたのは、ワーカーが読み取るキューの長さです。何が起こっているのかと思われるのは、ワーカーが共有メモリに同時にアクセスできず、キューから読み取るために絶えず戻っているため、お互いのつま先を踏んでいることです。操作するチャンクが大きくなると、ワーカーが戻る頻度が少なくなるため、この問題は改善され、全体的な実行が高速になります。

3
spring

個人的には、 このスレッド ごとにdaskを使用することをお勧めします。

@chrisbが指摘したように、pandas in pythonを使用したマルチプロセッシングは、不必要なオーバーヘッドを作成する可能性があります。また、 not マルチスレッドまたはシングルスレッドとしても。

Daskは、マルチプロセス専用に作成されます。

0
Jinhua Wang