はてだBlog(仮称)

私的なブログど真ん中のつもりでしたが、気づけばWebサイト系のアプリケーション開発周りで感じたこと寄りの自分メモなどをつれづれ述べています。2020年6月現在、Elasticsearch、pandas、CMSなどに関する話題が多めです。...ですが、だんだんとより私的なプログラムのスニペット置き場になりつつあります。ブログで述べている内容は所属組織で販売している製品などに関するものではなく、また所属する組織の見解を代表するものではありません。

Python itertools.groupby、itertools全般(accumulate他)、collectionsのちょっとつまみ食い

この記事について

itdepends.hateblo.jp

Python/Pandasのgroupbyについては、上記の前の記事で大口(?)を叩いていしまいましたが、そもそも標準ライブラリのitertoolsにgroupbyというメソッドがあるのでこれを機会に入門してみました。

itertoolsのgroupbyおよび、その他のitertools、collectionsの関数などでおもしろそうだと思ったものをつまみ食いしています。

公式へのリファレンスなど

◆itertools.groupby docs.python.org

◆itertools docs.python.org

◆collections docs.python.org

itertools.groupbyの定義

itertools.groupby(iterable, key=None)

keyのところは関数が引数に取れるようです。sortedなどと同様ですね。iterableの一つずつの要素に、keyで指定の関数で値を取り出して、その値を元にグループ化されるということでしょう。

試してみます。

itertools.groupbyの例

import itertools as it

a = list('ababaaacdeb')

# groupby(key引数指定無し)
print([[k, len(list(g))] for k, g in it.groupby(a)])
#[['a', 1], ['b', 1], ['a', 1], ['b', 1], ['a', 3], ['c', 1], ['d', 1], ['e', 1], ['b', 1]]

listの要素そのもの(a,b,..)単位でグループ化されて件数が得られるようです... というところですが、aの項が3つに分かれていますね。

公式の記事にあるとおり、Unixのuniqコマンドと同様にインプットの並びの塊でグループ化されるようです。

よって、並びの単位で同じものが続く件数を得たい場合は、この使い方になりますし、SQLのGROUPBYライクな動作をさせたい場合は、インプットをsortedなどでソートしてやることになります。

# groupby ソートする

a = list('ababaaacdeb')

print([[k, len(list(g))] for k, g in it.groupby(sorted(a))])
#[['a', 5], ['b', 3], ['c', 1], ['d', 1], ['e', 1]]

# a,b,c,d,eのそれぞれの件数が得られた!!

ちなみに上記の相当の結果を得たいだけなら、collections.Counterの方がより直接的です。

# collections.Counter -> このケースだと、ソートしたgroupbyと同等
import collections as cl
print(cl.Counter(a))
#Counter({'a': 5, 'b': 3, 'c': 1, 'd': 1, 'e': 1})

続いてgroupbyに関数を渡してみます。

# groupby再び:  グループ化の条件としてkeyにlambdaを渡すことができる
a = ['yaa', 'xab', 'xxx', 'xbb', 'zcc', 'ycc']
igen = it.groupby(sorted(a, key=lambda x: x[0]), lambda x: x[0])
print([[k, len(list(g))] for k, g in igen])
#[['x', 3], ['y', 2], ['z', 1]]


# 上記のように数えるだけなら、collection.Counterでも同等
print(cl.Counter(map(lambda x:x[0],a)))
#Counter({'x': 3, 'y': 2, 'z': 1})

「lambda x: x[0]」渡したことにより、インプットのリストの各要素の最初の1文字(x,y,z)でグループ化されましたね。

その他の関数例

chain

これは例を見た方が早いでしょう。

私は、似た動作をするものとしては、itertools.flattenの方をよく使うかな。

In [168]: list(it.chain('123','456'))                                                                                                          
Out[168]: ['1', '2', '3', '4', '5', '6']

In [169]: list(it.chain('abc','def','ghij'))                                                                                                   
Out[169]: ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']

In [170]: list(it.chain([[1],[2]],['aa','bb']))                                                                                                
Out[170]: [[1], [2], 'aa', 'bb']

何かと嬉しいジェネレーター(count、cycle) および compress

countとcycleです。

おおよそ名前のイメージのとおりの挙動です。

cnt += 1

のようなコードで可読性が低いと感じるようでしたら、そのような記法からはおさらばできそうです。

# count
g = it.count(10)
print(next(g))
print(next(g))

# cycle
g = it.cycle('ABCD')
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))

g = it.cycle(list('ABCD'))
print(next(g))
print(next(g))
print(next(g))
print(next(g))
print(next(g))

続いて compress です。

# compress ビットマスクのような印象
print(list(it.compress('ABCDE',[1,0,0,1,0]))) 
# A,D

product、permutations、combinations...といった関数もありますが、ここではproductへのリンクに留めておきます。

accumulateなど(filterfalse、takewhile、dropwhileなど兄弟?シリーズ)

accumulate、filterfalse、takewhile、dropwhile、accumlateらはなかなか面白いです。

まず、accumulateの軽めの例。

# accumulate
print(list(it.accumulate([1, 2, 3, 4, 5, 6])))
# 1,3,6,10,15,21

これらは引数が似ていてとっさにどれがどう使えるか条件反射的にわかるようにということで雑な図にしてみました。

f:id:azotar:20200212234741p:plain

In [330]: import itertools as it 
     ...: data = [1, 2, 3, 4, 5, 6, 5, 4, 3, 10] 
     ...: list(it.dropwhile(lambda x: x < 5, data)) 
     ...:  
     ...:  
     ...:  
     ...:                                                                                                                                               
Out[330]: [5, 6, 5, 4, 3, 10]

