search
LoginSignup
3

More than 1 year has passed since last update.

posted at

updated at

Pythonでリストに含まれる各要素の出現回数を重み付きで数える方法

やりたいこと

以下のような2つのリストが与えられた時に、aに含まれる各要素の出現回数をbの値で重み付けしてカウントしたい。
Pythonは3.7.5です。

a = ["A", "B", "C", "A"]
b = [ 1 ,  1 ,  2 ,  2 ]

c = hoge(a, b)
print(c)
出力
{"A": 3, "B": 1, "C": 2}  # こんな出力がほしい

# keyとvalueは別々でもいいけど
# (["A", "B", "C"], [3, 1, 2])

※追記:コメントで上記課題に対するシンプルな実装を紹介していただきました。

やりたいことの具体例

書店で今までに売れた本の冊数を、各本ごとにカウントしたいとします。1
ただし、手元にあるのは既に月ごとに集計された複数のテーブルデータのみ。
簡単のために以下の二つのcsvファイルをイメージしてみます。

■ 2020_01.csv

本の名前 売れた冊数
Book_A 1
Book_B 2
Book_C 3

■ 2020_02.csv

本の名前 売れた冊数
Book_A 2
Book_C 1
Book_D 3

この2つのデータを結合すると、"やりたいこと"に書いたような"要素"と"重み"を持ったカウントの問題になります。

方法

以下の3つの手法で出来ました。
どっちが良いか、別の方法など教えていただけると嬉しいです2

  1. 全テーブルを結合し、本の名前と一意に対応するlabelを作成し、numpy.bincountで重み付きカウントする。
  2. 各テーブル毎にcollections.Counterオブジェクトを作成し、全テーブルのCounterオブジェクトを足す。
  3. for文で辞書への 要素の追加 と 値の更新 を行う。
    3'. for文の代わりにreduceを使う。

※追記
コメントで教えていただいた 3 を追加しました。

1. numpy.bincount を使う

numpybincount関数を使うことで、入力に重み付けをしながらカウントすることができます。
参考:numpy.bincountのweightの意味

ただし、np.bincountに入力する各要素は負でない整数である必要があります。

numpy.bincount(x, weights=None, minlength=0)
Count number of occurrences of each value in array of non-negative ints.

x : array_like, 1 dimension, nonnegative ints
---- Input array.
weights : array_like, optional
---- Weights, array of the same shape as x.
minlength : int, optional
---- A minimum number of bins for the output array.
---- New in version 1.6.0.

そこで、np.bincountを使うために、本の名前と一意に対応するlabelを用意します。
labelの作成にはsklearnLabelEncoderを使いました。

コード

import pandas as pd
import numpy as np
from sklearn.preprocessing import LabelEncoder

# データの準備
df_01 = pd.DataFrame([["Book_A", 1],
                      ["Book_B", 2],
                      ["Book_C", 3]],
                     columns=["Name", "Count"])
df_02 = pd.DataFrame([["Book_A", 2],
                      ["Book_C", 1],
                      ["Book_D", 3]],
                     columns=["Name", "Count"])

# テーブルの結合
df_all = pd.concat([df_01, df_02])
# 中身はこんな感じです。
# |  | Name | Count |
# |--:|:--|--:|
# | 0 | Book_A | 1 |
# | 1 | Book_B | 2 |
# | 2 | Book_C | 3 |
# | 0 | Book_A | 2 |
# | 1 | Book_C | 1 |
# | 2 | Book_D | 3 |

# ラベルエンコーディング(LabelEncoder)
from sklearn.preprocessing import LabelEncoder
le = LabelEncoder()
encoded = le.fit_transform(df_all['Name'].values)

# Label列を新規に追加
df_all["Label"] = encoded

# np.bincountで重み付きカウント
# Label列に加えて、重みとしてCount列を入力します。結果が小数点付きなので、intに変換してます。
count_result = np.bincount(df_all["Label"], weights=df_all["Count"]).astype(int)
# resultに対応するNameを取得
name_result = le.inverse_transform(range(len(result)))

# 最終的に欲しい辞書を作成
result = dict(zip(name_result, count_result))
print(result)
出力
{'Book_A': 3, 'Book_B': 2, 'Book_C': 4, 'Book_D': 3}

補足

