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

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

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

c = hoge(a, b)
print(c)
```

```python:出力
{"A": 3, "B": 1, "C": 2}  # こんな出力がほしい

# keyとvalueは別々でもいいけど
# (["A", "B", "C"], [3, 1, 2])
```
※追記：[コメント](https://qiita.com/minetta30/items/cc3f1b3836ceca4e3971#comment-165b055404c4c2d02c48)で上記課題に対するシンプルな実装を紹介していただきました。
# やりたいことの具体例
書店で今までに売れた本の冊数を、各本ごとにカウントしたいとします。[^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`を使う。

※追記
[コメント](https://qiita.com/minetta30/items/cc3f1b3836ceca4e3971#comment-c06191265a976e267421)で教えていただいた 3 を追加しました。

## 1. numpy.bincount を使う
`numpy`の`bincount`関数を使うことで、入力に重み付けをしながらカウントすることができます。
参考：[numpy.bincountのweightの意味](http://koshian2.hatenablog.jp/entry/2018/04/02/153120)

ただし、`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`を使いました。

### コード
```python
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)
```

```python:出力
{'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`)もまとめて取得できます。

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

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

```python
# 欲しい辞書と同じ長さのゼロ埋め配列を作成
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)
```
```python:出力
{'Book_A': 3, 'Book_B': 2, 'Book_C': 4, 'Book_D': 3}
```

## 2. collections.Counter を使う
### collections.Counterの概要
標準モジュールである`collections`の`Counter`モジュールは、**重み付けなし**のカウントを行うために紹介されることが多いと思います。

```python
from collections import Counter

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

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

# 要素へのアクセスは辞書と同じ
print("A:", counter["A"])
```
```python:出力
Counter({'A': 2, 'B': 1, 'C': 1})
A: 2
```
また、今回のように既に集計されている場合、一度辞書型に格納してから渡す事でオブジェクトの作成ができます。

```python
counter = Counter(dict([["Book_A", 1],
                        ["Book_B", 2],
                        ["Book_C", 3]]))
print(counter)
```
```python:出力
Counter({'Book_A': 1, 'Book_B': 2, 'Book_C': 3})
```
### Counterを使った演算
ところで、この`Counter`オブジェクトは演算が可能です。
参考：[PythonのCounterで要素の出現回数を調べる様々な方法](https://www.headboost.jp/python-counter/#24_Counter)

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

```python
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)
```
```python:出力
Counter({'A': 2, 'C': 2, 'B': 1, 'D': 1})
```
### コード
```python
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)
```
```python:出力
Counter({'Book_C': 4, 'Book_A': 3, 'Book_D': 3, 'Book_B': 2})
```
~~どうやらカウント数の降順に勝手にソートされるようです。~~
※追記：ソートされない事もありました。そもそも辞書なので順番は関係なかったですね。

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

### 辞書への要素の追加と値の更新
辞書に同じ`key`に対して複数の`value`を与えると、最後に与えた`value`で上書きされます。

```python
print( {"A": 1, "B": 2, "C": 3, "A":10} )
```
```python:出力
{'A': 10, 'B': 2, 'C': 3}
```
これを利用すると、ある`key`のカウント値を更新するには **既存の辞書のvalue** を取得し、**追加するvalue** を足して後ろに追加すれば良さそうです。
また、既存の辞書の後ろに要素を追加するには、\*\*(star2つ)を変数の前につけて辞書を展開してやることで実現できます。
参考：[\[Python\]関数引数の\*(star)と\*\*(double star)](https://qiita.com/supersaiakujin/items/faee48d35f8d80daa1ac)


```python
# 既存の辞書
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)
```
```python:出力
{'A': 11, 'B': 2, 'C': 3}
```
ただし、辞書に存在しない`key`を指定するとエラーが発生するため、上記のままでは新たな`key`を追加する事ができません。
そこで、辞書オブジェクトの持つ関数`get()`を使います。`get()`を使うと、辞書に`key`が存在しないときにデフォルトで返す値を設定できます。
参考：[Pythonの辞書のgetメソッドでキーから値を取得（存在しないキーでもOK）](https://note.nkmk.me/python-dict-get/)

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

# 存在するkeyを指定
print(d.get("A", "NO KEY"))
# 存在しないkeyを指定
print(d.get("D", "NO KEY"))
```
```python:出力
1
NO KEY
```
これを利用すると、デフォルト値を`0`に設定すれば、追加と更新を同じように処理する事ができます。
以上の内容を使って、空の辞書に値を追加・更新することで重み付けカウントを行うコードが以下になります。

### コード
```python
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) )
```
```python:出力
{'A': 3, 'B': 2, 'C': 4, 'D': 3}
```
### 3' for文の代わりに`reduce`を使う
`reduce`を使うとfor文を書かずとも繰り返し処理が可能です。
`reduce`は、以下の引数をとります。

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

```python
from functools import reduce

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

print(reduce(func, a, start))
```
```python:出力
240  #    10*1 = 10
     # -> 10*2 = 20
     # -> 20*3 = 60
     # -> 60*4 = 240
```
`reduce`を使って上記の`merge_count`を再現すると以下のようになります。

```python
from functools import reduce

merge_count = lambda *data : reduce( add_elem, chain_items(data), {} )    # 上記の merge_count と等価
print( merge_count(data1, data2) )
```
```python:出力
{'A': 3, 'B': 2, 'C': 4, 'D': 3}
```
`reduce`については下記のサイトが非常に参考になりました。
参考：[関数型プログラミング入門](https://postd.cc/an-introduction-to-functional-programming/)

※方法 3 は[コメント](https://qiita.com/minetta30/items/cc3f1b3836ceca4e3971#comment-c06191265a976e267421)で教えていただきました。

# 参考にしたページ
[numpy.bincountのweightの意味](http://koshian2.hatenablog.jp/entry/2018/04/02/153120)
[カテゴリ変数のエンコーディング]
(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/)

[^1]: 伝わりやすいように適当な具体例を上げましたが、実際には複数文書の形態素解析結果を集計するのに利用しました。
[^2]: 実行速度とかメモリ効率とか…。
[^3]: 自分の知識ではfor文を書く以外思いつきませんでした…（リスト内包表記は除く）。