Pandas DataFrameにapply()がすべてのコアを使用するようにしますか?


98

2017年8月の時点で、Pandas DataFame.apply()は残念ながらまだシングルコアでの動作に制限されています。つまり、マルチコアマシンは、実行時に計算時間の大部分を浪費しますdf.apply(myfunc, axis=1)

すべてのコアを使用して、データフレームに並列で適用を実行するにはどうすればよいですか?

回答:


73

swifterパッケージを使用できます。

pip install swifter

これはパンダのプラグインとしてapply機能し、関数を再利用できます。

import swifter

def some_function(data):
    return data * 10

data['out'] = data['in'].swifter.apply(some_function)

ベクトル化されている(上記の例のように)かどうかに関係なく、関数を並列化する最も効率的な方法を自動的に判断します。

その他の例パフォーマンスの比較は、GitHubで入手できます。パッケージは開発中のため、APIが変更される可能性があることに注意してください。

また、これ文字列列に対しては自動的に機能ないことにも注意してください。文字列を使用する場合、Swifterは「単純な」パンダapplyにフォールバックしますが、これは並列ではありません。この場合、使用daskを強制してもパフォーマンスは向上しません。データセットを手動で分割し、を使用multiprocessingして並列化することをお勧めします。


1
私たちの純粋な好奇心ですが、並列適用を行うときに使用するコアの数を制限する方法はありますか?私は共有サーバーを持っているので、32コアすべてを取得しても、誰も満足しません。
マクシムハイトビッチ2018

1
@MaximHaytovichわからない。Swifterはバックグラウンドでdaskを使用するため、おそらく次の設定を尊重します:stackoverflow.com/a/40633117/435093 —それ以外の場合は、GitHubで問題を開くことをお勧めします。著者は非常に敏感です。
slhck 2018

@slhckありがとう!もう少し掘ります。とにかくWindowsサーバーでは動作しないようです-ハングアップしておもちゃのタスクでは何もしません
Maksim Khaitovich

- :あなたはこれを答えるのヘルプ私を喜ばせることができstackoverflow.com/questions/53561794/...
ak3191

2
文字列の場合は、次のallow_dask_on_strings(enable=True)ように追加します。df.swifter.allow_dask_on_strings(enable=True).apply(some_function) ソース:github.com/jmcarpenter2/swifter/issues/45
Sumit Sidana

99

最も簡単な方法は、Daskのmap_partitionsを使用することです。これらのインポートが必要です(必要になりますpip install dask):

import pandas as pd
import dask.dataframe as dd
from dask.multiprocessing import get

そして構文は

data = <your_pandas_dataframe>
ddata = dd.from_pandas(data, npartitions=30)

def myfunc(x,y,z, ...): return <whatever>

res = ddata.map_partitions(lambda df: df.apply((lambda row: myfunc(*row)), axis=1)).compute(get=get)  

(16コアの場合、30が適切なパーティション数だと思います)。完全を期すために、私は自分のマシン(16コア)の違いを測定しました。

data = pd.DataFrame()
data['col1'] = np.random.normal(size = 1500000)
data['col2'] = np.random.normal(size = 1500000)

ddata = dd.from_pandas(data, npartitions=30)
def myfunc(x,y): return y*(x**2+1)
def apply_myfunc_to_DF(df): return df.apply((lambda row: myfunc(*row)), axis=1)
def pandas_apply(): return apply_myfunc_to_DF(data)
def dask_apply(): return ddata.map_partitions(apply_myfunc_to_DF).compute(get=get)  
def vectorized(): return myfunc(data['col1'], data['col2']  )

t_pds = timeit.Timer(lambda: pandas_apply())
print(t_pds.timeit(number=1))

28.16970546543598

t_dsk = timeit.Timer(lambda: dask_apply())
print(t_dsk.timeit(number=1))

2.708152851089835

t_vec = timeit.Timer(lambda: vectorized())
print(t_vec.timeit(number=1))

0.010668013244867325

寄付10スピードアップの要因パンダから行くと、パーティションに適用DASKに適用されます。もちろん、ベクトル化できる関数がある場合は、そうする必要があります。この場合、関数(y*(x**2+1))は簡単にベクトル化されますが、ベクトル化できないものはたくさんあります。


2
投稿していただきありがとうございます。30パーティションを選択した理由を説明できますか?この値を変更するとパフォーマンスは変わりますか?
Andrew L

2
@AndrewL私は、各パーティションが個別のプロセスによって処理されると想定しています。16コアでは、16または32のプロセスを同時に実行できると想定しています。試してみたところ、最大32個のパーティションでパフォーマンスが向上したようですが、それ以上増やしても効果はありません。クアッドコアマシンでは8つのパーティションなどが必要だと思います。16と32の間でいくつかの改善が見られたので、2x $ NUM_PROCESSORS
Roko Mijic

8
唯一のことはThe get= keyword has been deprecated. Please use the scheduler= keyword instead with the name of the desired scheduler like 'threads' or 'processes'
wordforthewise 08/10/18