labelの作成はnp.uniqueを使ってもできます。
np.uniqueの引数return_inverseをTrueに設定すると、LabelEncoderfit_transformと同じ結果を得ることができます。
さらに、対応するName(上記でいうname_result)もまとめて取得できます。

# np.uniqueを使ったラベルエンコーディング
name_result, encoded = np.unique(df_all["Name"], return_inverse=True)
print(encoded)
print(name_result)
出力
[0 1 2 0 2 3]
['Book_A' 'Book_B' 'Book_C' 'Book_D']

また、np.bincountを使わなくてもfor文を回すことで重み付けカウントは可能です3

# 欲しい辞書と同じ長さのゼロ埋め配列を作成
unique_length = len(name_result)
count_result = np.zeros(unique_length, dtype=int)

# テーブル中で encoded が i に一致する行だけ抜き出し、Countの値の合計を求める。
for i in range(unique_length):
    count_result[i] = df_all.iloc[encoded==i]["Count"].sum().astype(int)

result = dict(zip(name_result, count_result))
print(result)
出力
{'Book_A': 3, 'Book_B': 2, 'Book_C': 4, 'Book_D': 3}

2. collections.Counter を使う

collections.Counterの概要

標準モジュールであるcollectionsCounterモジュールは、重み付けなしのカウントを行うために紹介されることが多いと思います。

from collections import Counter

a = ["A", "B", "C", "A"]

# Counterにリストを与えて、重み付けなしのカウントを行う
counter = Counter(a)
print(counter)

# 要素へのアクセスは辞書と同じ
print("A:", counter["A"])
出力
Counter({'A': 2, 'B': 1, 'C': 1})
A: 2

また、今回のように既に集計されている場合、一度辞書型に格納してから渡す事でオブジェクトの作成ができます。

counter = Counter(dict([["Book_A", 1],
                        ["Book_B", 2],
                        ["Book_C", 3]]))
print(counter)
出力
Counter({'Book_A': 1, 'Book_B': 2, 'Book_C': 3})

Counterを使った演算

ところで、このCounterオブジェクトは演算が可能です。
参考:PythonのCounterで要素の出現回数を調べる様々な方法

和の演算により、今回の目的が達成できそうです。

from collections import Counter

a = ["A", "B", "C", "A"]
b = ["C", "D"]

counter_a = Counter(a)
counter_b = Counter(b)

# sum で足し合わせる事が可能
counter_ab = sum([counter_a, counter_b], Counter())
print(counter_ab)
出力
Counter({'A': 2, 'C': 2, 'B': 1, 'D': 1})

コード

from collections import Counter

# データの準備
df_01 = pd.DataFrame([["Book_A", 1],
                      ["Book_B", 2],
                      ["Book_C", 3]],
                     columns=["Name", "Count"])
df_02 = pd.DataFrame([["Book_A", 2],
                      ["Book_C", 1],
                      ["Book_D", 3]],
                     columns=["Name", "Count"])

# Counter の作成
counter_01 = Counter(dict(df_01[["Name", "Count"]].values))
counter_02 = Counter(dict(df_02[["Name", "Count"]].values))

# 和を計算
# *補足: sum の第二引数には初期値を設定する事ができます。
#       今回は初期値に空のCounterを設定しています。デフォルトは 0 (int)です。
result = sum([counter_01, counter_02], Counter())
print(result)
出力
Counter({'Book_C': 4, 'Book_A': 3, 'Book_D': 3, 'Book_B': 2})

どうやらカウント数の降順に勝手にソートされるようです。
※追記:ソートされない事もありました。そもそも辞書なので順番は関係なかったですね。

3. for文で辞書への 要素の追加 と 値の更新 を行う

辞書への要素の追加と値の更新

辞書に同じkeyに対して複数のvalueを与えると、最後に与えたvalueで上書きされます。

print( {"A": 1, "B": 2, "C": 3, "A":10} )
出力
{'A': 10, 'B': 2, 'C': 3}

これを利用すると、あるkeyのカウント値を更新するには 既存の辞書のvalue を取得し、追加するvalue を足して後ろに追加すれば良さそうです。
また、既存の辞書の後ろに要素を追加するには、**(star2つ)を変数の前につけて辞書を展開してやることで実現できます。
参考:[Python]関数引数の*(star)と**(double star)

