球種データを使ってプロ野球投手をクラスタリングしてみる

こんにちは。今日は私が最近趣味で行っていた球種データを使ったプロ野球投手のクラスタリングについて書かせていただこうと思います。

# 概要
皆さんパリーグTVはご覧になりますか？私はよくパリーグTVで好きな投手の三振動画を見てキャッキャッしているのですが、投手って本当にそれぞれ特徴があって見ていて面白いですよね。みんな違ってみんないい。
そんな投手の特徴を【球種】という角度から機械学習で分類したらどんな結果がでるかやってみよう、というのが今回の目的です。

### やったこと

* webからスクレイピングで必要なデータを取得しデータセットを作成
* K-meansでクラスタリング
* クラスタをPCAでマッピング

なお、私はパリーグファンなのでパリーグの投手に限定しています。
（セリーグファンの方ごめんなさい！）

# スクレイピングで必要なデータを取得
### 2020年シーズンの球種データ
今回使用する球種データは[こちらのサイト](http://xdomain3pk.html.xdomain.jp/index.html)からお借りします。
(投手別のまとまった球種データって実はあまりwebに転がっていない、、あっても有料だったり、、デルタとか、、そんな中でこちらのサイトはとても助かりました[^1]）

がっつりhtmlで書かれているので`requests`と`BeautifulSoup`を使ってhtml要素を分解しながらデータを取得します。

```python
import requests
from bs4 import BeautifulSoup

#球団リストを準備
team_list = ['l', 'h', 'f', 'bs', 'm', 'e']
team_list_jp = ['西武', 'ソフトバンク', '日本ハム', 'オリックス', 'ロッテ', '楽天']

#2020-11-11時点のデータを取得
df_all = pd.DataFrame()
for team, team_jp in zip(team_list, team_list_jp):
  re = requests.get('http://xdomain3pk.html.xdomain.jp/hk'+str(team)+'.html')
  soup = BeautifulSoup(re.content, "html.parser")
  a = soup.find(class_="tbl")
  df = pd.DataFrame()
  for i in a.find_all("tr"):
    l = []
    for j in i.find_all("td"):
      l.append(j.text)
    s = pd.Series(l)
    df = df.append(s, ignore_index= True)
    df['球団'] = str(team_jp)

  df_all = df_all.append(df)
```
取得したデータに対し余分なレコードを除外したり、正規表現で整えたり、カラム名を設定したり。紆余曲折を経て以下のようなデータができました。(各球種の単位は%)

|&emsp;&emsp;&emsp;背番号|&emsp;&emsp;&emsp;選手名|&emsp;&emsp;球団|&emsp;&emsp;&emsp;&emsp;&emsp;ストレート|&emsp;&emsp;&emsp;&emsp;&emsp;スライダー|&emsp;&emsp;&emsp;&emsp;フォーク|&emsp;&emsp;&emsp;&emsp;チェンジ<br>アップ|&emsp;&emsp;&emsp;カーブ|&emsp;&emsp;&emsp;&emsp;シュート|&emsp;&emsp;&emsp;&emsp;カット<br>ボール|&emsp;&emsp;&emsp;&emsp;シンカー|&emsp;&emsp;&emsp;&emsp;&emsp;スクリュー|&emsp;&emsp;&emsp;&emsp;&emsp;スプリット|&emsp;&emsp;&emsp;&emsp;スラープ|&emsp;&emsp;&emsp;スロー<br>カーブ|&emsp;&emsp;&emsp;&emsp;&emsp;ツーシーム|&emsp;&emsp;&emsp;&emsp;ナックル<br>カーブ|&emsp;&emsp;&emsp;パワー<br>カーブ|&emsp;&emsp;&emsp;縦スラ|
|:---:|:----:|:--:|:--------:|:-----:|:--:|:-------:|:------:|:-:|:---:|:----:|:--:|:--------:|:-----:|:--:|:-------:|:------:|:-:|:-:|
|11|今井|L|58|16|0|18|8|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|
|13|高橋光|L|42|4|27|NaN|6|NaN|17|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|4|
|1|松井|E|56|20|15|2|6|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|NaN|		
|19|山岡|B|35|NaN|1|5|4|5|12|1|NaN|NaN|NaN|NaN|NaN|NaN|NaN|36|
|38|森|SB|21|NaN|19|NaN|NaN|NaN|43|NaN|NaN|NaN|NaN|NaN|8|10|NaN|NaN|

169名分のデータが取得できました。
速球派、軟投派、バランス型、、、ふむふむ。

#### 2020年シーズン規定投球回数データ
あまりに投球回数が少ないと2020年シーズンの傾向とはいい難いため、規定投球回数1/3以上投げている投手に絞ります。
いつもお世話になっている[プロ野球データFreak](https://baseball-data.com/)から規定投球回数をお借りします。
こちらは`read_html`でテーブルごと取得できるので楽ちんです。

```python
#パリーグの投手で2020年シーズン規定投球回1/3以上の選手データを取得
data = pd.read_html('https://baseball-data.com/stats/pitcher-pa/ip3-5.html', header = 1)
df_p = data[0]

#球種データに合わせて苗字だけのカラムを作成
df_p['苗字'] = df_p['選手名'].str.split('　', expand = True)[0]

#必要な項目に絞る
df_p = df_p[['苗字','チーム']]
print(df_p.shape)
(59, 2)
```
59名が対象です。

#### データ結合
球種データと規定1/3到達投手データを球団＋選手名苗字をキーに結合します。
選手名の表記ゆれによりそのままでは結合できないデータも出てきます。とてもめんどくさいのですが、スクレイピングに表記ゆれは付きものなので黙ってもくもくと手動で修正です（細かいのでコードは割愛）

#### データの確認
作成したデータから各球種を使う選手人数割合を確認します。

```python
#各球種の投手人数割合を算出
desc = df3.drop(columns = ['選手名','チーム', 'チーム_選手名']).describe().T.reset_index()
desc['perc']=desc['count']/len(df3)
desc = desc[desc['perc']!=0].sort_values(by = 'perc', ascending = False)
desc = desc.astype({'count':'int', 'perc':'float'})

#グラフにする
perc = desc['perc'].round(2)
labels = desc['index'].unique()
y = np.arange(len(labels))

fig = plt.figure(figsize=(9, 6))
ax = fig.add_subplot(1, 1, 1)
bar = ax.barh(y,perc, alpha = 0.6)

ax.set_yticks(y)
ax.set_yticklabels(labels, fontsize = 12)
ax.invert_yaxis()
ax.set_xlabel('投手人数割合')
ax.set_title('各球種を投げている投手人数割合')

for p in bar:
  value = p.get_width()
  ax.annotate('{:.1%}'.format(value),
              xy=(value, p.get_y() + p.get_height() / 2),
              xytext=(0, 3),
              textcoords="offset points",
              ha='left', va='top', fontsize = 12)

plt.show()
```
出力結果
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/294c5a06-3e9a-8aed-1670-d8974f8e95cd.png)
本来クラスタリングをする上であまりに使用選手割合が低い球種は対象外にした方がいい気もしますが、縦スラとかパワーカーブがなくなるのは寂しいので除外せずにやってみます。まぁ趣味なので。[^2]
# クラスタリングする
さて、データができたのでやっとクラスタリングにはいります！
最初はG-meansやX-means等のデータから自動でクラスタ数を決めてくれるアルゴリズムを使おうと思っていました。実際G-meansでやってみた結果クラスタ数は2となり、【ストレート割合が低めで球種が多い軟投派】と【それ以外】というとても自然な、しかし面白味に欠ける結果になりました（分類としては間違ってないけども）

ということで、K-meansを使います。本来はエルボー法なんかを使ってクラスタ数を推定するべきところなのでしょうが、今回はざっくりいきます！
#### K-means
```python
from sklearn.cluster import KMeans

#クラスタリング用データ
data = df4.drop(columns = ['選手名','チーム', 'チーム_選手名'])
#numpyarrayに変換
data = data.values

#4つのグループを作成し、元データにクラスを付与
pred = KMeans(n_clusters=4, random_state = 0).fit_predict(data)
df4['class'] = pred

#人数確認
df4.groupby('class')['選手名'].count()
```
|class|人数|
|:-:|:-:|
| 0  |19  |
| 1  |14  |
| 2  |22  |
| 3  |4   |

ふむふむ。class3は何か強い特徴があるっぽい。
#### 各球種の平均使用割合からクラス別の傾向を見る
<details><summary>可視化コード</summary><div>

```python
for i in df4['class'].unique():
  group = df4[df4['class']==i].drop(columns = ['選手名','チーム', 'チーム_選手名','class']).mean().T.reset_index()

  perc = group[0].round(1)
  labels = group['index'].unique()
  y = np.arange(len(labels))
  
  fig = plt.figure(figsize=(9, 6))
  ax = fig.add_subplot(111)
  bar = ax.barh(y,perc,color= 'blue', alpha = 0.4)

  ax.set_yticks(y)
  ax.set_yticklabels(labels, fontsize = 12)
  ax.invert_yaxis()
  ax.set_xlim([0,60])
  ax.set_xlabel('平均割合')
  ax.set_title('クラス：'+str(i))

  for p in bar:
    value = p.get_width()
    ax.annotate('{}'.format(value),
                xy=(value, p.get_y() + p.get_height() / 2),
                xytext=(0, 3),
                textcoords="offset points",
                ha='left', va='top', fontsize = 13)
```
</div></details>

**クラス0：** ストレートを中心にしつつスライダーやフォークを決め球として使う投手っぽい。ロッテや楽天の先発、西武の中継ぎに多いイメージ。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/adae49f9-0c4a-d260-3460-37ffd91a41ca.png)

**クラス1:** カットボールをはじめとした多様な変化球を投げる技巧派っぽい。基本的にコントロールは悪くないイメージ。日ハム多いかも？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/e0bd15ea-1652-6268-a8a1-4ac1959e0d17.png)

