階層的クラスタリングとシルエット係数


階層的クラスタリングは何度も取り扱いましたが、今回は、シルエット係数を用いて、ちょうどよさそうな大きさのクラスターの分割してその中身を見ることを考えてみたいと思います。

# 都道府県別お酒の消費量データを使います

データの詳細は <a href="https://qiita.com/maskot1977/items/453eddf5aa154c04668c">実習用データ</a> を参照のこと。「都道府県別アルコール類の消費量」をダウンロードして用います。


```python
# URL によるリソースへのアクセスを提供するライブラリをインポートする。
import urllib.request 

# ウェブ上のリソースを指定する
url = 'https://raw.githubusercontent.com/maskot1977/ipython_notebook/master/toydata/sake_dataJ.txt'

# 指定したURLからリソースをダウンロードし、名前をつける。
urllib.request.urlretrieve(url, 'sake_dataJ.txt') 
```




    ('sake_dataJ.txt', <http.client.HTTPMessage at 0x1058a3048>)



# pandas を用いたデータの読み込みと正規化

詳細は <a href="https://qiita.com/maskot1977/items/1f6c51965b1bfc341e4b">タブ区切りデータ、コンマ区切りデータ等の読み込み</a> を参照のこと。

```python
import pandas as pd
df = pd.read_csv('sake_dataJ.txt', sep='\t', index_col=0) # データの読み込み
```

今回は、axis=1 と指定することで「行ごとの正規化」を行ないます。


```python
# 行列の正規化
dfs = df.apply(lambda x: (x-x.mean())/x.std(), axis=1).fillna(0)
```

* axis=0 として「列ごとの正規化」をすると、どんなクラスタリング結果になるか
* 正規化を行わないと、どんなクラスタリング結果になるか

ぜひ、後で試してみてください。

# 階層的クラスタリング

階層的クラスタリングについては <a href="https://www.slideshare.net/maskot3/ss-121402523">階層的クラスタリング入門の入門</a> などを参照のこと。

クラスタリングには色んな metric（「距離」の定義）と method （結合方法）があるので、その中からそれぞれ一つずつを選択します。


```python
%matplotlib inline
import matplotlib.pyplot as plt
```


```python
# metric は色々あるので、ケースバイケースでどれかひとつ好きなものを選ぶ。
# method も色々あるので、ケースバイケースでどれかひとつ好きなものを選ぶ。
from scipy.cluster.hierarchy import linkage, dendrogram
result1 = linkage(dfs.iloc[:, :], 
                  #metric = 'braycurtis', 
                  #metric = 'canberra', 
                  #metric = 'chebyshev', 
                  #metric = 'cityblock', 
                  metric = 'correlation', 
                  #metric = 'cosine', 
                  #metric = 'euclidean', 
                  #metric = 'hamming', 
                  #metric = 'jaccard', 
                  #method= 'single')
                  method = 'average')
                  #method= 'complete')
                  #method='weighted')
plt.figure(figsize=(8, 8))
dendrogram(result1, orientation='right', labels=list(df.index), color_threshold=0.01)
plt.title("Dedrogram")
plt.xlabel("Threshold")
plt.grid()
plt.show()
```


![output_8_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/84653e5a-9b93-634a-85ea-caf5824dd923.png)






# 階層的クラスタリングの結果ファイル

上記のコードで、階層クラスタリングの結果を result1 という変数に格納しました。これがどういうデータか見てみましょう。


```python
pd.DataFrame(result1)
```




<div>
<style scoped>
    .dataframe tbody tr th:only-of-type {
        vertical-align: middle;
    }

    .dataframe tbody tr th {
        vertical-align: top;
    }

    .dataframe thead th {
        text-align: right;
    }
</style>
<table border="1" class="dataframe">
  <thead>
    <tr style="text-align: right;">
      <th></th>
      <th>0</th>
      <th>1</th>
      <th>2</th>
      <th>3</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <th>0</th>
      <td>27.0</td>
      <td>28.0</td>
      <td>0.000026</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>1</th>
      <td>36.0</td>
      <td>38.0</td>
      <td>0.000028</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>2</th>
      <td>10.0</td>
      <td>11.0</td>
      <td>0.000037</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>3</th>
      <td>23.0</td>
      <td>35.0</td>
      <td>0.000038</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>4</th>
      <td>7.0</td>
      <td>8.0</td>
      <td>0.000051</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>5</th>
      <td>20.0</td>
      <td>29.0</td>
      <td>0.000061</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>6</th>
      <td>25.0</td>
      <td>46.0</td>
      <td>0.000066</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>7</th>
      <td>39.0</td>
      <td>48.0</td>
      <td>0.000073</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>8</th>
      <td>17.0</td>
      <td>51.0</td>
      <td>0.000094</td>
      <td>3.0</td>
    </tr>
    <tr>
      <th>9</th>
      <td>1.0</td>
      <td>34.0</td>
      <td>0.000131</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>10</th>
      <td>3.0</td>
      <td>32.0</td>
      <td>0.000174</td>
      <td>2.0</td>
    </tr>
    <tr>
      <th>...</th>
      <td>...</td>
      <td>...</td>
      <td>...</td>
      <td>...</td>
    </tr>

  </tbody>
