カテゴリー

Python基礎

【collections.Counter】配列の要素を要素名と個数で勝手に集計

2021年9月3日

こんな人にオススメ

配列にどんな要素があって、その要素がいくつあるのか知りたい!

ループを回してdictに要素を入れていくとかいう面倒なことはなしで。

ということで、今回はpythonを使用して配列内の要素名とその個数を集計するcollections.Counterを解説する。簡単に種類別の個数にできるのでかなり便利。

執筆者は昔、これを知らなかったがために、いちいち全要素をループさせながら個数を足していた。めちゃくちゃコードが面倒だしめちゃくちゃ時間がかかってた。知るは正義。

python環境は以下。

  • Python 3.9.6
  • numpy 1.21.1
  • plotly 5.1.0
  • plotly-orca 3.4.2

運営者のメガネとです。YouTubeTwitterInstagramも運営中。

自己紹介はこちらから、お問い合わせはこちらからお願いいたします。

運営者メガネ

作成したコード全文

下準備

import sys
import collections
import numpy as np
import plotly.graph_objects as go
import plotly.io as pio

sys.path.append('../../')
import plotly_layout_template as template

まずは下準備としてのimport関連。人によってはfrom collections import Counterと書いてimportする場合もあるけど、ここではimportcollectionsだけとして、collections.Counterとして使用する。

また、plotly_layout_templateは自作のplotlyテンプレート。最後にplotlyで応用例のグラフを作成するため導入。これで簡単にキレイなグラフを描くことが可能。

【随時更新 備忘録】plotlyのグラフ即席作成コード