# 既存の辞書
d = {"A": 1, "B": 2, "C": 3}

# valueを追加する要素
k = "A"
v = 10
# 更新
d = {**d, k: d[k]+v}    # {"A": 1, "B": 2, "C": 3, "A": 1+10} と等価

print(d)
出力
{'A': 11, 'B': 2, 'C': 3}

ただし、辞書に存在しないkeyを指定するとエラーが発生するため、上記のままでは新たなkeyを追加する事ができません。
そこで、辞書オブジェクトの持つ関数get()を使います。get()を使うと、辞書にkeyが存在しないときにデフォルトで返す値を設定できます。
参考:Pythonの辞書のgetメソッドでキーから値を取得(存在しないキーでもOK)

d = {"A": 1, "B": 2, "C": 3}

# 存在するkeyを指定
print(d.get("A", "NO KEY"))
# 存在しないkeyを指定
print(d.get("D", "NO KEY"))
出力
1
NO KEY

これを利用すると、デフォルト値を0に設定すれば、追加と更新を同じように処理する事ができます。
以上の内容を使って、空の辞書に値を追加・更新することで重み付けカウントを行うコードが以下になります。

コード

import pandas as pd
from itertools import chain

# データの準備
import pandas as pd
from itertools import chain
from functools import reduce

# データの準備
df_01 = pd.DataFrame([["Book_A", 1],
                      ["Book_B", 2],
                      ["Book_C", 3]],
                     columns=["Name", "Count"])
df_02 = pd.DataFrame([["Book_A", 2],
                      ["Book_C", 1],
                      ["Book_D", 3]],
                     columns=["Name", "Count"])
# データフレームを辞書に変換
data1 = dict(df_01[["Name", "Count"]].values)
data2 = dict(df_02[["Name", "Count"]].values)

# 関数の定義
chain_items = lambda data : chain.from_iterable( d.items() for d in data )  # 複数の辞書を結合して " key と value のペア " を返す関数
add_elem = lambda acc, e : { **acc, e[0]: acc.get(e[0], 0) + e[1] }  # 辞書に要素の追加、及び値の更新を行う関数

# key が要素、 value が重みの辞書を複数受け取り、マージする関数
def merge_count(*data) :
    result = {}
    for e in chain_items(data) :
        result = add_elem(result, e)
    return result

print( merge_count(data1, data2) )
出力
{'A': 3, 'B': 2, 'C': 4, 'D': 3}

3' for文の代わりにreduceを使う

reduceを使うとfor文を書かずとも繰り返し処理が可能です。
reduceは、以下の引数をとります。

  • 第一引数: 関数。ただし引数に 前回までの計算結果 と 今回の値 を取るようにする。
  • 第二引数: ループ可能なオブジェクト(リスト、ジェネレータ等)
  • 第三引数(オプション): 初期値。デフォルトは 0
from functools import reduce

func = lambda ans, x: ans * x
a = [1, 2, 3, 4]
start = 10

print(reduce(func, a, start))
出力
240  #    10*1 = 10
     # -> 10*2 = 20
     # -> 20*3 = 60
     # -> 60*4 = 240

reduceを使って上記のmerge_countを再現すると以下のようになります。

from functools import reduce

merge_count = lambda *data : reduce( add_elem, chain_items(data), {} )    # 上記の merge_count と等価
print( merge_count(data1, data2) )
出力
{'A': 3, 'B': 2, 'C': 4, 'D': 3}

reduceについては下記のサイトが非常に参考になりました。
参考:関数型プログラミング入門

※方法 3 はコメントで教えていただきました。

参考にしたページ

numpy.bincountのweightの意味
カテゴリ変数のエンコーディング

【Python】リストの要素の数え上げ、collections.Counterの使い方
PythonのCounterで要素の出現回数を調べる様々な方法

[Python]関数引数の*(star)と**(double star)
関数型プログラミング入門


  1. 伝わりやすいように適当な具体例を上げましたが、実際には複数文書の形態素解析結果を集計するのに利用しました。 

  2. 実行速度とかメモリ効率とか…。 

  3. 自分の知識ではfor文を書く以外思いつきませんでした…(リスト内包表記は除く)。  

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
What you can do with signing up
3