</table>
</div>



まず、今回のデータは、沖縄県を除く４６都道府県からなので、０番目から４５番目までのノードから成ります。これらのノードは、メンバー数 1 のクラスタと見なすことができます。

上の表の０行目は、２７番目のノードと２８番目のノード（ノード間距離＝0.000026）を結合して４６番目のノードにしたことを意味します。この新しく生成されたクラスタのメンバ数は 2 となります。

同様に１行目は、３６番目のノードと３８番目のノード（ノード間距離＝0.000028）を結合して４７番目のノードにしたことを意味します。この新しいクラスタのメンバ数は 2 となります。

同様にどんどん続けると、６行目で、２５番目のノードと４６番目のノードを結合します。この４６番目のノードは、０行目の結合で誕生したノード（クラスタ）です。この新しいクラスタのメンバ数は 3 となります。

... というような、何と何をどういう順番で結合したかという情報が result1 に記述されています。

# 閾値とクラスタ数・平均クラスタサイズの関係

上記のように、距離の短いものから順にどんどん結合していくと、クラスタ数は１つずつ減り、クラスタあたりのメンバ数（クラスタサイズ）は大きくなります。その様子を計算して図示してみましょう。


```python
import numpy as np
import matplotlib.pyplot as plt

# Threshold を変えるとクラスタ数や平均クラスタサイズがどう変わるか調べる
def draw_threshold_dependency(result):
    n_clusters = len(result)
    n_samples = len(result)
    df1 = pd.DataFrame(result)
    x1 = []
    y1 = []
    x2 = []
    y2 = []
    for i in range(len(result) - 1):
        n1 = int(result[i][0])
        n2 = int(result[i][1])
        val = result[i][2]
        n_clusters -= 1
        x1.append(val)
        x2.append(val)
        y1.append(n_clusters)
        y2.append(float(n_samples) / float(n_clusters))

    plt.subplot(2, 1, 1)
    plt.plot(x1, y1, 'yo-')
    plt.title('Threshold dependency of hierarchical clustering')
    plt.ylabel('Num of clusters')
    plt.subplot(2, 1, 2)
    plt.plot(x2, y2, 'ro-')
    plt.xlabel('Threshold')
    plt.ylabel('Ave cluster size')
    plt.show()
```


```python
draw_threshold_dependency(result1)
```


![output_13_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/d1bfa143-73df-cb13-e7b9-28842350912f.png)



# 指定した閾値でクラスタを得る

次のような関数で、指定した閾値でクラスタを得ることができます。


```python
# 指定した thoreshold でクラスタを得る関数を作る
def get_cluster_by_threshold(result, threshold):
    output_clusters = []
    output_cluster_ids = []
    x_result, y_result = result.shape
    n_clusters = x_result + 1
    cluster_id = x_result + 1
    father_of = {}
    x1 = []
    y1 = []
    x2 = []
    y2 = []
    for i in range(len(result) - 1):
        n1 = int(result[i][0])
        n2 = int(result[i][1])
        val = result[i][2]
        n_clusters -= 1
        if val < threshold:
            father_of[n1] = cluster_id
            father_of[n2] = cluster_id

        cluster_id += 1

    cluster_dict = {}
    for n in range(x_result + 1):
        if n not in father_of:
            output_clusters.append([n])
            continue

        n2 = n
        m = False
        while n2 in father_of:
            m = father_of[n2]
            #print [n2, m]
            n2 = m

        if m not in cluster_dict:
            cluster_dict.update({m:[]})
        cluster_dict[m].append(n)

    output_clusters += cluster_dict.values()

    output_cluster_id = 0
    output_cluster_ids = [0] * (x_result + 1)
    for cluster in sorted(output_clusters):
        for i in cluster:
            output_cluster_ids[i] = output_cluster_id
        output_cluster_id += 1

    return output_cluster_ids
```


