collections.Counter
は要素の出現回数を扱うのに便利ですが、Counter同士で計算するときは演算子とメソッドで結果が異なる点に注意が必要です。
特に+と-は非破壊で正のカウントのみ返す一方、updateとsubtractは破壊的で0や負数も保持します。
本記事では、初心者の方にも分かるように、実行可能なサンプルを交えながら丁寧に解説します。
Pythonのcollections.Counterの基本
Counterとは
collections.Counter
は辞書(dict)のサブクラスで、ハッシュ可能な要素の「個数(カウント)」を手軽に扱えるデータ構造です。
文字列やリストから頻度を数えたり、在庫数やイベント回数などの加減算を行う用途に向いています。
カウントは整数が基本ですが、実は0や負数の値も保持できます(ただし、後述の演算子では0や負数が除外される場合があります)。
サンプルCounterを用意する
以下の2つのCounter
を例に、以降の足し算・引き算を説明していきます。
あえて0や負数になる可能性がある値を含め、挙動の違いが分かるようにしています。
# サンプルの準備
from collections import Counter
c1 = Counter({"apple": 2, "banana": 1, "cherry": -1})
c2 = Counter({"apple": 1, "banana": -1, "durian": 2})
print("c1:", c1)
print("c2:", c2)
c1: Counter({'apple': 2, 'banana': 1, 'cherry': -1})
c2: Counter({'durian': 2, 'apple': 1, 'banana': -1})
Counter同士の足し算(+)
+演算子の挙動(0や負数は除外)
+
は非破壊で新しいCounter
を返し、合計が0以下のキーは結果から除外します。
つまり「正のカウントだけを残す足し算」です。
from collections import Counter
c1 = Counter({"apple": 2, "banana": 1, "cherry": -1})
c2 = Counter({"apple": 1, "banana": -1, "durian": 2})
result_plus = c1 + c2 # 非破壊。0や負数になるキーは落ちる
print(result_plus)
# 元のc1, c2は変更されない
print("c1 after +:", c1)
print("c2 after +:", c2)
Counter({'apple': 3, 'durian': 2})
c1 after +: Counter({'apple': 2, 'banana': 1, 'cherry': -1})
c2 after +: Counter({'durian': 2, 'apple': 1, 'banana': -1})
この例ではbanana
の合計が1 + (-1) = 0
となるため結果から除外され、cherry
はもともと負数なのでやはり残りません。
updateで合算する(破壊的・負数も許容)
Counter.update()
は破壊的(元のオブジェクトを書き換え)に合算します。
マッピングを渡すと、その値がそのまま加算されるため、負数や0も保持されます。
from collections import Counter
c1 = Counter({"apple": 2, "banana": 1, "cherry": -1})
c2 = Counter({"apple": 1, "banana": -1, "durian": 2})
c1_copy = c1.copy()
c1_copy.update(c2) # 破壊的に合算(負数や0も保持)
print(c1_copy)
Counter({'apple': 3, 'durian': 2, 'banana': 0, 'cherry': -1})
在庫の増減を正確に記録したいときなど、マイナスや0を含めて状態を残す必要がある場合に向いています。
なお、update
にイテラブル(例:文字列やリスト)を渡すと、各要素の出現回数が加算されるため、値は自然に非負になります。
合算の実例(頻度の合体/在庫の集計)
頻度の合体は+
が安全です。
負数や0を除外し、純粋な「出現回数の合算」ができます。
一方、在庫の集計では、返品や不良などを負数で記録したいケースがあり、update
のほうが都合が良いことがあります。
目的に応じて「正の値だけ残すか」「0や負数も保持するか」を選ぶのがポイントです。
from collections import Counter
# 頻度の合体(+: 正の合計のみ残る)
log_a = Counter({"INFO": 5, "WARN": 2})
log_b = Counter({"INFO": 3, "ERROR": 1, "WARN": -2}) # 集計調整で負数を含むケース
merged_freq = log_a + log_b
print("merged_freq:", merged_freq)
# 在庫の集計(update: 0や負数を保持)
stock = Counter({"A": 10, "B": 3})
delta = Counter({"A": -2, "C": 5}) # 出荷(-2), 新規入荷(+5)
stock.update(delta) # 破壊的
print("stock after update:", stock)
merged_freq: Counter({'INFO': 8, 'ERROR': 1})
stock after update: Counter({'A': 8, 'C': 5, 'B': 3})
Counter同士の引き算(-)
-演算子の挙動(結果<=0は除外)
-
も非破壊で、結果が0以下になるキーは除外します。
つまり「正の差だけを残す引き算」です。
from collections import Counter
c3 = Counter({"apple": 3, "banana": 1})
c4 = Counter({"apple": 1, "banana": 2, "citrus": 5})
result_minus = c3 - c4
print(result_minus)
# 元は変わらない
print("c3 after -:", c3)
print("c4 after -:", c4)
Counter({'apple': 2})
c3 after -: Counter({'apple': 3, 'banana': 1})
c4 after -: Counter({'apple': 1, 'banana': 2, 'citrus': 5})
banana
は1 - 2 = -1
で負になり除外、citrus
は0 - 5 = -5
相当でやはり除外されます。
subtractで差分を取る(負のカウントを保持)
Counter.subtract()
は破壊的で、差分をそのまま反映します。
負数や0も保持されるため、どこで不足が出ているかを追跡したいときに便利です。
from collections import Counter
c3 = Counter({"apple": 3, "banana": 1})
c4 = Counter({"apple": 1, "banana": 2, "citrus": 5})
c3_copy = c3.copy()
c3_copy.subtract(c4) # 破壊的。負数や0も保持
print(c3_copy)
Counter({'apple': 2, 'banana': -1, 'citrus': -5})
欠けているキーの扱い(存在しない要素は0)
存在しないキーのカウントは0として扱われます。
これは演算でも同様です。
辞書アクセス時も、存在しないキーは0
を返します。
from collections import Counter
c = Counter({"x": 2})
print("missing:", c["missing"]) # 存在しないキーは0
print("x before:", c["x"])
c.subtract({"x": 1, "y": 3}) # yは0からの減算として扱われる
print("x after:", c["x"])
print("y after:", c["y"])
missing: 0
x before: 2
x after: 1
y after: -3
よくある落とし穴とベストプラクティス
+と-は非破壊、update/subtractは破壊的
演算子(+, -)は新しいCounterを返す非破壊で、メソッド(update, subtract)は元を直接書き換える破壊的です。
元データを保ちたいときは.copy()
を使ってから更新するか、演算子を選ぶと安全です。
0や負数の扱いに注意する
+, – の結果は0や負数のキーが除外され、update, subtract は0や負数も残すという違いが最重要ポイントです。
頻度のように「非負のみ」を扱いたいなら+
や-
が自然です。
対して、差異や不足を分析したいならsubtract、入出庫を網羅的に累積したいならupdateを使います。
計算後のクリーニングの考え方
演算や更新の後に0や負数を取り除きたい場合は、以下のいずれかが便利です。
from collections import Counter
c = Counter({"a": 3, "b": 0, "c": -2})
# 1) 単項プラスで正のカウントだけを残す(非破壊)
cleaned1 = +c
print("cleaned1:", cleaned1)
# 2) 空のCounterを足してゼロ・負数を削除(破壊的に行うならc += Counter())
c2 = c.copy()
c2 += Counter()
print("cleaned2:", c2)
# 3) 内包表記で条件フィルタ(自分で条件を定義したい場合)
cleaned3 = Counter({k: v for k, v in c.items() if v > 0})
print("cleaned3:", cleaned3)
cleaned1: Counter({'a': 3})
cleaned2: Counter({'a': 3})
cleaned3: Counter({'a': 3})
なお、elements()
はもともと正のカウントのみを展開するため、0や負数は対象外です。
以下に、代表的な操作の違いをまとめます。
操作 | 破壊的か | 0/負数の保持 | 欠けキーの扱い | 主な用途 |
---|---|---|---|---|
+ | 非破壊 | 除外(正のみ残る) | 0として扱う | 頻度の合体、非負の集計 |
– | 非破壊 | 除外(正のみ残る) | 0として扱う | 正の差分の抽出 |
update | 破壊的 | 保持(0/負も残る) | 0として加算 | 在庫・累積カウントの更新 |
subtract | 破壊的 | 保持(0/負も残る) | 0として減算 | 不足・差異の追跡 |
まとめ
Counter同士の計算は「何を残したいか」で使い分けるのがコツです。
正の値だけを保ちたいなら + や –、0や負数も含めて履歴や差分を残したいなら update や subtractを選びます。
欠けているキーは常に0として扱われる点を押さえつつ、必要に応じて+c
やc += Counter()
でクリーニングすると、意図した集計結果を保ちやすくなります。
用途に即した演算子とメソッドを選択し、データの意味を損なわない計算を心掛けてください。