私は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)
これは実際に機能しているようですが、実際にはパンダに組み込む必要があります
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)
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の動作を取得する必要があります。
パンダで並列化を行うために使用するハックがあります。データフレームをチャンクに分割し、各チャンクをリストの要素に入れてから、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で並列化しています。
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
グループごとに費やす時間は変更されていないことに注意してください。むしろ、変更されたのは、ワーカーが読み取るキューの長さです。何が起こっているのかと思われるのは、ワーカーが共有メモリに同時にアクセスできず、キューから読み取るために絶えず戻っているため、お互いのつま先を踏んでいることです。操作するチャンクが大きくなると、ワーカーが戻る頻度が少なくなるため、この問題は改善され、全体的な実行が高速になります。
個人的には、 このスレッド ごとにdaskを使用することをお勧めします。
@chrisbが指摘したように、pandas in pythonを使用したマルチプロセッシングは、不必要なオーバーヘッドを作成する可能性があります。また、 not マルチスレッドまたはシングルスレッドとしても。
Daskは、マルチプロセス専用に作成されます。