こんな人にオススメ plotlyって{ ...

続きを見る

collections.Counterの出力を確認する関数

def print_counter(lst, pickup):
    counter = collections.Counter(lst)
    print(f"counter: {counter}")

    # 要素で個数の指定可能
    print(f"\\t{pickup}: {counter[pickup]}")

    print(f"\\ttype: {type(counter)}")
    print(f"\\tkeys: {counter.keys()}")
    print(f"\\tvalues: {counter.values()}")
    print(f"\\titems: {counter.items()}")

    # 出現回数順に要素を取得
    # (要素, 出現回数)でtuple出力
    most = counter.most_common()
    print(f"most: {most}")

    # スライスで指定可能
    print(f"\\tmost[0]: {most[0]}")
    print(f"\\tmost[0][0]: {most[0][0]}")

    most2 = counter.most_common(2)
    print(f"\\tmost 2ko: {most2}")

    return counter

まずはcollections.Counterでの出力を確認するための関数を作成。中身やタイプを確認するだけではなく、most_commontuple出力もする。

引数はcollections.Counterを適用するlstと、その中で個数を名指しで抽出したい要素名のpickupを設定。

collections.Counterdictと同じように扱うことができるので、.keysなどで要素名を抽出している。あとは個数を返り値として出力する。

collections.Counterの基本的な使い方

まずは文字の配列、文字列、数値の配列でcollections.Counterを適用する。これが基本的な使い方。

文字の配列でカウント

lst_str = ['c', 'b', 'c', 'd', 'a', 'a', 'c', 'c', 'd', 'd', 'c', 'b', 'a']
counter_lst = print_counter(lst=lst_str, pickup='c')

まずは文字の配列で適用。print_counter関数の出力は以下。

# counter: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})
# 	c: 5
# 	type: <class 'collections.Counter'>
# 	keys: dict_keys(['c', 'b', 'd', 'a'])
# 	values: dict_values([5, 2, 3, 3])
# 	items: dict_items([('c', 5), ('b', 2), ('d', 3), ('a', 3)])
# most: [('c', 5), ('d', 3), ('a', 3), ('b', 2)]
# 	most[0]: ('c', 5)
# 	most[0][0]: c
# 	most 2ko: [('c', 5), ('d', 3)]

存在しない要素名を名指しで抽出しようとしてもその要素数が0になるだけ。

# 存在しない文字の個数なら0と返される
print_counter(lst=lst_str, pickup=1)
# counter: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})
# 	1: 0
# 	type: <class 'collections.Counter'>
# 	keys: dict_keys(['c', 'b', 'd', 'a'])
# 	values: dict_values([5, 2, 3, 3])
# 	items: dict_items([('c', 5), ('b', 2), ('d', 3), ('a', 3)])
# most: [('c', 5), ('d', 3), ('a', 3), ('b', 2)]
# 	most[0]: ('c', 5)
# 	most[0][0]: c
# 	most 2ko: [('c', 5), ('d', 3)]

文字列でカウント

string = ''.join(lst_str)
print(f"string: {string}")
# string: cbcdaaccddcba

今度はさっきのlst_strを結合して文字列にしてみた。これでも要素をカウントできるが、この場合は1文字ずつのカウントとなるので、複数文字列の合体も1文字ずつになる。

counter_str = print_counter(lst=string, pickup='c')
# counter: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})
# 	c: 5
# 	type: <class 'collections.Counter'>
# 	keys: dict_keys(['c', 'b', 'd', 'a'])
# 	values: dict_values([5, 2, 3, 3])
# 	items: dict_items([('c', 5), ('b', 2), ('d', 3), ('a', 3)])
# most: [('c', 5), ('d', 3), ('a', 3), ('b', 2)]
# 	most[0]: ('c', 5)
# 	most[0][0]: c
# 	most 2ko: [('c', 5), ('d', 3)]

記号や空白が入っていても、これらは独立して認識される。よって、下手に文字列の入った配列や文字列で適用するととんでもない出力量になるので注意。

string = 'a, b|b.c/d_"d3a$'
print(f"string: {string}")
# string: a, b|b.c/d_"d3a$
print_counter(lst=string, pickup='|')
# counter: Counter({'a': 2, 'b': 2, 'd': 2, ',': 1, ' ': 1, '|': 1, '.': 1, 'c': 1, '/': 1, '_': 1, '"': 1, '3': 1, '$': 1})
# 	|: 1
# 	type: <class 'collections.Counter'>
# 	keys: dict_keys(['a', ',', ' ', 'b', '|', '.', 'c', '/', 'd', '_', '"', '3', '$'])
# 	values: dict_values([2, 1, 1, 2, 1, 1, 1, 1, 2, 1, 1, 1, 1])
# 	items: dict_items([('a', 2), (',', 1), (' ', 1), ('b', 2), ('|', 1), ('.', 1), ('c', 1), ('/', 1), ('d', 2), ('_', 1), ('"', 1), ('3', 1), ('$', 1)])
# most: [('a', 2), ('b', 2), ('d', 2), (',', 1), (' ', 1), ('|', 1), ('.', 1), ('c', 1), ('/', 1), ('_', 1), ('"', 1), ('3', 1), ('$', 1)]
# 	most[0]: ('a', 2)
# 	most[0][0]: a
# 	most 2ko: [('a', 2), ('b', 2)]

数値でカウント

lst_int = [0, 1, 2, 1, 3, 2, 1, 1, 2, 0, 1, 1, 2]
counter_int = print_counter(lst=lst_int, pickup=1)
# counter: Counter({1: 6, 2: 4, 0: 2, 3: 1})
# 	1: 6
# 	type: <class 'collections.Counter'>
# 	keys: dict_keys([0, 1, 2, 3])
# 	values: dict_values([2, 6, 4, 1])
# 	items: dict_items([(0, 2), (1, 6), (2, 4), (3, 1)])
# most: [(1, 6), (2, 4), (0, 2), (3, 1)]
# 	most[0]: (1, 6)
# 	most[0][0]: 1
# 	most 2ko: [(1, 6), (2, 4)]

文字だけではなく、数値でも同じようにcollections.Counterを適用することができる。この場合も同じように、どの数字がどの個数存在しているかを出力する。

ただし、要素名とその個数がどちらも数字になるので、dictの状態だと見にくいという欠点がある。

要素の削除と追加

続いては要素を削除したり追加したりする。dictと同じように操作可能なので、戸惑うことは少ないだろう。

要素の削除

print(f"counter_str: {counter_str}")
# counter_str: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

# 要素名で削除可能
del counter_str['a']
print(f"counter_str: {counter_str}")
# counter_str: Counter({'c': 5, 'd': 3, 'b': 2})

要素の削除はdictkeysを選択するように[]を使用して指定、それをdelで削除すればいい。そうすると要素自体が削除され、出力されなくなる。

この状態で削除した要素の個数を出力すると、エラーではなく0が出力される。

# 存在しない要素は0
print(f"counter_str['a']: {counter_str['a']}")
# counter_str['a']: 0

要素の追加

# 要素の追加はdictと同じように行える
counter_str['x'] = 10
print(counter_str)
# Counter({'x': 10, 'c': 5, 'd': 3, 'b': 2})

要素の追加もdictと同じように要素名を[]で指定して=でその数を入れたらいい。今回はxを10個で追加した。

要素名をlistで出力

お次は要素名をlistで出力する方法。

dict形式で要素名を出力する

print(f"counter_lst: {counter_lst}")
# counter_lst: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

# この場合はCounterをした配列で登場した順番に並べられる
print(f"list(counter_lst.keys()): {list(counter_lst.keys())}")
# list(counter_lst.keys()): ['c', 'b', 'd', 'a']

# listにするだけでも取り出し可能
# この場合も元の配列で登場した順番に並べられる
print(f"list(counter_lst): {list(counter_lst)}")
# list(counter_lst): ['c', 'b', 'd', 'a']

collections.Counterdictのようなものなので、.keyslistにすることが可能。また、直接listで囲むことでもlistにすることが可能。もちろん要素数の出力も可能。

# valuesをlistで囲むことで要素数を抽出することができる
print(f"list(counter_lst.values()): {list(counter_lst.values())}")
# list(counter_lst.values()): [5, 2, 3, 3]

setで取り出し

# setを使用すると一意の配列が出力される
# ただし、順番はバラバラ
print(f"set(counter_lst): {set(counter_lst)}")
# set(counter_lst): {'c', 'd', 'b', 'a'}

一意な要素を出力するsetを使用することでも要素名の取り出しが可能。シンプルにsetにするだけだと順番がバラバラになる。これもlistで囲むことでlistに変換することが可能。

# listに変換するにはlistで囲む
print(f"list(set(counter_lst)): {list(set(counter_lst))}")
# list(set(counter_lst)): ['c', 'd', 'b', 'a']

elementを使用して変換

# elementを使用することで要素を抽出可能
print(f"counter_lst.elements(): {counter_lst.elements()}")
# counter_lst.elements(): <itertools.chain object at 0x10300cca0>

また、.elementを使用することで全要素の抽出ができる。しかし、このままだと何が何だかわからないのでlistに変換する。また、tupleへの変換も可能

# Counterの個数情報をlistで再現
print(f"list(counter_lst.elements()): {list(counter_lst.elements())}")
# ['c', 'c', 'c', 'c', 'c', 'b', 'b', 'd', 'd', 'd', 'a', 'a', 'a']

# tupleも可能
print(f"tuple(counter_lst.elements()): {tuple(counter_lst.elements())}")
# ('c', 'c', 'c', 'c', 'c', 'b', 'b', 'd', 'd', 'd', 'a', 'a', 'a')

.elementを使用しても元の配列に影響はない。

# 元の配列に影響なし
print(f"counter_lst: {counter_lst}")
# counter_lst: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

要素数の計算

collections.Counterで計算した要素名と要素の配列を使って計算したい時があるかもしれない。ここでは足し算、引き算、掛け算を行う。割り算は要素数が小数になる可能性があるので今回はなし。

加減の計算に使用する新たな要素の配列

# 足し引きしたい要素
other = collections.Counter({'a': 2, 'b': 3, 'c': 1})
print(f"other: {other}")
# other: Counter({'b': 3, 'a': 2, 'c': 1})

otherは以下で解説する加減で使用する要素の配列。適当に数値を決めて作成。

要素の足し算

added = counter_lst + other
print(f"added: {added}")
# added: Counter({'c': 6, 'b': 5, 'a': 5, 'd': 3})

シンプルに足し算すればいい。今回だとaを2コ、bを3コ、cを1コ足し算したことになる。他にも.elementのようなものもあるが、この場合は元の配列が上書きされるのでコピーを作成。

# 上書きされるので複製しておく
counter_lst_cpy = counter_lst.copy()
print(f"counter_lst_cpy before: {counter_lst_cpy}")
# counter_lst_cpy before: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

.updateを使用することで足し算することができる。上書きされるので、元の配列をそのまま活用したい時は便利。

# .updateを使って足し算することも可能
counter_lst_cpy.update(other)
print(f"counter_lst_cpy after: {counter_lst_cpy}")
# counter_lst_cpy after: Counter({'c': 6, 'b': 5, 'a': 5, 'd': 3})

要素の引き算

# カウンター同士で引く
# 足りない分は無視されて表示されなくなる
subbed = counter_lst - other
print(f"subbed: {subbed}")
# subbed: Counter({'c': 4, 'd': 3, 'a': 1})

# 上書きされるので複製しておく
counter_lst_cpy = counter_lst.copy()
print(f"counter_lst_cpy before: {counter_lst_cpy}")
# counter_lst_cpy before: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

# .subtractを使うと、足りない分は負の数に
counter_lst_cpy.subtract(other)
print(f"counter_lst_cpy after: {counter_lst_cpy}")
# counter_lst_cpy after: Counter({'c': 4, 'd': 3, 'a': 1, 'b': -1})

要素の引き算も同じように直接引き算することもできるし、.updateに相当する.subtractを使用しても実現できる。しかし、以下のような違いがある。

  • 直接引き算: 足りなくなった要素は非表示
  • .subtract: 足りなくなった要素は負の数

なので、無くなっても表示したいなら.subtractを使用するのが良いだろう。

要素の掛け算

# シンプルに掛け算するとエラー
counter_lst * 2
# TypeError: unsupported operand type(s) for *: 'Counter' and 'int'

要素の掛け算をする際に、シンプルに掛け算をしてしまうとエラー。ちなみにdictもエラーになる。一旦.elementで全要素を抽出してから、これをlistにして掛け算することで掛け算を実行可能。

# 一旦listに変換して掛け算
lst_2 = list(counter_lst.elements()) * 2
print(f"lst_2: {lst_2}")
# lst_2: ['c', 'c', 'c', 'c', 'c', 'b', 'b', 'd', 'd', 'd', 'a', 'a', 'a', 'c', 'c', 'c', 'c', 'c', 'b', 'b', 'd', 'd', 'd', 'a', 'a', 'a']

# その後に勘定したらいい
counter_lst_2 = collections.Counter(lst_2)
print(f"counter_lst_2: {counter_lst_2}")
# counter_lst_2: Counter({'c': 10, 'd': 6, 'a': 6, 'b': 4})

論理演算子を使用

print(f"counter_lst: {counter_lst}")
# counter_lst: Counter({'c': 5, 'd': 3, 'a': 3, 'b': 2})

other = {'a': 2, 'b': 3, 'c': 1}
print(f"other: {other}")
# other: {'a': 2, 'b': 3, 'c': 1}

counter_element = collections.Counter(other)
# 積集合は一番少ない要素を抽出
# どちらかに存在しない要素は抽出しない
print(f"&: {counter_lst & counter_element}")
# &: Counter({'b': 2, 'a': 2, 'c': 1})

# 羽集合は一番多い要素を抽出
# どちらかに存在しない要素も抽出
print(f"|: {counter_lst | counter_element}")
# |: Counter({'c': 5, 'b': 3, 'd': 3, 'a': 3})

論理演算子は複数種類あるけど、ここでは「かつ」として使用される「&」と「または」で使用される「|」について解説。&を使用すると一番少ない要素数が、|だと一番多い要素数が抽出。

また、&だとどちらか一方に存在しない要素は出力されず、|だとどちらか一方にでも存在していれば出力される。dがその例だ。

collections.Counterを使用した棒グラフへの応用


ということで、最後はcollections.Counterを使用した棒グラフへの応用。具体的にはランダムで作成した数値データの各数の出現回数をplotlyの棒グラフにしたというもの。

データの作成

# 0以上10未満のランダム値を生成
np.random.seed(1)  # 固定のランダム値を作成
data = np.random.randint(0, 10, 100)  # 0から9の間で100データ作成
print(data)
# [5 8 9 5 0 0 1 7 6 9 2 4 5 2 4 2 4 7 7 9 1 7 0 6 9 9 7 6 9 1 0 1 8 8 3 9 8
#  7 3 6 5 1 9 3 4 8 1 4 0 3 9 2 0 4 9 2 7 7 9 8 6 9 3 7 7 4 5 9 3 6 8 0 2 7
#  7 9 7 3 0 8 7 7 1 1 3 0 8 6 4 5 6 2 5 7 8 4 4 7 7 4]

まずはデータの作成。seedでランダム値を固定して作成。データ数は0から9までの間で100データ。これにcollections.Counterを適用して、各数の出現回数を計算。

counter = collections.Counter(data)
print(counter)
# Counter({7: 18, 9: 14, 4: 11, 8: 10, 0: 9, 1: 8, 6: 8, 3: 8, 5: 7, 2: 7})

各数をx、出現回数をyとした。

x = list(counter.keys())
y = list(counter.values())
print(x)
# [5, 8, 9, 0, 1, 7, 6, 2, 4, 3]
print(y)
# [7, 10, 14, 9, 8, 18, 8, 7, 11, 8]

出現回数を棒グラフに

# plotlyでグラフ化
plot = []
d = go.Bar(
    x=x, y=y,
    name='bar',
    hovertemplate='Number: %{x} <br>'
    + '%{y} Counts<br>'
    + "<extra></extra>",
)
plot.append(d)

layout = go.Layout(
    template=template.plotly_layout(),
    title=dict(text='Number vs Counts',),
    xaxis=dict(title='Number',),
    yaxis=dict(title='Counts',),
)

config = template.plotly_config()
fig = go.Figure(data=plot, layout=layout)
fig.show(config=config)

save_name = 'counter_bar'
pio.orca.config.executable = '/Applications/orca.app/Contents/MacOS/orca'
pio.write_html(fig, f"{save_name}.html", config=config,)
pio.write_image(fig, f"{save_name}.png")

ということで、上のコードにて棒グラフが出来上がる。plotlyのグラフについては他の記事を見ていただくとして、collections.Counterを応用すると勘定した配列の簡単なグラフができる。

カウントする機会

今回はcollections.Counterを使用して配列や文字列中の要素がいくつあるのかをカウントする方法について解説した。執筆者はこのようなカウント系を研究でする機会があった。

当時はcollections.Counterを知らなかったしdictも扱えてなかったので、頑張ってifで既にカウントしているか判定して要素数を+1したりしていた。不毛。

collections.Counterを使えばすぐに解決するのはするんだけど、そもそもこれ以降はカウント系ってあまり使っていない。けど、まあどっかで使うっしょ。

関連記事

【plotly&add_vrect, hrect】グラフに垂直・水平の塗りつぶし

こんな人にオススメ ploltyで垂İ ...

続きを見る

【plotly&グラフ内グラフ】plotlyでグラフ内に別グラフやグラフの一部拡大を描画

こんな人にオススメ 以前、mat ...

続きを見る

【plotly&heatmap】go.Heatmapで2次元配列をマップ化

こんな人にオススメ 以前、mat ...

続きを見る

【python3.7以降&dictのkeys】ネスト(入れ子)されたdictのkeys一覧をカッコで出力

こんな人にオススメ dictとやら ...

続きを見る

スイッチボット

2022/9/11

【SwitchBotロックレビュー】これからのスタンダードになりうるスマートロック

こんな人にオススメ SwitchBotからスマートロック「SwitchBotロック」が発売された ...

生活に役立つ

2022/10/25

【メガネ厳選】クソ便利に使っているサービスやアイテム達

このページでは執筆者「メガネ」が実際に使って便利だと感じているサ ...

マウス

2022/9/11

【Logicool MX ERGO vs MX Master 3】ERGOをメインにした決定的な理由

こんな疑問・お悩みを持っている人におすすめ 執筆者はLogicoolのハイエンӠ ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【ながら聴きイヤホン比較】SONY LinkBuds、ambie、BoCoはどれがおすすめ?

こんな人におすすめ 耳を塞がない開放型のイヤホンに完全ワイヤレスӟ ...

macOSアプリケーション

2022/10/15

【M1 Mac】MacBook Proに入れている便利でニッチなアプリを21個紹介する

こんな人におすすめ MacBookを購入してLINEとか必要最低限のアプリは入れた。 ...

完全ワイヤレスイヤホン(TWS)

2022/10/23

【SENNHEISER MOMENTUM True Wireless 3レビュー】高レベルでバランス型の高音質イヤホン

こんな人におすすめ SENNHEISER MOMENTUM True Wireless 3って実際のところどうなの? 評判は良い ...

完全ワイヤレスイヤホン(TWS)

2022/11/21

【SONY WF-1000XM4レビュー】神とゴミのハーフ&ハーフ

こんな人におすすめ SONYのフラグシップモデル「SONY WF-1000XM4」ってどれくらい性 ...

完全ワイヤレスイヤホン(TWS)

2022/8/19

【Nothing ear (1)レビュー】ライトな完成度、アップデートに期待

こんな人にオススメ 完全ワイヤレスイヤホン(TWS)でスケルトンボディ ...

Pythonを学びたいけど独学できる時間なんてない人へのすゝめ

執筆者は大学の研究室・大学院にて独学でPythonを習得した。

でも社会人になったら独学で行うには時間も体力もなくて大変だ。

時間がない社会人だからこそプロの教えを乞うのが効率的。

ここでは色んなタイプに合ったプログラミングスクールの紹介をする。

  • この記事を書いた人

メガネ

ベンチャー企業のWebエンジニア駆け出し。独学のPythonで天文学系の大学院を修了→新卒を1.5年で辞める→転職→今に至る。
常時金欠のガジェット好きでM1 MacBook Pro x Galaxy S22 Ultraの狂人。
人見知りで根暗だったけど、人生楽しもうと思って良い方向に狂う→人生が楽しい

ガジェットのレビューとPythonコードを記事にしています。ぜひ楽しんでください🦊
自己紹介と半生→変わって楽しいの繰り返し

-Python基礎
-