それ,numpy で書かない?
Python ではリストがよく使われる。また,for ループが遅いのでリストに特化したリスト内包表記も推奨されることが多い。
それなりの根拠があるからではあるが...
課題:$n$ 個の整数を要素とするリストが 2 対ある。対応する要素が等しいものと等しくないものの個数を求めよ。
テストデータとして以下のような,0 ~ 999 の整数要素からなる,2 個のリストを作る。
from time import time
import random
n = 10000000
random.seed(123)
expected = [random.randint(0, 999) for _ in range(n)]
predicted = [random.randint(0, 999) for _ in range(n)]
for を使うとたしかに遅い。
s = time()
correct = 0.0
wrong = 0.0
for i in range(n):
if expected[i] == predicted[i]:
correct += 1
else:
wrong += 1
print("@1", correct, wrong, time() - s)
@1 10026.0 9989974.0 1.076805830001831
遅いのは遅いが,ちょっと無駄がある。同じものと違うものを別々に数えているが,これはどちらか一方を数えるだけで十分だ。
s = time()
correct = 0.0
wrong = 0.0
for i in range(n):
if expected[i] == predicted[i]:
correct += 1
wrong = n - correct
print("@2", correct, wrong, time() - s)
@2 10026.0 9989974.0 0.7036769390106201
実行速度は倍速になるかと思いきや,そうはいかない。
for が遅いのを,リスト内包表記でカバーするのが Python 流。
書き方は何通りかあるので,遅い順に挙げてみる。
まずは,for の前に判定条件を置く場合。判定結果は長さが同じ True/False からなるリストなので,同じものの個数は sum() で得られる。
最初のものに比べるとほぼ倍速になる。
s = time()
correct = sum([expected[_] == predicted[_] for _ in range(n)])
wrong = n - correct
print("@3", correct, wrong, time() - s)
@3 10026 9989974 0.49999427795410156
次は,for のあとに if を置く場合。結果は等しいものだけなので,同じものの個数は len() で得られる。
前のものに比べて少し速くなる。
s = time()
correct = len([_ for _ in range(n) if expected[_] == predicted[_]])
wrong = n - correct
print("@4", correct, wrong, time() - s)
@4 10026 9989974 0.43302106857299805
次は,イテレータに zip() を使う方法。
更に速くなる。
s = time()
correct = sum([_1 == _2 for _1, _2 in zip(expected, predicted)])
wrong = n - correct
print("@5", correct, wrong, time() - s)
@5 10026 9989974 0.2798469066619873
次は,イテレータに zip() を使う方法。
更に速くなる。最初の for を使った場合に比べてほぼ 4 倍速である。
s = time()
correct = len([_1 for _1, _2 in zip(expected, predicted) if _1 == _2])
wrong = n - correct
print("@6", correct, wrong, time() - s)
@6 10026 9989974 0.22952604293823242
ここまでにしようかとも思うが,観点を変えて numpy を使った場合を見てみる。
リスト内包表記を使った最後のプログラムは 1 行で書けるとはいえ,決して簡潔ではない。
numpy で書くと,簡潔に書くことができる(もちろん 1 行で)。
速度は,ちょっと遅い。
以下ではリストを numpy 配列にする処理を挟むが,最初から numpy で書けば,余分な処理をする必要はない。
import numpy as np
expected2 = np.ravel(expected)
predicted2 = np.ravel(predicted)
s = time()
correct = sum(expected2 == predicted2)
wrong = n - correct
print("@7", correct, wrong, time() - s)
@7 10026 9989974 0.6402859687805176
内包表記で書いた場合の 2 倍ぐらいかかる。
ならば,内包表記で書く...
いやいや,待って。
長さが 1千万のリストで 0.27 秒かかるのが 0.51 なのだから,体感的にはそんなに差がないともいえる。
numpy を使うメリット,局面は他にもあるので,今回の課題にはこのやり方で十分だろう...
個人の感想です。