前回のおさらい
前回の記事で、Pythonでアイテムの個数を数える方法を2種類(+α)ご紹介して、どれが速いか調べてみました。
これらの方法はどんな入力にも(入力される値の事前情報を仮定せずに)使えるものですが、記事にコメントをいただいたように、どんな値が来るか分かっていて、しかもその値の種類が少ないときには、もっと速くカウントすることができます。
前回、入力の題材にしていた
seq = 'ACAGCATGCA' * 10000000
は、取り得る値(文字)がA, C, G, Tの4種類しかありませんので、
- Aの個数を数える
- Cの個数を数える
- Gの個数を数える
- Tの個数を数える
という処理を行うだけでよく、Counter, defaultdictと比較して高速に動作します。そういう意味だと、実はCounter, defaultdictの比較記事としてはよくない入力例だったのかもしれません。
「少ない」って何種類まで?
それでは、具体的に値が何種類までだったら速くできるのでしょうか?
数十個までなのか、数百個までなのか、それとも数千個までなのか、くらいの見当はつけたいですね。
測定環境
前回と同じです。
- Cygwin (Windows 10)
- Python 2.7.14 / 3.6.4
- CPU: Intel Core i7-7500U
リストが入力される場合
import collections
import timeit
def count_counter(iterable):
counter = collections.Counter(iterable)
return dict(counter)
def count_defaultdict(iterable):
counter = collections.defaultdict(int)
for x in iterable:
counter[x] += 1
return dict(counter)
def count_seqcount(seq, setvalues):
counter = dict((x, seq.count(x)) for x in setvalues)
return counter
if __name__ == '__main__':
for num_uniqvalues in (2**i for i in range(8)):
seq = list(range(num_uniqvalues)) * (10000000 // num_uniqvalues)
uniqvalues = set(seq)
print("# unique values:", num_uniqvalues)
print(timeit.timeit("count_counter(seq)", setup='from __main__ import count_counter, seq', number=1))
print(timeit.timeit("count_defaultdict(seq)", setup='from __main__ import count_defaultdict, seq', number=1))
print(timeit.timeit("count_seqcount(seq, uniqvalues)", setup='from __main__ import count_seqcount, seq, uniqvalues', number=1))
まずは、入力列を数値のリストとして、
- 前回試したcollections.Counter()を使う方法
- 前回試したcollections.defaultdict()を使う方法
- 取り得る値の種類が与えられているときに、Sequence.count() メソッドを値の種類ぶんだけ呼び出して数える方法
の3種類を試しました。
入力列の長さは同じにしておいて、その中で取り得る値の種類を増やしていきます。上のコードでは
- 0だけからなる長さ10,000,000の列
- 0, 1が5,000,000個ずつある長さ10,000,000の列
- 0, 1, 2, 3が2,500,000個ずつある長さ10,000,000の列
- 0~7の整数が1,250,000個ずつある長さ10,000,000の列
- (以下略)
というように入力列を変えていきます。
簡単のため、上のようにそれぞれの値が均等に出現するケースで考えることにしました。
(今回の場合に限っていえば、0始まりの連番を数えるタスクなので、カウンタ変数をlistベースにすればdefaultdictより速くできると思いますが。)
Python 2.7の場合
処理時間が3つ並んでいる箇所は、上から順番にCounter, defaultdict, Sequence.countのものです。
$ python2.7 counter_list.py
('# unique values:', 1)
2.5760269165
0.542164087296
0.0341150760651
('# unique values:', 2)
2.54313802719
0.530303955078
0.121192932129
('# unique values:', 4)
2.53363919258
0.527471065521
0.284231901169
('# unique values:', 8)
2.52918481827
0.528441905975
0.73012304306
('# unique values:', 16)
2.51960992813
0.52666592598
1.39528107643
('# unique values:', 32)
2.57073092461
0.527655124664
2.73206114769
('# unique values:', 64)
2.53179192543
0.527481079102
5.43943691254
('# unique values:', 128)
2.55097103119
0.527207136154
10.7969200611
Counterとdefaultdictは取り得る値の種類が多くなってもほぼ処理時間が変わりませんが、Sequence.countの場合は取り得る値の種類に比例して処理時間が長くなります。Sequence.countの呼び出し回数が倍々に増えているわけですから、当然処理時間も倍々に増えていくわけですね。
値の種類が8個になった時点で、Sequence.countはdefaultdictより時間が掛かるようになりました。Sequence.countの恩恵を受けられるのは、せいぜい6種類くらいまでの場合に限られるでしょうか。
Python 3.6の場合
$ python3.6 counter_list.py
# unique values: 1
0.41325897397473454
0.695587629918009
0.02663882402703166
# unique values: 2
0.4152745339088142
0.7006769538857043
0.14070909889414907
# unique values: 4
0.4618919459171593
0.7234666077420115
0.3422027160413563
# unique values: 8
0.4247186812572181
0.6958386925980449
0.8311411123722792
# unique values: 16
0.4215944930911064
0.6860581482760608
1.7771289623342454
# unique values: 32
0.41267539095133543
0.6848959219641984
3.570915916003287
# unique values: 64
0.41819067765027285
0.6896195830777287
7.129963087849319
# unique values: 128
0.4306373633444309
0.7228756221011281
14.110849821940064
defaultdictとCounterの順序がPython 2.7とは逆転していますが(前回の記事参照)、Python 2.7と同様に、8種類になるとSequence.countは最速ではなくなります。
Counterの処理がPython 2.7より速くなっているために、4種類の時点で既にかなり差が詰まっています。
文字列が入力される場合
前回のA, C, G, Tの例は文字列だったので、こちらのケースに該当します。
import collections
import timeit
def count_counter(iterable):
counter = collections.Counter(iterable)
return dict(counter)
def count_defaultdict(iterable):
counter = collections.defaultdict(int)
for x in iterable:
counter[x] += 1
return dict(counter)
def count_seqcount(seq, setvalues):
counter = dict((x, seq.count(x)) for x in setvalues)
return counter
if __name__ == '__main__':
for num_uniqvalues in (2**i for i in range(8)):
seq = "".join(chr(x) for x in range(num_uniqvalues)) * (10000000 // num_uniqvalues)
uniqvalues = set(seq)
print("# unique values:", num_uniqvalues)
print(timeit.timeit("count_counter(seq)", setup='from __main__ import count_counter, seq', number=1))
print(timeit.timeit("count_defaultdict(seq)", setup='from __main__ import count_defaultdict, seq', number=1))
print(timeit.timeit("count_seqcount(seq, uniqvalues)", setup='from __main__ import count_seqcount, seq, uniqvalues', number=1))
リストのときと比較して、seq
の作り方が違うだけです。今回、seq
は文字列です。
- collections.Counter()を使う方法
- collections.defaultdict()を使う方法
- 取り得る文字の種類が与えられているときに、str.count() メソッドを文字の種類ぶんだけ呼び出して数える方法
の3種類の比較になっています。
Python 2.7の場合
$ python2.7 counter_string.py
('# unique values:', 1)
2.64913105965
0.507971048355
0.004310131073
('# unique values:', 2)
2.55145812035
0.507522106171
0.0079140663147
('# unique values:', 4)
2.54508900642
0.505310058594
0.015830039978
('# unique values:', 8)
2.54428601265
0.520057916641
0.0289571285248
('# unique values:', 16)
2.55457091331
0.507193088531
0.0538790225983
('# unique values:', 32)
2.55689311028
0.504297971725
0.100095033646
('# unique values:', 64)
2.60951495171
0.53032207489
0.217819929123
('# unique values:', 128)
2.69009685516
0.531752824783
0.49088716507
str.countはリストより善戦していて、128種類のときにdefaultdictとほぼ同じという感じです。これ以上増やすとdefaultdictの方が速くなりますね。
Python 3.6の場合
$ python3.6 counter_string.py
# unique values: 1
0.4239531490020454
0.6908756061457098
0.0058090160600841045
# unique values: 2
0.44697694201022387
0.6905332137830555
0.022880982141941786
# unique values: 4
0.5058784820139408
0.7603472019545734
0.02937478106468916
# unique values: 8
0.4713318641297519
0.7984626600518823
0.05456399591639638
# unique values: 16
0.5241422927938402
0.8530813110992312
0.10187289072200656
# unique values: 32
0.4731873348355293
0.750194292049855
0.19266901584342122
# unique values: 64
0.4803232508711517
0.7610510247759521
0.41825661808252335
# unique values: 128
0.5159714459441602
0.812115999404341
0.8506077011115849
こちらだと64種類の時点でCounterとほぼ同等になり、128種類でCounterに(defaultdictにも)逆転されます。
まとめ
- リストが入力されるときは、値が4種類程度までの場合
- 文字列が入力されるときは、文字が**128種類(Python 2) / 64種類(Python 3)**程度までの場合
には、リストや文字列のcountメソッドを使うことで、defaultdictやCounterなどの汎用的に使える方法より速くできる可能性があります。
逆に、この範囲に収まらない可能性が高い場合は、素直にdefaultdictやCounterを使っておきましょう。