Python の range と ndarray と Series と DataFrame のイテレーターのパフォーマンスを比較してみました

以前、作った日足のヒストリカルデータでバックテストをするのに、 Python の実行速度としてのパフォーマンスが気になったので比較してみました。

環境

  • Python 3.5.2
  • pandas 0.22.0
  • NumPy 1.14.3

pandas を pip install pandas でインストールしておきました。 一緒に NumPy もインストールされました。

Jupyter Notebook で動かすので、 pip install jupyter でインストールしておきました。

準備

これ以降、 Jupyter Notebook に入力しながら進めてみます。

NumPy と pandas のモジュールを読み込みました。

import numpy as np
import pandas as pd

データを作るパフォーマンス

100 万件のデータで比較しようと思いましたので、 range, ndarray, Series, DataFrame のそれぞれを作りました。

range

%timeit -n 100 r = range(1000000)

結果です。

509 ns ± 13.6 ns per loop (mean ± std. dev. of 7 runs, 100 loops each)

ndarray

%timeit -n 100 a = np.arange(1000000)

結果です。

2.21 ms ± 292 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

Series

%timeit -n 100 s = pd.Series(np.arange(1000000))

結果です。

2.35 ms ± 171 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

DataFrame

%timeit -n 100 df = pd.DataFrame(data=np.arange(1000000), columns=['col1'])

結果です。

2.56 ms ± 182 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

データを作るパフォーマンスのまとめ

range は 500ns 程度なので一番速かったです。

Series と DataFrame は ndarray でデータを作ってからオブジェクトを作ったので、 ndarray よりも少しだけ遅くなりました。 でも、どれも 3ms 程度なので思っていたより違わないと感じました。

イテレーターのパフォーマンス

最初にデータを作りました。

r = range(1000000)
a = np.arange(1000000)
s = pd.Series(np.arange(1000000))
df = pd.DataFrame(data=np.arange(1000000), columns=['col1'])
df2 = pd.DataFrame(data=np.arange(2000000).reshape((1000000, 2)), columns=['col1', 'col2'])

range

%%timeit
# https://wiki.python.org/moin/ForLoop
i = 0
for x in r:
    i = x
i

結果です。

54.3 ms ± 839 µs per loop (mean ± std. dev. of 7 runs, 10 loops each)

ndarray

%%timeit
# https://docs.scipy.org/doc/numpy/reference/generated/numpy.nditer.html
# https://docs.scipy.org/doc/numpy/reference/arrays.nditer.html
i = 0
for x in np.nditer(a):
    i = x
i

結果です。

276 ms ± 18.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

Series

%%timeit
# https://pandas.pydata.org/pandas-docs/stable/generated/pandas.Series.iteritems.html
i = 0
for index, value in s.iteritems():
    i = value
i

結果です。

326 ms ± 22.7 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

DataFrame

%%time
# https://pandas.pydata.org/pandas-docs/stable/generated/pandas.DataFrame.iterrows.html
i = 0
for index, row in df.iterrows():
    i = row['col1']
i

結果です。

CPU times: user 1min 46s, sys: 438 ms, total: 1min 46s
Wall time: 1min 49s

DataFrame.iterrows は試していたら遅かったので %%timeit による繰り返しの計測はせずに、 %%time による計測を 1 回だけしました。 2 分近くかかってしまっているため待っていられませんでした。

イテレーターのパフォーマンスのまとめ

DataFrame が極端に遅いです。 ndarray, Series はあまり違いはないのかもしれません。 range は一番速かったです。

参照のパフォーマンス

i への代入 (i =) は同じ時間がかかっていると思うので、イテレーターの値を参照している箇所だけのパフォーマンスを比較してみました。

range

for x in range(10):
    %time x

結果です。

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 22.2 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 31 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 21.7 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 29.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 28.4 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 28.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 27.2 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 20.3 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 20 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 26 µs

20~31µs の範囲になりました。

ndarray

for x in np.nditer(np.arange(10)):
    %time x

結果です。

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 14.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 21.7 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 13.4 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 12.4 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 12.2 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 12.2 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 12.2 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 20.5 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 18.1 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 22.6 µs

12.2~22.6µs の範囲になりました。

数が少ないですけれども range よりも速いところがあって意外でした。 一番遅い 22.6µs でも range の一番遅い 31µs よりは速いです。

