概要
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
がありますね。
これを利用した場合の速度比較を行ってみました(^ワ^*)
具体的には、
- dataframeをいったん辞書型配列に変換(to_dict)
- 辞書型の状態でloop処理を行う
- 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
はかなり早くておすすめなので、これで代用できるときは使ってみてください(以下の関連記事を参照)。
関連記事
-
[Python3 / pandas] dataframeに辞書型データを1行ずつ追加していきたいとき(速度比較)
この記事を書いたときに、frompyfunc
のすごさを知りました