30
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

[Python3 / pandas] DataFrameをどうしても1行ずつ処理したいときの速度改善策

Last updated at Posted at 2020-03-22

概要

DataFrameをloopさせると非常に遅い。

参考:[Python3 / pandas] dataframeに辞書型データを1行ずつ追加していきたいとき(速度比較)

なので、本当はDataFramをloopさせたくはないのだが、どうしてもやらざるを得ない時があります。
たとえば、「直前の行の値が決まって初めて、次の行の値を決定できる」ような操作を、全ての行に対して行いたいときとか(´・ω・`)

そんな場合に試してみた方法と、その結果を記載しています。

参考記事

先に、参考にした記事を紹介します。

pandas.DataFrameのforループ処理(イテレーション)

この記事では、以下の方法の速度比較が行われていました。
参考までに、この記事の概要だけ記載しておきます。

# 1. 一番遅い
for row in dataframe.iterrows():

# 2. そこそこ早い
for tuple in dataframe.itertuples():

# 3. 最も早い
for colemn1, column2, column3 in zip(dataframe['column1'], dataframe['column2'], dataframe['column3']):

独自調査内容

dataframeには、辞書型との相互変換をしてくれる、to_dict from_dictがありますね。
これを利用した場合の速度比較を行ってみました(^ワ^*)

具体的には、

  1. dataframeをいったん辞書型配列に変換(to_dict)
  2. 辞書型の状態でloop処理を行う
  3. loopが終わったらまたdataframeに戻す(from_dict)

という処理を行い、速度を計測してみました。
比較対象として、その他に4つの処理速度も測定。

ソースコード

計測に使うデータ [2161 rows x 2 columns]

まず、今回処理対象となる、データの生成処理がこちら。

利用データ
df
                     x  gross
2020-01-01 00:00:00  9      9
2020-01-01 01:00:00  1    0.0
2020-01-01 02:00:00  9    0.0
2020-01-01 03:00:00  0    0.0
2020-01-01 04:00:00  2    0.0
...                 ..    ...
2020-03-30 20:00:00  4    0.0
2020-03-30 21:00:00  5    0.0
2020-03-30 22:00:00  7    0.0
2020-03-30 23:00:00  3    0.0
2020-03-31 00:00:00  9    0.0

[2161 rows x 2 columns]
---
# こうやって生成したよ
indexes = pd.date_range(
    start=datetime.datetime(2020, 1, 1, 0, 0),
    end=datetime.datetime(2020, 3, 31, 0, 0),
    freq='1H'
)
df_length = len(indexes)
x = np.random.randint(10, size=df_length)
gross = np.zeros(df_length)

df = pd.DataFrame({
    'x': x, 'gross': gross
}, index=indexes)
df.iloc[0, 1] = df.iloc[0]['x']

このdataframeを、次のような状態にする処理の速度を計測します。
dataframe.cumsum()メソッドと同じ処理を、様々な方法で実装してみる感じになります。

処理結果
                     x  gross
2020-01-01 00:00:00  9      9
2020-01-01 01:00:00  1     10
2020-01-01 02:00:00  9     19
2020-01-01 03:00:00  0     19
2020-01-01 04:00:00  2     21
...                 ..    ...
2020-03-30 20:00:00  4   9629
2020-03-30 21:00:00  5   9634
2020-03-30 22:00:00  7   9641
2020-03-30 23:00:00  3   9644
2020-03-31 00:00:00  9   9653

dfが上書きされると困るので、実装前に別々の変数にコピーしておきます。

# データ準備
df1 = df.copy()
df2 = df.copy()
df3 = df.copy()
df4 = df.copy()
df5 = df.copy()

実行環境

Google Colaboratory (2020/03/22時点)
python は 3
ほかのバージョンは調べてません!

処理内容

それでは、色々な方法でseries.cumsum()を実装してみます。

1. series.cumsum()

これは本物。

%timeit df1['gross'] = df1['x'].cumsum()
---
The slowest run took 5.76 times longer than the fastest. This could mean that an intermediate result is being cached.
1000 loops, best of 3: 301 µs per loop

301マイクロ秒(= 0.301ミリ秒)

2. df.iterrows()

いわゆるめちゃ遅い処理。

%%timeit
for i, (index, row) in enumerate(df2.iterrows()):
    if i == 0:
        next
    # warning が出たので、1行下の処理を2行下の処理に書き換えた
    # row.loc['gross2'] = df2.iloc[i - 1, :]['gross2'] + row.x
    df2.at[index, 'gross'] = df2.iloc[i - 1, :]['gross'] + row.x
---
1 loop, best of 3: 887 ms per loop

887ミリ秒
処理結果は同じなのに、1. の3000倍くらいかかっている....(´・□・`;)

3. df.itertuples()

参考記事においては iteritems() より早かったが...

%%timeit
for i, row in enumerate(df3.itertuples()):
    if i == 0:
        next
    df3.at[row.Index, 'gross'] = df3.iloc[i - 1, :]['gross'] + row.x
---
1 loop, best of 3: 676 ms per loop

676ミリ秒

参考記事の言う通り、他の条件が一緒であれば、iteritems() よりも itertuples() を loop させた方が速いようです。

4. zip(df.column1, df.column2)

%%timeit
for i, (index, x) in enumerate(zip(df4.index, df4['x'])):
    if i == 0:
        next
    df4.at[index, 'gross'] = df4.iloc[i - 1, :]['gross'] + x
---
1 loop, best of 3: 682 ms per loop

682ミリ秒

参考記事においては、itertuples() よりも速いことが確認されていましたが、その他の条件によっても影響があるのかもしれません。
3. とほぼ同程度の結果となりました。

5. df.to_dict -> loop -> DataFrame.from_dict([dict])

今回の目玉だよ(^ワ^*)

%%timeit
df_dicts = df5.to_dict(orient='records')

for i, df_dict in enumerate(df_dicts):
    if i == 0:
        next
    df_dict['gross'] = df_dicts[i - 1]['gross'] + df_dict['x']

new_df5 = pd.DataFrame.from_dict(df_dicts) \
            .set_index(df5.index)
---
100 loops, best of 3: 10.1 ms per loop

10ミリ秒!(((o(*゚▽゚*)o)))

to_dict from_dict だけでなく、他の処理結果と同じ結果にするためには set_index も必要だったのですが、それでも圧倒的な速さ!

やはり、 dataframe は loop させないに限る(`・ω・´)

調査結果

実装方法 処理時間(ミリ秒) 対cumsum()比
1 series.cumsum() 0.301 1.0倍
2 df.iterrows() 887 2,946.8倍
3 df.itertuples() 676 2,245.8倍
4 zip(df.column1, df.column2) 682 2,265.8倍
5 df.to_dict -> loop -> DataFrame.from_dict([dict]) 10 33.2倍

わかること

dataframeのloopは遅いけれど、そもそもloop自体が遅い。
なるべくnumpyやpandasに実装されている行列演算処理(df.cumsum()とかdf['x'] * 3とか)を使おう....

けれども、どうしても行列演算で一発処理できない場合には、dataframeを辞書配列化して処理することで、速度低下を相当程度防ぐことができる

補足

1行ずつ処理する必要がある場合の例なので、frompyfuncによる計測はしていません。
frompyfuncだと、前の行の値の決定を待ってから次の行を処理する、ということができないので、今回のケースでは使えないため。

ですが、frompyfuncはかなり早くておすすめなので、これで代用できるときは使ってみてください(以下の関連記事を参照)。

関連記事

30
27
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
30
27

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?