```python
# 指定した thoreshold でクラスタを得る関数を使う。
clusterIDs = get_cluster_by_threshold(result1, 0.005)
print(clusterIDs)
```

    [0, 0, 0, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0, 0, 2, 1, 1, 1, 3, 1, 1, 0, 0, 1, 1, 0, 0, 0, 0, 1, 1, 1, 1, 0, 0, 1, 0, 0, 0, 0, 1, 0, 4, 4, 5, 5]


上記の clusterIDs には、クラスターID（それが何番目のクラスターに所属するか）が格納されます。今回は、0 - 5 の６つのクラスターに分かれました。ところがこの関数には弱点があります。metric などの変数を変えてクラスタリングすると、その「距離」の定義が変わるので、そのたびに閾値がどのくらいかいちいち確認しながら調整しなければいけないのです。

# 指定したクラスタ数でクラスタを得る

そこで、以下のように、閾値ではなく「いくつのクラスタに分割したいか」で決めることにしてみましょう。


```python
# 指定したクラスタ数でクラスタを得る関数を作る。
def get_cluster_by_number(result, number):
    output_clusters = []
    x_result, y_result = result.shape
    n_clusters = x_result + 1
    cluster_id = x_result + 1
    father_of = {}
    x1 = []
    y1 = []
    x2 = []
    y2 = []
    for i in range(len(result) - 1):
        n1 = int(result[i][0])
        n2 = int(result[i][1])
        val = result[i][2]
        n_clusters -= 1
        if n_clusters >= number:
            father_of[n1] = cluster_id
            father_of[n2] = cluster_id

        cluster_id += 1

    cluster_dict = {}
    for n in range(x_result + 1):
        if n not in father_of:
            output_clusters.append([n])
            continue

        n2 = n
        m = False
        while n2 in father_of:
            m = father_of[n2]
            #print [n2, m]
            n2 = m

        if m not in cluster_dict:
            cluster_dict.update({m:[]})
        cluster_dict[m].append(n)

    output_clusters += cluster_dict.values()

    output_cluster_id = 0
    output_cluster_ids = [0] * (x_result + 1)
    for cluster in sorted(output_clusters):
        for i in cluster:
            output_cluster_ids[i] = output_cluster_id
        output_cluster_id += 1

    return output_cluster_ids
```

次のようにして「１０個のクラスタに分けたい」というふうに指定できます。


```python
clusterIDs = get_cluster_by_number(result1, 10)
print(clusterIDs)
```

    [0, 1, 1, 2, 3, 3, 3, 1, 1, 1, 0, 0, 4, 0, 5, 2, 2, 2, 6, 2, 2, 1, 4, 2, 2, 7, 4, 7, 7, 2, 2, 3, 2, 7, 1, 2, 7, 7, 7, 0, 2, 0, 8, 8, 9, 9]


得られたクラスタのメンバ数の分布は次のようになります。


```python
plt.hist(clusterIDs)
plt.grid()
```


![output_22_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/e7e56b4f-a750-b581-f7a9-8c2ee1d00bd7.png)



# シルエット係数

シルエット係数は、a を「同じクラスタに属するメンバ間の距離の平均」、b を「異なるクラスタに属するメンバ間の距離の平均」としたときに、

* (b - a) / max(b, a)

で表されます。シルエット係数が高いほど、よく分割できている（クラスタ間距離に比べ、クラスタ内距離が十分に短い）ことを示します。


```python
import math
def silhouette_coefficient(clusters, df):
    a_same = []
    b_diff = []
    for i, j in enumerate(clusters):
        vec1 = df.iloc[i, :].values
        for k, l in enumerate(clusters):
            if i < k:
                vec2 = df.iloc[k, :].values
                dist = 0.
                for v1, v2 in zip(vec1, vec2):
                    dist += (v1 - v2) ** 2
                dist = math.sqrt(dist)
                if j == l: # same cluster
                    a_same.append(dist)
                else: # different cluster
                    b_diff.append(dist)
    a = sum(a_same) / len(a_same)
    b = sum(b_diff) / len(b_diff)
    return (b - a) / max(b, a)
```


```python
silhouette_coefficient(clusterIDs, df)
```




    0.6192139990573414

ただし、上記のコードで得られたシルエット係数は、距離の定義を「ユークリッド距離」とした場合のシルエット係数になります。


# シルエット係数のクラスタ数依存性

