閉じる

PythonのCounter同士で計算する方法(足し算・引き算)

collections.Counterは要素の出現回数を扱うのに便利ですが、Counter同士で計算するときは演算子とメソッドで結果が異なる点に注意が必要です。

特に+と-は非破壊で正のカウントのみ返す一方、updateとsubtractは破壊的で0や負数も保持します。

本記事では、初心者の方にも分かるように、実行可能なサンプルを交えながら丁寧に解説します。

Pythonのcollections.Counterの基本

Counterとは

collections.Counterは辞書(dict)のサブクラスで、ハッシュ可能な要素の「個数(カウント)」を手軽に扱えるデータ構造です。

文字列やリストから頻度を数えたり、在庫数やイベント回数などの加減算を行う用途に向いています。

カウントは整数が基本ですが、実は0や負数の値も保持できます(ただし、後述の演算子では0や負数が除外される場合があります)。

サンプルCounterを用意する

以下の2つのCounterを例に、以降の足し算・引き算を説明していきます。

あえて0や負数になる可能性がある値を含め、挙動の違いが分かるようにしています。

Python
# サンプルの準備
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以下のキーは結果から除外します。

つまり「正のカウントだけを残す足し算」です。

Python
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も保持されます。

Python
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や負数も保持するか」を選ぶのがポイントです。

Python
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以下になるキーは除外します。

つまり「正の差だけを残す引き算」です。

Python
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})

banana1 - 2 = -1で負になり除外、citrus0 - 5 = -5相当でやはり除外されます。

subtractで差分を取る(負のカウントを保持)

Counter.subtract()破壊的で、差分をそのまま反映します。

負数や0も保持されるため、どこで不足が出ているかを追跡したいときに便利です。

Python
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を返します。

Python
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や負数を取り除きたい場合は、以下のいずれかが便利です。

Python
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として扱われる点を押さえつつ、必要に応じて+cc += Counter()でクリーニングすると、意図した集計結果を保ちやすくなります。

用途に即した演算子とメソッドを選択し、データの意味を損なわない計算を心掛けてください。

この記事を書いた人
エーテリア編集部
エーテリア編集部

人気のPythonを初めて学ぶ方向けに、文法の基本から小さな自動化まで、実際に手を動かして理解できる記事を書いています。

クラウドSSLサイトシールは安心の証です。

URLをコピーしました!