打者のタイプをクラスタリングしてみた①

1.はじめに 
--
今回、セ・リーグの野球打者成績データを用いて、Pythonでクラスタリング（KMeans）を行い、打者タイプを分類・可視化してみました。データ分析も初心者のため、コードや分析方法などレベルが低いことはご了承ください。

2.目的
--
「打率や長打率といった指標から、打者タイプの傾向を自動的に分類できるのか？」
という問いを出発点に、以下のような視点で分析を進めました。

- どんな特徴を持ったタイプの打者が存在するか？
- 各チームにはどのタイプの打者が多いのか？

3.使用したデータ
--
使用したデータは「[プロ野球Freak](baseball-data.com)」さんから、お借りしました。とても良くデータがまとまっているサイトになります。

4.分析ステップ
--
**ステップ1. データの取得と前処理**
```Python
#必要なものをインポート
import nu#py as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# データのURL
url = "https://baseball-data.com/24/stats/hitter-ce/tpa-1.html"

# データの読み込み
df = pd.read_html(url)[0]

# カラム名変更
df.columns = ['順 位', '選手名', 'チーム', '打 率', '試 合', '打 席 数', '打 数', '安 打',
             '本 塁 打', '打 点', '盗 塁', '四 球', '死 球', '三 振', '犠 打', '併 殺 打',
             '出 塁 率', '長 打 率', 'O P S', 'R C 2 7', 'X R 2 7']

# 文字列は数値に、欠損値はnanに
df_1 = df.apply(pd.to_numeric, errors='coerce')
```

**ステップ2. 特徴量（指標）の作成**
今回使用した特徴量は「打率、出塁率、長打率、三振率、四球率、純長打率」ですが、「三振率、四球率、 純長打率」はデータがなかったため、自分で計算しています。
```Python
# 三振率の計算
df_1['三振率'] = df_1['三 振'] / df_1['打 席 数']

# 四球率の計算
df_1['四球率'] = df_1['四 球'] / df_1['打 席 数']

# 純長打率の計算
df_1['純長打率'] = df_1['長 打 率'] - df_1['打 率']
```

**ステップ3. データの標準化**
```Python
#標準化モジュールのインポート
from sklearn.preprocessing import StandardScaler

# 標準化
scaler = StandardScaler()
df_filtered_scalerd = scaler.fit_transform(df_filtered)
```

**ステップ4. KMeansによるクラスタリング（クラスタ数 = 4）**
今回は、エルボー法などを行わず、これまでの自分の野球観を基にクラスタ数を決めてみました。
```Python
#KMeansのインポート
from sklearn.cluster import KMeans

#クラスタモデルの作成
kmeans = KMeans(n_clusters=4, random_state=0)

# 学習と分類
clusters = kmeans.fit_predict(df_filtered_scalerd)
```

**ステップ5. 各クラスタの特徴を把握**
下記の結果から、今回は、「0:ミート型、1:パワー特化型、2:控えクラス型、3:スラッガー型」と命名しました。
```Python
# 各クラスタの特徴を把握
df_filtered_scalerd.groupby('タイプ').mean()
```
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4100334/1db20eee-74ec-4d8d-a52e-35c26ec96380.png)



**ステップ6. 各チームごとのタイプ分布を円グラフで可視化**
全球団のコードを乗せると長くなってしまうため、阪神タイガースのコードのみを記載します。
```Python
import matplotlib.font_manager as fm

# 日本語フォント
plt.rcParams['font.family'] = 'Meiryo'

fig, axes = plt.subplots(2, 3, figsize=(15, 8))
fixed_colors = {"控えクラス型": '#99FF99', "パワー特化型": '#FF9999',
                "スラッガー型": '#FFCC99', "ミート型": '#66B2FF'}

# 阪神のラベル、サイズ
labels_T = df_T_type_all
sizes_T = df_T_type["count"]
colors = [fixed_colors[label] for label in labels_T]

# 円グラフ作成(阪神)
ax_1 = axes[0, 0]
ax_1.pie(sizes_T, labels=labels_T, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_1.set_title('阪神')
```