次のようにして、クラスタ数を変えた時にシルエット係数がどう変わるかを可視化できます。


```python
x = []
y = []
for i in range(2, len(df)):
    x.append(i)
    y.append(silhouette_coefficient(get_cluster_by_number(result1, i), df))
plt.plot(x, y)
plt.xlabel("Num of clusters")
plt.ylabel("silhouette coefficient")
plt.grid()
plt.show()
```


![output_27_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/16aa3748-90d6-461f-9165-47f223e2f319.png)

## 計算の高速化

上記の計算手法は時間がかかります。なぜなら、同じ計算（距離の計算）を何度も行なっているからです。そこで、計算した距離は distance_matrix に格納して、それを必要に応じて読み込む形にしましょう。


```python
import math
def get_distance_matrix(df):
    distance_matrix = []
    for i in range(len(df)):
        vec1 = df.iloc[i, :].values
        distance_array = []
        for j in range(len(df)):
            vec2 = df.iloc[j, :].values
            dist = 0.
            for v1, v2 in zip(vec1, vec2):
                dist += (v1 - v2) ** 2
            distance_array.append(math.sqrt(dist))
        distance_matrix.append(distance_array)
    return distance_matrix
```


```python
def silhouette_coefficient2(clusters, distance_matrix):
    a_same = []
    b_diff = []
    for i, j in enumerate(clusters):
        for k, l in enumerate(clusters):
            if i < k:
                dist = distance_matrix[i][k]
                if j == l: # same cluster
                    a_same.append(dist)
                else: # different cluster
                    b_diff.append(dist)
    a = sum(a_same) / len(a_same)
    b = sum(b_diff) / len(b_diff)
    return (b - a) / max(b, a)
```


```python
distance_matrix = get_distance_matrix(df)
x = []
y = []
for i in range(2, len(df)):
    x.append(i)
    y.append(silhouette_coefficient2(get_cluster_by_number(result1, i), distance_matrix))
plt.plot(x, y)
plt.xlabel("Num of clusters")
plt.ylabel("silhouette coefficient")
plt.grid()
plt.show()
```


![output_27_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/16aa3748-90d6-461f-9165-47f223e2f319.png)


これで、結果は変わらず、高速化ができたことと思います。


# クラスタに分割して中身を見る

上記の計算より、クラスタ数が１０個の時にシルエット数が極大になることが分かりました。そこで、クラスタ数を１０個として、どのようなクラスターが得られたか確認してみましょう。


```python
clusterIDs = get_cluster_by_number(result1, 10)
```

次のようにして、１０個のクラスターのメンバーとそのアルコール消費量が確認できます。どのように分かれたか、得られたグラフをよくみて考察してみましょう。


```python
for i in range(max(clusterIDs) + 1):
    cluster = []
    for j, k in enumerate(clusterIDs):
        if i == k:
            cluster.append(j)
    plt.title("Cluster {}: {} samples".format(i + 1, len(cluster)))
    for j in cluster:
        plt.plot(dfs.iloc[j, :], label=dfs.index[j])
    plt.legend( loc = 'center right',
          bbox_to_anchor = (0.8, 0.5, 0.5, 0.1),
          borderaxespad = 0.0)
    plt.grid()
    plt.show()
```


![output_31_0.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/5a1c99f6-f464-497a-7986-7d2db8a727c5.png)


![output_31_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/806e5fe9-17f6-16e5-65f3-6dcda6066e94.png)


![output_31_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/98b2b5a2-c395-d02a-7f96-d19509692c20.png)

![output_31_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/c24f23d3-9fe0-edef-8154-8d477069cabe.png)


![output_31_4.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/8f27b769-1d05-d86a-cd81-ac6b238fea25.png)


![output_31_5.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/db878f81-90d7-0e6e-0f11-5363401115af.png)


![output_31_6.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/1af25d9d-eddb-c803-0ff3-efc8698fb634.png)


![output_31_7.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/d2bd7850-1ada-182d-2a9d-54756b1d1151.png)


![output_31_8.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/ab30dc79-7a66-f376-22d7-ef386414d266.png)


![output_31_9.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/211162/2d2a9fba-2874-78da-07cd-f35220486453.png)


# 課題

「行ごとの正規化」を「列ごとの正規化」「正規化しない」などに変更した場合どのように結果が変わるか確認し、「正規化」の効果を考察しましょう。

また、階層クラスタリングの metric や method を変えた時にどのように結果が変わるか確認しましょう。