実行時間を高速化する別の記述方法×2を追記しています。(2021/08/20)
背景
pandasのデータフレームを使用したfor文で、データフレームを行ごとに取得して…という処理を書いていました。しかし、実現したい内容に大して現実的でない時間がかかってしまうことが判明しました。いろいろ調べたり確認してみると、以下コードに時間がかかってしまっていることがわかりました。
for id, row in df.iterrows():
調べてみると.iterrows()
に時間がかかるようです。とにかく脱iterrows()
を試みて実行時間を高速化する必要がありました。
その中でリストに変換する方法(df.values
)がありました。しかし、これはrow
の型が変わってしまい、一筋縄ではいきません。具体的には、row['特定のカラム名']
と記述できていたものを、row[1]
などリストのインデックスを使わなければいけなくなります(1
は特定のカラム名
に対応するインデックス)。後から見返した時に「この数字何!?あっリストのインデックスか!なんのデータなんだろう?」となってしまいます。
以上を踏まえて、個人的に「これがいいんじゃないか?」と思える改善案を見つけました。
結論
例えば、👇の場合…
for id, row in df.iterrows():
# something to do
_ = row['特定のカラム名']
👇のようにするとOK👍
# preparation
map_colmun_and_index = {}
for index, colmun in enumerate(df.columns, 0):
map_colmun_and_index[colmun] = index
for row in df.values:
# something to do
_ = row[map_colmun_and_index['特定のカラム名']]
簡単にまとめると…
- 実行時間の高速化
-
df.values
でリスト化したものでfor文を実施
-
-
iterrows()
を使用するメリットの一部を残す&改善コストの削減- 事前にカラム名とリスト化した場合のインデックスを紐付け、マッピング
- マッピングを使って、カラム名でリストの値の参照を実現
これにより、約70倍(※)の高速化が実現されます㊗️
(※:後述する実験の条件において算出)
念のため、差分で見たい人向けに、差分も紹介します。
+ map_colmun_and_index = {}
+ for index, colmun in enumerate(df.columns, 0):
+ map_colmun_and_index[colmun] = index
+
- for id, row in df.iterrows():
+ for row in df.values:
# something to do
- _ = row['特定のカラム名']
+ _ = row[map_colmun_and_index['特定のカラム名']]
実験
結論を先に紹介しましたが以下を確認するために実験した様子も紹介します。
- 本当に高速化が実現する?
- その効果は何倍?
実験概要
- 先に紹介した方法(改善後)と高速化を試みる前にiterrows()を使っていた方法(改善前)を比較
- 10,000回実施した実行時間の中央値から何倍の高速化が実現したかを評価
実験環境
実験&結果
scikit-learnよりiris(アヤメ)のデータを使用します。まずはデータを取得し、データフレームに変換します。
from sklearn.datasets import load_iris
iris = load_iris()
iris_df = pd.DataFrame(iris.data, columns=iris.feature_names)
行・列の長さは150、4でした。
iris_df.shape
(150, 4)
データの中身(上位5件)は以下の通りです。
iris_df.head()
sepal length (cm) | sepal width (cm) | petal length (cm) | petal width (cm) |
---|---|---|---|
5.1 | 3.5 | 1.4 | 0.2 |
4.9 | 3.0 | 1.4 | 0.2 |
4.7 | 3.2 | 1.3 | 0.2 |
4.6 | 3.1 | 1.5 | 0.2 |
5.0 | 3.6 | 1.4 | 0.2 |
それでは、実験を実施します。
import time
results_pre_kaizen = []
results_post_kaizen = []
for _ in range(10000):
start_time = time.time()
for id, row in iris_df.iterrows():
# something to do
_ = row['sepal length (cm)']
results_pre_kaizen.append(time.time() - start_time)
start_time = time.time()
# preparation
map_colmun_and_index = {}
for index, colmun in enumerate(iris_df.columns, 0):
map_colmun_and_index[colmun] = index
for row in iris_df.values:
# something to do
_ = row[dict_hoge['sepal length (cm)']]
results_post_kaizen.append(time.time() - start_time)
実行結果×10,000をデータフレームに変換し、基本統計量を確認します。
df = pd.DataFrame({"改善前":results_pre_kaizen,"改善後":results_post_kaizen}).describe()
df
改善前 | 改善後 | |
---|---|---|
count | 10000.000000 | 10000.000000 |
mean | 0.008074 | 0.000115 |
std | 0.000852 | 0.000029 |
min | 0.007478 | 0.000087 |
25% | 0.007632 | 0.000105 |
50% | 0.007767 | 0.000112 |
75% | 0.008166 | 0.000118 |
max | 0.019278 | 0.001674 |
基本統計量における全ての値において、実行時間は改善前より改善後の方が高速であることがわかりました。
最後に中央値(50%)からどれだけの高速化が実現できたのかを確認します。
df["改善前"]["50%"]/df["改善後"]["50%"]
69.06794055201699
約70(≒69.06794055201699)倍の高速化が実現されたことがわかりました!
おわりに
「pandasのデータフレームを使用したfor文において脱iterrows()を試みたら実行時間が約70倍高速化した話」を紹介しました。似たような悩み抱く方に響けば嬉しいです。より良い改善案や助言などがありましたら教えてください!
追記(2021/08/20)
実行時間を高速化する別の記述方法×2を紹介します。本記事を公開後に発見した記述方法ということもあり、上述した記述方法より、差分が少なく、理解も容易かと思われます。
辞書に変換する方法
辞書に変換することで、カラム名による値の指定が実現可能です。
for row in df.to_dict(orient="records"):
# something to do
_ = row['特定のカラム名']
念のため、差分で見たい人向けに、差分も紹介します。
- for id, row in df.iterrows():
+ for row in df.to_dict(orient="records"):
# something to do
_ = row['特定のカラム名']
df4loopを利用する方法
これまでの経緯から「可読性を損なわず脱iterrows()
を試みることはできないか」と考えました。そこで、df4loopというライブラリ(OSS)を開発し、リリースしました。
Pypiで公開しています。
df4loop自体はデータフレームに関する諸々のサポートを目的としてます。中でもDFIterator
というクラスに脱iterrows()
を目指した実装がなされています。
Pypiで公開されているため、以下でインストールが可能です。
pip install df4loop
iterrows()におけるindexが必要な場合は以下のような記述で実現可能です。
# preparation
from df4loop import DFIterator
df_iterator = DFIterator(df)
for index, row in df_iterator.iterrows():
# something to do
_ = row['特定のカラム名']
iterrows()
におけるindex
が必要な場合は以下のような記述で実現可能です。return_indexes
の値をFalse
にしています。
# preparation
from df4loop import DFIterator
df_iterator = DFIterator(df)
for row in df_iterator.iterrows(return_indexes=False):
# something to do
_ = row['特定のカラム名']
念のため、差分で見たい人向けに、差分も紹介します。
+ from df4loop import DFIterator
+ df_iterator = DFIterator(df)
- for id, row in df.iterrows():
+ for index, row in df_iterator.iterrows():
# something to do
_ = row['特定のカラム名']
+ from df4loop import DFIterator
+ df_iterator = DFIterator(df)
- for id, row in df.iterrows():
+ for row in df_iterator.iterrows(return_indexes=False):
# something to do
_ = row['特定のカラム名']
参考までに、以下はKaggleのNotebookでdf4loopを利用した例です。