5.分析結果
--
以下は、各球団に所属する打者をクラスタリングで分類した結果を示しています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/4100334/d624c395-9581-4647-9848-0c607e2be981.png)

6.まとめ
--
今回は、控えクラスに分類される選手が多い結果となりました。実際に選手名とタイプを目視で確認したところ、レギュラー選手であっても控えクラスに分類されているケースがありました。これは、守備型の選手や育成目的で我慢して起用している選手など、多様な層が控えクラスに含まれているためだと考えられます。そのため、クラスタ数を5などに増やしてより細かく分類すると、詳細な傾向を掴みやすくなるのではないかと感じました。また、信頼性の高いデータにするため、150打席以上の選手で分析しましたが、100打席以上や規定打席の半分以上の条件でも試すことで、より正確な分類ができるのではないかと思います。

※以下、今回の全コードです。
--
確認用にしたことなどすべてを含んでいます。

<details>
<summary>長いコードを開く</summary>

```python
# 必要なものをインポート
import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt

# データのURL
url = "https://baseball-data.com/24/stats/hitter-ce/tpa-1.html"

# データの読み込み
df = pd.read_html(url)[0]

#データの確認
df.head(2)

#カラム名の確認
df.columns

# カラム名変更
df.columns = ['順 位', '選手名', 'チーム', '打 率', '試 合', '打 席 数', '打 数', '安 打',
             '本 塁 打', '打 点', '盗 塁', '四 球', '死 球', '三 振', '犠 打', '併 殺 打',
             '出 塁 率', '長 打 率', 'O P S', 'R C 2 7', 'X R 2 7']

# 文字列は数値に、欠損値はnanに
df_1 = df.apply(pd.to_numeric, errors='coerce')

# 三振率の計算
df_1['三振率'] = df_1['三 振'] / df_1['打 席 数']

# 四球率の計算
df_1['四球率'] = df_1['四 球'] / df_1['打 席 数']

# 純長打率の計算
df_1['純長打率'] = df_1['長 打 率'] - df_1['打 率']

#変更後のデータ確認
df_1.head(2)

# 打席数150打席以上
df_filtered = df_1.loc[df['打 席 数'] >= 150]

# 後でチーム名でソートするよう
df_team = df.loc[df['打 席 数'] >= 150]

# 後でチーム名でソートするよう
df_team = df.loc[df['打 席 数'] >= 150]

# 不要な行を削除
df_filtered = df_filtered.drop(['順 位','選手名', 'チーム', '試 合', '打 席 数', '打 数', 
                                '安 打', '本 塁 打', '打 点', '盗 塁', '四 球', '死 球', '三 振',
                                '犠 打', '併 殺 打', 'O P S', 'R C 2 7', 'X R 2 7'], axis=1)

# データの確認
df_filtered

#標準化モジュールのインポート
from sklearn.preprocessing import StandardScaler

# 標準化
scaler = StandardScaler()
df_filtered_scalerd = scaler.fit_transform(df_filtered)

#KMeansのインポート
from sklearn.cluster import KMeans

#クラスタ分析
kmeans = KMeans(n_clusters=4, random_state=0)

# 学習と分類
clusters = kmeans.fit_predict(df_filtered_scalerd)

#データの結合、整理
df_filtered_scalerd = pd.DataFrame(df_filtered_scalerd)
df_filtered_scalerd['タイプ'] = clusters
df_filtered_scalerd.columns = ["打 率", "出 塁 率", "長 打 率", "三振率",	"四球率", "純長打率", "タイプ"] 
df_filtered_scalerd

# 各クラスタの特徴を把握
df_filtered_scalerd.groupby('タイプ').mean()

#タイプ列の取り出し
df_filtered_scalerd_type = df_filtered_scalerd["タイプ"]

#データの結合、表示
df_all = pd.concat([df_team, df_filtered_scalerd], axis=1)
df_all

# チーム列を文字列型に変更
df_all['チーム'] = df_all['チーム'].astype(str)

# チームごとの抽出
df_T = df_all.query('`チーム` == "阪神"')
df_G = df_all.query('`チーム` == "巨人"')
df_De = df_all.query('`チーム` == "DeNA"')
df_Y = df_all.query('`チーム` == "ヤクルト"')
df_D = df_all.query('`チーム` == "中日"')
df_C = df_all.query('`チーム` == "広島"')

# 円グラフ用のタイプ別人数
df_T_type = df_T["タイプ"].value_counts().sort_index().reset_index()
df_G_type = df_G["タイプ"].value_counts().sort_index().reset_index()
df_De_type = df_De["タイプ"].value_counts().sort_index().reset_index()
df_Y_type = df_Y["タイプ"].value_counts().sort_index().reset_index()
df_D_type = df_D["タイプ"].value_counts().sort_index().reset_index()
df_C_type = df_C["タイプ"].value_counts().sort_index().reset_index()

# タイプ列の名前付け
df_T_type_all = df_T_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})
df_G_type_all = df_G_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})
df_De_type_all = df_De_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})
df_Y_type_all = df_Y_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})
df_D_type_all = df_D_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})
df_C_type_all = df_C_type["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})

import matplotlib.font_manager as fm

# 日本語フォント
plt.rcParams['font.family'] = 'Meiryo'

fig, axes = plt.subplots(2, 3, figsize=(15, 8))
fixed_colors = {"控えクラス型": '#99FF99', "パワー特化型": '#FF9999',
                "スラッガー型": '#FFCC99', "ミート型": '#66B2FF'}

# 阪神のラベル、サイズ
labels_T = df_T_type_all
sizes_T = df_T_type["count"]
colors = [fixed_colors[label] for label in labels_T]

# 円グラフ作成(阪神)
ax_1 = axes[0, 0]
ax_1.pie(sizes_T, labels=labels_T, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_1.set_title('阪神')

# 巨人のラベル、サイズ
labels_G = df_G_type_all
sizes_G = df_G_type["count"]
colors = [fixed_colors[label] for label in labels_G]

# 円グラフ作成(巨人)
ax_2 = axes[0, 1]
ax_2.pie(sizes_G, labels=labels_G, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_2.set_title('巨人')

# 横浜のラベル、サイズ
labels_De = df_De_type_all
sizes_De = df_De_type["count"]
colors = [fixed_colors[label] for label in labels_De]

# 円グラフ作成(横浜)
ax_3 = axes[0, 2]
ax_3.pie(sizes_De, labels=labels_De, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_3.set_title('横浜')

# ヤクルトのラベル、サイズ
labels_Y = df_Y_type_all
sizes_Y = df_Y_type["count"]
colors = [fixed_colors[label] for label in labels_Y]

# 円グラフ作成(ヤクルト)
ax_4 = axes[1, 0]
ax_4.pie(sizes_Y, labels=labels_Y, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_4.set_title('ヤクルト')

# 中日のラベル、サイズ
labels_D = df_D_type_all
sizes_D = df_D_type["count"]
colors = [fixed_colors[label] for label in labels_D]

# 円グラフ作成(中日)
ax_5 = axes[1, 1]
ax_5.pie(sizes_D, labels=labels_D, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_5.set_title('中日')

# 広島のラベル、サイズ
labels_C = df_C_type_all
sizes_C = df_C_type["count"]
colors = [fixed_colors[label] for label in labels_C]

# 円グラフ作成(広島)
ax_5 = axes[1, 2]
ax_5.pie(sizes_C, labels=labels_C, autopct='%1.1f%%', startangle=90,
         counterclock=False, colors=colors)
ax_5.set_title('広島')

plt.show()

#数値から文字列への変更
df_all["タイプ"] = df_all["タイプ"].replace({0: 'ミート型', 1: 'パワー特化型', 2: '控えクラス型', 3: 'スラッガー型'})

#CSVファイルへの出力
df_all.to_csv('Sreag_150.csv')
```