Series

for index, item in pd.Series(np.arange(10)).iteritems():
    %time item

結果です。

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 14.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 18.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 33.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 20.7 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 28.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 42.4 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 143 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 29.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 33.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 20.7 µs

14.8~143µs の範囲になりました。

一つだけ突出して遅いのがありました。

DataFrame

for index, row in pd.DataFrame(data=np.arange(10), columns=['col1']).iterrows():
    %time row['col1']

結果です。

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 233 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 340 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 90.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 85.6 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 143 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 143 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 116 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 186 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 253 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 177 µs

85.6~340µs の範囲になりました。

参照のパフォーマンスのまとめ

DataFrame が遅いです。 列の名前で参照するのは遅いみたいです。

range, ndarray, Series はあまり違いはないのかもしれません。

DataFrame.itertuples のパフォーマンス

DataFrame のイテレーターは DataFrame.iterrows だけじゃなくて DataFrame.itertuples もあるみたいでした。 せっかくなので DataFrame.itertuples も試してみました。

DataFrame.iteritems は列に対するイテレーターみたいなので試しませんでした。

DataFrame.itertuples 1 列のイテレーターのパフォーマンス

%%timeit
i = 0
for row in df.itertuples():
    i = row[1]
i

DataFrame.itertuples はタプルが返ってくるで、列の名前で参照することができず、インデックスで参照することになるので、少し見づらいです。 row[1]1 が何を参照しているのか認識しづらいですが、 df['col1'] の列を参照しています。 row[0]df.index の列を参照するみたいです。

結果です。

1.13 s ± 46.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

DataFrame.itertuples 2 列のイテレーターのパフォーマンス

%%timeit
i = 0
for row in df2.itertuples():
    i = row[1]
i

結果です。

1.22 s ± 44.1 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)

100ms くらい増えました。 比例するように増えてはいかないだろうけれども、 range の 100 万件のイテレーターが 50ms くらいだったので、それと比べると遅さが目立ちます。

DataFrame.itertuples 参照のパフォーマンス

for row in pd.DataFrame(data=np.arange(10), columns=['col1']).itertuples():
    %time row[1]

結果です。

CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 11.7 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 24.1 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 11 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 10.3 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 9.54 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 18.8 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 10.5 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 9.78 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 14.3 µs
CPU times: user 0 ns, sys: 0 ns, total: 0 ns
Wall time: 14.3 µs

9.78~24.1µs の範囲になりました。

range, ndarray, Series と同じくらいになりました。 一番速い値が 9.78µs なので、 ndarray の一番速い値 (12.2µs) よりも、 range の一番速い値 (20µs) よりも、速かったです。

繰り返しが多い場合は DataFrame.iterrows よりも DataFrame.itertuples を使った方がよさそうです。

Comprehensions (内包表記)

for より内包表記の方が速い認識でいたので、気になって確認してみました。

DataFrame.iterrows も内包表記を使ったら速くなるのかどうか。

%time l1 = [row['col1'] for index, row in df2.iterrows()]
%time l2 = [row[1] for row in df2.itertuples()]
l1[-5:] + l2[-5:]

結果です。

CPU times: user 1min 43s, sys: 688 ms, total: 1min 43s
Wall time: 1min 46s
CPU times: user 953 ms, sys: 219 ms, total: 1.17 s
Wall time: 1.23 s

[1999990,
 1999992,
 1999994,
 1999996,
 1999998,
 1999990,
 1999992,
 1999994,
 1999996,
 1999998]

DataFrame.iterrows を使った内包表記だと 100 万件が 1 分 46 秒。 DataFrame.itertuples を使った内包表記だと 100 万件が 1.23 秒。 大きな違いになりました。

DataFrame.iterrows の内包表記を使うくらいだったら、 DataFrame.itertuples の for を使った方がよっぽどいいです。 ざっくりした認識で内包表記は速いと思って、よく調べもせずに使っていてはいけないと感じました。

Gist のソース

Gist にソースをアップしました。

終わり

次の順番で下に行くにつれて遅くなっていくと思っていたのですが、イテレーターだけだとあまり違いはないように感じました。

  1. Python の標準のオブジェクト
  2. Numpy
  3. pandas

繰り返しの多いところでは DataFrame.iterrows は使わないようにしようと思いました。