やりたいこと
以下のような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。
- 全テーブルを結合し、本の名前と一意に対応する
label
を作成し、numpy.bincount
で重み付きカウントする。 - 各テーブル毎に
collections.Counter
オブジェクトを作成し、全テーブルのCounter
オブジェクトを足す。 - for文で辞書への 要素の追加 と 値の更新 を行う。
3'. for文の代わりにreduce
を使う。
※追記
コメントで教えていただいた 3 を追加しました。
1. numpy.bincount を使う
numpy
のbincount
関数を使うことで、入力に重み付けをしながらカウントすることができます。
参考: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
の作成にはsklearn
のLabelEncoder
を使いました。
コード
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に設定すると、LabelEncoder
のfit_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の概要
標準モジュールであるcollections
のCounter
モジュールは、重み付けなしのカウントを行うために紹介されることが多いと思います。
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の意味
[カテゴリ変数のエンコーディング]
(https://qiita.com/ground0state/items/f516b97c7a8641e474c4)
[【Python】リストの要素の数え上げ、collections.Counterの使い方]
(https://qiita.com/ellio08/items/259388b511e24625c0d7)
[PythonのCounterで要素の出現回数を調べる様々な方法]
(https://www.headboost.jp/python-counter/)
[[Python]関数引数の*(star)と**(double star)]
(https://qiita.com/supersaiakujin/items/faee48d35f8d80daa1ac)
[関数型プログラミング入門]
(https://postd.cc/an-introduction-to-functional-programming/)