Python講座 / collectionsとitertoolsで集計・反復する

collectionsは件数や集計向けのコンテナー、itertoolsは反復の流れを組み合わせる道具を提供します。短く書けるかだけでなく、順序、既定値、イテレーターの消費方法に合わせて選びます。

件数と値の合計を区別する

Counterは要素の出現回数を数えます。defaultdictは存在しないキーを角括弧で参照した際、factoryで既定値を作ります。defaultdict(int)なら0から加算でき、分類別の金額をまとめやすくなります。

不足するキーを読むだけで辞書へ追加したくないなら、通常の辞書のgetが意図に合う場合があります。便利な道具を使っても、何を数え、何を加算するかというデータの意味は別に確認します。

辞書による集計と反復子の消費は、辞書でキーと値を管理するで扱う規則とつながります。

件数・合計・グループ化を実行する

collections_demo.pyを保存し、python collections_demo.pyを実行します。

from collections import Counter, defaultdict
from itertools import chain, groupby

rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
counts = Counter(category for category, _ in rows)
totals = defaultdict(int)
for category, amount in rows:
    totals[category] += amount
print(sorted(counts.items()))
print(sorted(totals.items()))
print(list(chain([1, 2], [3])))
for category, group in groupby(sorted(rows), key=lambda row: row[0]):
    print(category, sum(amount for _, amount in group))

予想結果です。foodは2件ですが、金額合計は11500です。

[('bus', 1), ('food', 2)]
[('bus', 2500), ('food', 11500)]
[1, 2, 3]
bus 2500
food 11500

groupbyは連続する同じキーをまとめる

itertools.groupbyは同じキーが連続する間を一つのグループにします。SQLのGROUP BYのように離れた同一キーを自動で全体集計するわけではありません。この例では並べ替えて同じ分類を隣接させます。

グループのイテレーターは元の反復の流れを共有します。次のグループへ進んだ後、前のグループを再び読めるとは限りません。必要な合計をすぐ計算するか、必要な値をリストに保存してから進みます。

消費とメモリーの使い方を確認する

chainは複数の反復を順につなぎ、isliceはイテレーターの一部分を選べます。ただし最後にlistへ変換すれば、その結果全体を保持します。無限に続く反復にはisliceや明示的な終了条件が必要です。

groupbyの入力からsortedを外し、foodが二つのグループとして出ることを確認してから戻します。出現回数と金額の合計を取り違えないよう、期待値と照合してください。基になる消費の動きはジェネレーターでも確認できます。

keyは各要素から比較・分類の基準を取り出す関数です。lambda row: row[0] は下のcategory関数と同じ働きの匿名関数です。groupbyは連続する同じキーだけをまとめるため、未整列ではfoodが二つに分かれます。同じkeyで整列するとfoodの合計11,500が一つになります。グループの反復子は元を共有するため、次へ進む前に消費します。

全体を groups.py に保存し、同じ練習フォルダーで python groups.py を実行します。

from itertools import groupby
rows = [("food", 3500), ("bus", 2500), ("food", 8000)]
def category(row):
    return row[0]
for label, data in [("original", rows), ("sorted", sorted(rows, key=category))]:
    print(label)
    for name, group in groupby(data, key=category):
        print(name, sum(amount for _, amount in group))
print(category(rows[0]) == (lambda row: row[0])(rows[0]))

予想結果:

original
food 3500
bus 2500
food 8000
sorted
bus 2500
food 11500
True

動作と適用条件の公式説明:Python: collections.defaultdict · Python: itertools.groupby

関連する公式リファレンス