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

# 概要

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

参考：[[Python3 / pandas] dataframeに辞書型データを1行ずつ追加していきたいとき（速度比較）](https://qiita.com/siruku6/items/4846431198769b38bb41)

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

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


# 参考記事

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

[pandas.DataFrameのforループ処理（イテレーション）](https://note.nkmk.me/python-pandas-dataframe-for-iteration/)

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

```python
# 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]

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

```python:利用データ
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()`メソッドと同じ処理を、様々な方法で実装してみる感じになります。

```python:処理結果
                     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`が上書きされると困るので、実装前に別々の変数にコピーしておきます。

```python
# データ準備
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()

これは本物。

```python
%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()

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

```python
%%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() より早かったが...

```python
%%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)

```python
%%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])

今回の目玉だよ（^ワ^*）

```python
%%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行ずつ追加していきたいとき（速度比較）](https://qiita.com/siruku6/items/4846431198769b38bb41)  
この記事を書いたときに、`frompyfunc`のすごさを知りました