4
dask v0.20.0以降では、ddata.map_partitions(lambda df:df.apply((lambda row:myfunc(* row))、axis = 1))。compute(scheduler = 'processes')、またはその他のスケジューラオプション。現在のコードでは「TypeError:get =キーワードが削除されました。代わりに、「threads」や「processes」などの目的のスケジューラーの名前を付けてscheduler =キーワードを使用してください」
mork

1
これを行う前に、スローするときにデータフレームに重複するインデックスがないことを確認してくださいValueError: cannot reindex from a duplicate axis。これを回避するには、で重複したインデックスを削除するdf = df[~df.index.duplicated()]か、でインデックスをリセットする必要がありますdf.reset_index(inplace=True)
Habib Karbasian、

23

pandarallel代わりに試すことができます:すべてのCPUでパンダ操作を並列化するためのシンプルで効率的なツール(LinuxおよびmacOS)

  • 並列化にはコスト(新しいプロセスのインスタンス化、共有メモリを介したデータの送信など)があるため、並列化する計算量が十分に多い場合にのみ、並列化は効率的です。非常に少量のデータの場合、パラレゼーションを使用することは必ずしも価値がありません。
  • 適用される関数はラムダ関数であってはなりません。
from pandarallel import pandarallel
from math import sin

pandarallel.initialize()

# FORBIDDEN
df.parallel_apply(lambda x: sin(x**2), axis=1)

# ALLOWED
def func(x):
    return sin(x**2)

df.parallel_apply(func, axis=1)

https://github.com/nalepae/pandarallelを参照してください


こんにちは、1つの問題を解決できません。pandarallelを使用すると、エラーが発生します。AttributeError:Ca n't pickle local object 'prepare_worker。<locals> .closure。<locals> .wrapper'。これを手伝ってくれませんか?
Alex Cam

@Alex Sry私はそのモジュールの開発者ではありません。コードはどのように見えますか?「内部関数」をグローバルとして宣言してみることができますか?(推測)
G_KOBELIEF

@AlexCam Pythonがマルチプロセッシング用にピクルできるように、関数を他の関数の外部で定義する必要があります
Kenan

@G_KOBELIEF Python> 3.6では、pandaparallelでラムダ関数を使用できます
user110244

16

ネイティブのPythonを使い続けたい場合:

import multiprocessing as mp

with mp.Pool(mp.cpu_count()) as pool:
    df['newcol'] = pool.map(f, df['col'])

関数をデータフレームのfcolに並行して適用しますdf


このようなアプローチに従って、私はValueError: Length of values does not match length of indexから取得しまし__setitem__pandas/core/frame.py。私が何か間違ったことをしたのか、またはへの割り当てdf['newcol']がスレッドセーフではないのかわかりません。
ガラガラ

2
pool.mapを中間のtemp_resultリストに書き込んで、長さがdfと一致するかどうかを確認してから、df ['newcol'] = temp_result?
Olivier Cruchant

新しい列を作成することを意味しますか?何を使いますか?
Olivier Cruchant

はい、マップの結果をデータフレームの新しい列に割り当てます。mapは、関数fに送信された各チャンクの結果のリストを返しませんか?それを列「newcol」に割り当てるとどうなりますか?PandasとPython 3の使用
ミナ

それは実際には本当にスムーズに動作します!やってみましたか?送信されたものと同じ順序で、同じ長さのdfのリストを作成します。文字通りc2 = f(c1)を並列に実行します。Pythonでマルチプロセスを行う簡単な方法はありません。パフォーマンスの点では、Rayも良いことを行えるようですが(datascience.com/…)、成熟度が低く、インストールが必ずしもスムーズに行われない場合があります
Olivier Cruchant

1

これは、パンダが適用されるsklearnベーストランスフォーマーの例です。

import multiprocessing as mp
from sklearn.base import TransformerMixin, BaseEstimator

class ParllelTransformer(BaseEstimator, TransformerMixin):
    def __init__(self,
                 n_jobs=1):
        """
        n_jobs - parallel jobs to run
        """
        self.variety = variety
        self.user_abbrevs = user_abbrevs
        self.n_jobs = n_jobs
    def fit(self, X, y=None):
        return self
    def transform(self, X, *_):
        X_copy = X.copy()
        cores = mp.cpu_count()
        partitions = 1

        if self.n_jobs <= -1:
            partitions = cores
        elif self.n_jobs <= 0:
            partitions = 1
        else:
            partitions = min(self.n_jobs, cores)

        if partitions == 1:
            # transform sequentially
            return X_copy.apply(self._transform_one)

        # splitting data into batches
        data_split = np.array_split(X_copy, partitions)

        pool = mp.Pool(cores)

        # Here reduce function - concationation of transformed batches
        data = pd.concat(
            pool.map(self._preprocess_part, data_split)
        )

        pool.close()
        pool.join()
        return data
    def _transform_part(self, df_part):
        return df_part.apply(self._transform_one)
    def _transform_one(self, line):
        # some kind of transformations here
        return line

詳細については、https: //towardsdatascience.com/4-easy-steps-to-improve-your-machine-learning-code-performance-88a0b0eeffa8を参照してください

弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.