こんな人にオススメ
配列にどんな要素があって、その要素がいくつあるのか知りたい!
ループを回してdict
に要素を入れていくとかいう面倒なことはなしで。
ということで、今回はpythonを使用して配列内の要素名とその個数を集計するcollections.Counter
を解説する。簡単に種類別の個数にできるのでかなり便利。
執筆者は昔、これを知らなかったがために、いちいち全要素をループさせながら個数を足していた。めちゃくちゃコードが面倒だしめちゃくちゃ時間がかかってた。知るは正義。
python環境は以下。
- Python 3.9.6
- numpy 1.21.1
- plotly 5.1.0
- plotly-orca 3.4.2
作成したコード全文
下準備
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
する場合もあるけど、ここではimport
はcollections
だけとして、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_common
でtuple
出力もする。
引数はcollections.Counter
を適用するlst
と、その中で個数を名指しで抽出したい要素名のpickup
を設定。
collections.Counter
はdict
と同じように扱うことができるので、.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})
要素の削除はdict
のkeys
を選択するように[]
を使用して指定、それを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.Counter
はdict
のようなものなので、.keys
でlist
にすることが可能。また、直接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一覧をカッコで出力
続きを見る