**クラス2:** ストレートが持ち味の皆さん。スライダー、チェンジアップ、カーブを使いつつ、やっぱり勝負所はストレートで攻めるイメージ。西武多い印象あり。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/e393c234-cdb6-c56b-e802-61009e0f6851.png)

**クラス3:** ツーシーム！フォーシームとツーシームの違いがそもそも、、、的な感じもあるものの。やっぱり外国人投手がほとんど。と、二保。二保はメジャータイプだった・・？
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/41f6db8f-3d82-c9d6-9f45-2f2f5dd61a2b.png)

# PCAでクラスタをマッピング
主成分分析を使って球種データを次元削減し、クラスタをマッピングしてみます。

```python
from sklearn.decomposition import PCA

#主成分数を2つでインスタンス生成
pca = PCA(n_components = 2)
pca.fit(df4[df4.columns[2:15]])
train = pca.transform(df4[df4.columns[2:15]])

#可視化
fig = plt.figure(figsize=(9, 6))
ax = fig.add_subplot(111)
ax.scatter(train[:, 0], train[:, 1], c=df4['class'], s=60, alpha=0.5, cmap=plt.cm.rainbow)
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/937675/344f8892-a319-2eed-261d-2e2d086d665a.png)
決めでやった割には悪くないかも。

散布図に選手名のラベル付けと各球種の寄与率を出そうと思いましたが、力尽きました・・・

# おわりに
初めて記事的なものを書きましたが、まとめるのって難しいですね。最後の方力尽きて雑になってしまった。まだ試したいことがいくつかあるので、年末年始あたりにもう少し球種データで遊んでみようと思ってます。

クラスタリングは解釈しやすい＆教師なしなので、ビジネスでも比較的応用しやすい分野ですね。でも可視化はやっぱりRの方がしやすい場面も多い気がする、、、Plotlyのインタラクティブなグラフを埋め込みたかったのですが、qiitaへの埋め込み方がよくわからず断念しました[^3]。


[^1]: データ元が載ってないのが気にはなりますが。。。性善説で信じる。
[^2]: 趣味ということでところどころ雑なところがあります。間違っているところなどがあればご指摘ください！
[^3]: ご存じの方ぜひ教えてください！ plotlyの動くグラフ好き。