カテゴリー

当サイトはアフィリエイトプログラムによる収益を得ています〈景品表示法に基づく表記です)

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のグラフ即席作成コード

続きを見る

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】グラフに垂直・水平の塗りつぶし

続きを見る

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

続きを見る

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

続きを見る

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

続きを見る

ガジェット

2023/9/18

【デスクツアー2022下半期】モノは少なく、でも効率的に Desk Updating #0

今回はガジェットブロガーなのにデスク環境を構築していない執筆者の ...

ライフハック

2023/9/16

【Audible vs YouTube Premium】耳で聴く音声学習コンテンツを比較

ワイヤレスイヤホンが普及し耳で学習することへのハードルが格段に下 ...

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

2023/9/18

【SENNHEISER MOMENTUM True Wireless 3レビュー】全てが整ったイヤホン

今回は高音質・高機能なSENNHEISERのフラグシップ完全ワイヤレスイヤホン「SENNH ...

ライフハック

2023/3/11

【YouTube Premiumとは】メリットしかないから全員入れ

今回はYouTube Premiumを実際に使ってみてどうなのか、どんなメリット/デメリット ...

マウス

2023/8/17

【Logicool MX ERGOレビュー】疲れない作業効率重視トラックボールマウス

こんな人におすすめ トラックボールマウスの王道Logicool MX ERGOが気になるけどऩ ...

ベストバイ

2023/9/18

【ベストバイ2022】今年買って良かったモノのトップ10

2022年ベストバイ この1年を振り返って執筆者は何を買ったのか。ガジェッ& ...

スマホ

2023/1/15

【楽天モバイル×povo2.0の併用】月1,000円の保険付きデュアルSIM運用

こんな人におすすめ 楽天モバイルとpovo2.0のデュアルSIM運用って実際のとこ ...

マウス

2023/9/16

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

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

macOSアプリケーション

2022/9/30

【Chrome拡張機能】便利で効率的に作業できるおすすめの拡張機能を18個紹介する

こんな人におすすめ Chromeの拡張機能を入れたいけど、調べても同じような ...

macOSアプリケーション

2023/5/3

【Automator活用術】Macで生産性を上げる作業の自動化術

今回はMacに標準でインストールされているアプリ「Automator」を使ってできる ...

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

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

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

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

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

  • この記事を書いた人

メガネ

Webエンジニア駆け出し。独学のPythonで天文学系の大学院を修了。常時金欠のガジェット好きでM2 Pro MacBook Pro(30万円) x Galaxy S22 Ultra(17万円)使いの狂人。自己紹介と半生→変わって楽しいの繰り返しレビュー依頼など→お問い合わせ運営者情報、TwitterX@m_ten_pa、 YouTube@megatenpa、 Threads@megatenpa

-Python基礎
-