accumulateも他のもの同様に関数を引数に取れるのですが、map/reduceのreduce風というか、複数引数をとることができますので、細工の余地がありそうです。

# accumulateの例 再び

In [163]: import itertools as it 

In [164]: data                                                                                                                                 
Out[164]: [3, 4, 6, 2, 1, 9, 0, 7, 5, 8]

In [161]: list(it.accumulate(data))                                                                                                            
Out[161]: [3, 7, 13, 15, 16, 25, 25, 32, 37, 45]

In [162]: list(it.accumulate(data,lambda x,y: x + y ))                                                                                         
Out[162]: [3, 7, 13, 15, 16, 25, 25, 32, 37, 45]

In [165]: list(it.accumulate(data,lambda x,y: x + str(y) ,initial='a'))                                                                        
Out[165]: 
['a',
 'a3',
 'a34',
 'a346',
 'a3462',
 'a34621',
 'a346219',
 'a3462190',
 'a34621907',
 'a346219075',
 'a3462190758']

In [167]: list(it.accumulate(data,lambda x,y: x + ( str(y) if y < 7 else '') ,initial='a'))                                                    
Out[167]: 
['a',
 'a3',
 'a34',
 'a346',
 'a3462',
 'a34621',
 'a34621',
 'a346210',
 'a346210',
 'a3462105',
 'a3462105']

accumlateを使って、カナとそのカナの行のローマ字の対応表を生成

accumlateを使って、カナとそのカナの行のローマ字の対応表を生成させて見ました。

◆accumulate使わず

import itertools as it

kanaall = [chr(c) for c in range(ord('ァ'), ord('ン') + 1)]
kana = list('ァカサタナハマャラヮン')
roma = 'a,ka,sa,ta,na,ha,ma,ya,ra,wa'.split(',')
kanaxroma = {k: r for k, r in zip(kana, roma)}


kashira_roma = {}
for k in kanaall:
    if kanaxroma.get(k) :
        kashira_roma[k] = tmp = kanaxroma[k]
    else: 
        kashira_roma[k] = tmp 

print(kashira_roma)

◆accumulateを使う

import itertools as it

kanaall = [chr(c) for c in range(ord('ァ'), ord('ン') + 1)]
kana = list('ァカサタナハマャラヮン')
roma = 'a,ka,sa,ta,na,ha,ma,ya,ra,wa'.split(',')
kanaxroma = {k: r for k, r in zip(kana, roma)}
_roma = it.accumulate(kanaall, lambda x, y: (y in kanaxroma) and kanaxroma[y] or x, initial='')
next(_roma) # 1要素目はダミーなのでスキップする
print({k:v for k,v in zip(kanaall,_roma)})

◆出力結果

どちらも下記の結果が得られます。

私の実力の都合でそれほど面白い事例にはなりませんでしたが...

{'ァ': 'a', 'ア': 'a', 'ィ': 'a', 'イ': 'a', 'ゥ': 'a', 'ウ': 'a', 'ェ': 'a', 'エ': 'a', 'ォ': 'a', 'オ': 'a', 'カ': 'ka', 'ガ': 'ka', 'キ': 'ka', 'ギ': 'ka', 'ク': 'ka', 'グ': 'ka', 'ケ': 'ka', 'ゲ': 'ka', 'コ': 'ka', 'ゴ': 'ka', 'サ': 'sa', 'ザ': 'sa', 'シ': 'sa', 'ジ': 'sa', 'ス': 'sa', 'ズ': 'sa', 'セ': 'sa', 'ゼ': 'sa', 'ソ': 'sa', 'ゾ': 'sa', 'タ': 'ta', 'ダ': 'ta', 'チ': 'ta', 'ヂ': 'ta', 'ッ': 'ta', 'ツ': 'ta', 'ヅ': 'ta', 'テ': 'ta', 'デ': 'ta', 'ト': 'ta', 'ド': 'ta', 'ナ': 'na', 'ニ': 'na', 'ヌ': 'na', 'ネ': 'na', 'ノ': 'na', 'ハ': 'ha', 'バ': 'ha', 'パ': 'ha', 'ヒ': 'ha', 'ビ': 'ha', 'ピ': 'ha', 'フ': 'ha', 'ブ': 'ha', 'プ': 'ha', 'ヘ': 'ha', 'ベ': 'ha', 'ペ': 'ha', 'ホ': 'ha', 'ボ': 'ha', 'ポ': 'ha', 'マ': 'ma', 'ミ': 'ma', 'ム': 'ma', 'メ': 'ma', 'モ': 'ma', 'ャ': 'ya', 'ヤ': 'ya', 'ュ': 'ya', 'ユ': 'ya', 'ョ': 'ya', 'ヨ': 'ya', 'ラ': 'ra', 'リ': 'ra', 'ル': 'ra', 'レ': 'ra', 'ロ': 'ra', 'ヮ': 'wa', 'ワ': 'wa', 'ヰ': 'wa', 'ヱ': 'wa', 'ヲ': 'wa', 'ン': 'wa'}

最後ですが、この流れでやや唐突ですが、collectionsの nametupleです。

私は他の言語でいうenumを比較的好むので、nametupleはわりかし好きな関数です。

In [333]: from collections import namedtuple 
     ...:  
     ...: Rect = namedtuple('Rect', ('x', 'y', 'width', 'height', )) 
     ...: rect = Rect(1, 2, 3, 4)                                                                                                                       

In [334]: rect.height                                                                                                                                   
Out[334]: 4

In [335]:                                                                           

さいごに

公式の下記のレシピが、itertools自体およびちょっとしたPythonicなコードとして非常に参考になると思いました。

docs.python.org