プロ野球選手の特徴ってなんだ？ ~クラスター分析(教師なし学習)で解き明かす~

#クラスター分析
- クラスター…似ているもの同士の集まり。

- クラスター分析とは、**似ているデータ同士をグループ化**する作業。

- 教師なし学習であるため、**ラベル付けがなされていなデータ**をグループ化しなければならない。
 →期待とは違ったグループ分けがなされることもある。

- 大きく**「階層的クラスタリング」**と**「非階層的クラスタリング」**に分けられる。

#階層的クラスタリング
- 凝集型(下から上へ)…全てのデータについて、似ているデータ同士を逐次結び付けていく手法。

- 分割型(上から下へ)…全てのデータについて、似ていないもの同士を逐次分離させていく手法。
- **デンドログラム**でクラスター形成の様子が見れる。
→理解が容易

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/d986e9c5-afdb-f90b-bc52-27ae6bfe7643.png)


#似ている(似ていない)」はどう判断するのか？

- 多くの場合、類似度の測定には、**ユークリッド距離**が用いられる。つまり、2つのn次元データ
A:a=(a1,a2,.....,an)
B:b=(b1,b2,.....,bn)
の類似度は、以下のように決める。

**d(A,B)=√{(a1-b1)^2+(a2-b2)^2+......+(an-bn)^2)}**

#クラスター間の距離
- 先の基準に基づいて似ているデータ同士を結びつけていくとクラスター(グループ)が形成される。
- 次の段階では、このクラスター(グループ)について似ているもの同士を結びつけなければいけない…
→クラスター(グループ)同士の距離はどう測るのか？

- クラスター間の距離の求め方はいくつかある。

**1:最短距離法**…2つのクラスターに含まれるデータの中で最も近いデータ同士の距離をクラスター間の距離とする。

**2:最長距離法**…2つのクラスターに含まれるデータの中で、最も遠いデータ同士の距離をクラスター間の距離とする。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/12b8044b-57b3-a101-7fb3-8eab2ac0369a.png)


**3:群平均法**…２つのクラスター間に含まれる全てのデータ同士の距離の平均をクラスター間の距離とする。

**4:重心法**…２つのクラスターそれぞれの重心(平均のデータ)間の距離をクラスター間の距離とする。

#具体例(最短距離法)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/dc212ed8-db93-0570-fd40-cb4654f20461.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/da896684-2186-d9b9-6011-c8a58f47b396.png)

#非階層的クラスタリング
**k-meansクラスタリング**が最もポピュラー。手順は以下の通り。

1:ランダムにk個のクラスターの中心を決める。

2:全てのデータについて、１で定めたクラスターの中心のうち、最も近いものにデータを振り分け、k個のクラスターに分割する。

3:k個のクラスター重心(平均値)を求める。

4:3で求めた重心(平均値)を用いて、再度データを振り分ける。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/d4a82e22-9e3d-e41e-b0e3-237a720b1c02.png)

#実際にやってみた
以下のサイトからwebスクレイピングをして選手データを収集しました。
(スクレイピングの過程は省略)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/7d550d5f-1255-1668-64aa-8dcf570561a6.png)


#データセット
```py
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
sns.set_style('whitegrid')

df = pd.read_csv('player_records.csv')
columns_list = ['年度', '所属球団', '登板', '勝利', '敗北', 'セーブ', 'H', 'HP', 
                '完投', '完封勝', '無四球', '勝率', '打者', '投球回', '安打', '本塁打', 
                '四球', '死球', '三振', '暴投', 'ボーク', '失点', '自責点', '防御率', '名前']
df = df.loc[:, columns_list]
df.head()
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/1045b0fe-3020-6d18-9a73-631d39164062.png)

#前処理

```py
df.info()

#防御率をobject型からfloat型へ
df['防御率'] = pd.to_numeric(df['防御率'], errors='coerce')
df.info()

#欠損値削除
df.dropna(inplace=True)
```

```py
from sklearn.preprocessing import StandardScaler
from sklearn.cluster import KMeans
from sklearn.metrics import silhouette_score, silhouette_samples
```


```py
df = df[df['年度'] == 2019]

#学習に使えないデータを削除する
data_df = df.drop(columns=['年度', '所属球団', '名前'])

#データの尺度を整える(クラスタリングには必須)
scaler = StandardScaler()
data_scaled = scaler.fit_transform(data_df)

data = np.array(data_df)
data_scaled = np.array(data_scaled)
```

#教師なし学習(k-means法でモデル構築)

```py
#エルボー法を用いてk_meansのkを決定する

distortions = []
for k in range(1, 11):
    kmeans = KMeans(n_clusters=k, n_init=10, max_iter=100)
    kmeans.fit(data_scaled)
    distortions.append(kmeans.inertia_)
    
fig = plt.figure(figsize=(12, 8))
plt.xticks(range(1, 11))
plt.plot(range(1, 11), distortions)

#縦軸はクラスタ内平方和＝どれだけ分類できてるか→小さいほうがよい
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/a9329dae-2d2b-445f-6e18-4763e9341015.png)


```py
#今回は3個に分類してみる
n_clusters = 3
kmeans = KMeans(n_clusters=n_clusters, n_init=10, max_iter=100)
kmeans.fit(data_scaled)
cluster_labels = kmeans.predict(data_scaled)
```

```py
cluster_labels
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/758c6edf-36ab-a588-656e-33d1a339bb14.png)


```py
#カラムを途中省略せず、全部をみる方法
pd.set_option('display.max_columns', 30)

df['cluster'] = cluster_labels
df.head()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/7c4fe13e-2bae-5881-6be6-f00f4062284a.png)

```py
df[df['cluster'] == 0]
#登板数多い、投球回と登板数が似ている。登板数少ない。
#2軍中継ぎ投手？クローザー？
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/60f3f469-a795-e50e-1688-77a7c3d42a76.png)


```py
df[df['cluster'] == 1]
#登板数多い、投球回と登板数が似ている。
#一軍クローザーor中継ぎ
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/bb17b4eb-fcfd-861f-643e-426a06ecbe7f.png)

```py
df[df['cluster'] == 2]
#登板数<投球回が多い→先発投手？
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/4f4ac477-6859-01f9-1348-85c041958456.png)


```py
#どのくらいの精度でクラスタリングできているか？

silhouette_avg = silhouette_score(data_scaled, cluster_labels)

silhouette_scores = silhouette_samples(data_scaled,
                                       cluster_labels, 
                                       metric='euclidean')

colorlist = ['#87cefa', '#3cb371', '#ffa500', '#dc143c', 
             '#ba55d3', '#a9a9a9', '#d8bfd8']

fig = plt.figure(figsize=(12, 8))
ax = fig.add_subplot(1, 1, 1)

y_lower = 0

for i in range(n_clusters):
    ith_cluster_silhouette_values = silhouette_scores[cluster_labels == i]
    ith_cluster_silhouette_values.sort()
    y_upper = y_lower + ith_cluster_silhouette_values.shape[0]

    color = colorlist[i]
    ax.fill_betweenx(np.arange(y_lower, y_upper), 0,
                     ith_cluster_silhouette_values,
                     facecolor=color, edgecolor=color)

    y_lower = y_upper 

ax.set_xlabel('silhouette score')

# The vertical line for average silhouette score of all the values
ax.axvline(x=silhouette_avg, color='red', ls='--')

#赤い線を超えてるほどよい
#長方形程よい
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/449715/6c3476e9-b861-4d81-6f61-4824fb5f66a9.png)