RでMLBのMVPプレイヤーのホームラン時の投球コースと球種を可視化する

## はじめに
[mediba Advent Calender 2024](https://qiita.com/advent-calendar/2024/mediba)の9日目の記事になります。

今回は、R言語のアウトプットとして```baseballr```というライブラリを利用しMLBのデータを可視化することにしました。

具体的には、MLB 2024年シーズンのMVPプレイヤーである以下2選手のホームラン時の投球コースと球種を可視化しました。

- American League MVP：Aaron Judge（NYY）
- National League MVP：Shohei Ohtani（LAD）

## アウトプット

最終的に、2選手のホームラン時の投球コースと球種を可視化することができました。

:::note warn
本来ストライクゾーンの位置は選手ごとに異なるのですが、以下は一律同じ位置に固定しているため実際のストライクゾーンとは異なります。
:::

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/4db761e6-5afe-a36f-9d5e-3fd299438862.png)

可視化すると、ジャッジ選手は、ある程度コースを絞ってホームランにしているのが分かります。

逆に、大谷選手は特定のコースに絞らず、ホームランにしています。かなり外れたボール球をホームランにしているのも分かりますね。（[第52号ホームランの映像](https://baseballsavant.mlb.com/sporty-videos?playId=eeee8ce7-d49e-4661-bdfd-8e79258d97ec)）

## おまけ

ホームランの投球コース別割合も可視化してみました。比較すると、以下の傾向が見えてきました。

:::note warn
以下ストライクゾーンの位置は選手ごとに異なるため、実際のストライクゾーンに近い位置で可視化されています。
:::

- ジャッジ選手：高めの球のホームランは少なく、真ん中から低めに強い
- 大谷選手：どの投球コースでもホームランを打っている（中でも高めが少し強い）

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/e28cd715-3e38-30b7-9192-15ed629ccfd2.png)

## 作業工程

今回の大まかな作業の流れは以下の通りです。

1. データ収集（baseballr）
1. スキーマを理解する
1. 前処理（dplyr）
1. 可視化（ggplot2/patchwork）

## データ収集（baseballr）

Rの```baseballr```を利用して、Baseball SavantというWebサイトからスクレイピングし必要なデータを取得します。

https://baseballsavant.mlb.com/

Baseball Savantには「Statcast」という分析ツールで計測されたMLBの試合に関するあらゆるデータが掲載されています。

> ![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/9d29f5a3-36bc-34a1-0d0a-ae043038c4e9.png)
引用 - 「[Introducing Statcast 2020: Hawk-Eye and Google Cloud](https://technology.mlblogs.com/introducing-statcast-2020-hawk-eye-and-google-cloud-a5f5c20321b8)」Ben Jedlovec氏

なお、```baseballr```には[Baseball Savant](https://baseballsavant.mlb.com/)以外のWebサイトからもデータをスクレイピングする関数が含まれており、効率的なデータ取得が可能です。

- [FanGraphs.com](https://www.fangraphs.com/)
- [Baseball-Reference.com](https://www.baseball-reference.com/)

それでは、データをスクレイピングしていきます。

### 必要パッケージのインストール

CRANに登録されていない```baseballr```パッケージをインストールしたいため、```devtools```を利用しています。

```r
install.packages("devtools")
install.packages("tidyverse")
install.packages("dplyr")
install.packages("ggplot2")
install.packages("patchwork")

library(devtools) #CRANに登録されていないパッケージのインストール
library(tidyverse) 
library(dplyr) #前処理
library(ggplot2) #可視化
library(patchwork) #ggplot2で作成された複数のプロットの結合

install_github("billpetti/baseballr", force = TRUE)
```

余談ですが、```pacman```というライブラリを利用すれば、対象パッケージがインストールされていなくても自動でインストールし読み込んでくれるようです。

```r
install.packages("pacman")
library(pacman)

p_load(devtools, tidyverse, dplyr, ggplot2, patchwork)

install_github("billpetti/baseballr", force = TRUE)
```

### Baseball Savantのデータをスクレイピングする

以下の関数で、対象選手の指定期間の打者データのスクレイピングが可能です。

```r
df_judge_raw <- scrape_statcast_savant_batter("2024-01-01",
                                              "2024-12-31",
                                              batterid = 592450)

df_ohtani_raw <- scrape_statcast_savant_batter("2024-01-01",
                                               "2024-12-31",
                                               batterid = 660271)
```

なお、上記関数の引数に必要な```batterid```は、以下2種類の方法で検索します。

1. ```baseballr```の```playerid_lookup```関数
2. [razzball.com](https://razzball.com/mlbamids/)内での検索

1の```playerid_lookup```関数による```batterid```の検索は少々時間がかかるため、2の方法をおすすめします。

```r
playerid_lookup("Judge", "Aaron")
playerid_lookup("Ohtani", "Shohei")
```

2は[razzball.com](https://razzball.com/mlbamids/)にアクセスし、対象選手の氏名でフィルタをかけて検索します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/1bee4d2e-353a-99e1-d960-8b3b75319a98.png)

データフレームの中身を確認すると、2024年シーズンの対象選手に投じられた1球ごとのデータが格納されています。

打球速度や打球角度、カウントなどあらゆるデータが正確に計測されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/a8848fc2-1d63-b75c-9b01-9c822c8e839b.png)

補足となりますが、スクレイピングするための関数は投手用など他にもあるようでした。

```r
scrape_statcast_savant(start_date = "yyyy-mm-dd",
                       end_date = "yyyy-mm-dd",
                       playerid = {playerid},
                       player_type = "{pitcher or batter}", ...)

scrape_statcast_savant_batter_all(start_date = "yyyy-mm-dd",
                                  end_date = "yyyy-mm-dd",
                                  batterid = {batterid}, ...)

scrape_statcast_savant_pitcher(start_date = "yyyy-mm-dd",
                               end_date = "yyyy-mm-dd",
                               pitcherid = {pitcherid}, ...)

scrape_statcast_savant_pitcher_all(start_date = "yyyy-mm-dd",
                                   end_date = "yyyy-mm-dd",
                                   pitcherid = {pitcherid}, ...)
```

https://billpetti.github.io/baseballr/reference/index.html#legacy

## スキーマを理解する

可視化に必要なカラムを判断するため、以下のページを参考に先ほどのスクレイピングしたデータのスキーマを理解します。

https://baseballsavant.mlb.com/csv-docs

以下のカラムが、可視化に必要となりそうです。

- ```events```：打席の結果
- ```plate_x```：ホームベースを横切った際のボールの水平位置（キャッチャー視点）
    - 単位は「ft」（フィート）/ 1ft = 30.48cm
    - 捕手視点で真ん中を「0」、右打者側を「-」マイナス、左打者側を「+」プラスとする
- ```plate_z```：ホームベースを横切った際のボールの垂直位置（キャッチャー視点）
    - 単位は「ft」（フィート）
    - 地面を「0」、ワンバウンドを「-」マイナス、地上を「+」プラスとする
- ```pitch_name```：球種

なお、野球独自の指標などは、以下のページを参考に理解しました。

https://www.mlb.com/glossary

## 前処理（dplyr）

簡単な前処理を行います、具体的な内容は以下の通りです。

- 対象選手それぞれのデータフレームを作成する
- 可視化に必要なカラムを抽出する
- レギュラーシーズンのホームランのみのレコードにフィルタする
- 日付順（昇順）に並び替える

```r
df_judge_hr_result <- df_judge_raw %>% 
  select(pitch_type, game_date, player_name, events, zone, game_type, plate_x, plate_z, pitch_name) %>% 
  filter(events == "home_run", game_type == "R") %>% 
  arrange(game_date)

df_ohtani_hr_result <- df_ohtani_raw %>% 
  select(pitch_type, game_date, player_name, events, zone, game_type, plate_x, plate_z, pitch_name) %>% 
  filter(events == "home_run", game_type == "R") %>% 
  arrange(game_date)
```

前処理した結果は以下の通りです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/6f5f846b-2e50-c221-49ab-52ee03e5271d.png)

念のため、対象選手の2024年レギュラーシーズンのホームラン数が合っているか確認しておきます。

まずは、先ほどの2つのデータフレームを```rbind```で結合します。

```r
df_mvp_hr_result <- rbind(df_judge_hr_result, df_ohtani_hr_result) %>% 
  arrange(player_name, game_date)
```

次に、以下コードをRStudioのConsole上で実行すると対象選手の2024年レギュラーシーズンのホームラン数が出力されます。

```r
df_mvp_hr_count <- df_mvp_hr_result %>% 
  group_by(player_name) %>% 
  summarize(hr_count = sum(events == "home_run")) %>% 
  ungroup()

# 実行結果
# A tibble: 2 × 2
  player_name    hr_count
  <chr>             <int>
1 Judge, Aaron         58
2 Ohtani, Shohei       54
```

両者2024年のレギュラーシーズンのホームラン数と一致しているので、問題ないですね。

## 可視化（ggplot2/patchwork）

それでは、最後に以下の手順に沿って可視化を行います。

1. 13分割された投球コースとホームベースをプロットする
1. ホームラン時のボールの投球コースと球種をプロットする

なお、本工程は以下の記事を参考にさせていただきました。

https://note.com/11_tjr/n/nd0970f208355

### 13分割された投球コースとホームベースをプロットする

まずは、13分割された投球コースとホームベースをグラフ上にプロットするために、必要なX、Y座標のデータをデータフレーム化します。

13分割された投球コースのX座標、Y座標は以下の通りとなります。手作業で座標を指定いくのはかなり大変でした。

また、各座標データがどの投球コースに属するか番号を振っておくことが重要です。番号が存在しないと、プロット時に描画が崩れる原因となります。

```r
ids_4 <- 1:9
ids_6 <- 11:14

zone_vector <- c(
  rep(ids_4, each = 4), rep(ids_6, each = 6)
)

positions <- tribble(
  ~ width, ~ height,
  #1
  -0.947, 3.6,
  -0.947, 2.9,
  -0.316, 2.9,
  -0.316, 3.6,
  
  #2
  -0.316, 3.6,
  -0.316, 2.9,
  0.315, 2.9,
  0.315, 3.6,
  
  #3
  0.315, 3.6,
  0.315, 2.9,
  0.947, 2.9,
  0.947, 3.6,
  
  #4
  -0.947, 2.9,
  -0.947, 2.2,
  -0.316, 2.2,
  -0.316, 2.9,
  
  #5
  -0.316, 2.9,
  -0.316, 2.2,
  0.315, 2.2,
  0.315, 2.9,
  
  #6
  0.315, 2.9,
  0.315, 2.2,
  0.947, 2.2,
  0.947, 2.9,
  
  #7
  -0.947, 2.2,
  -0.947, 1.5,
  -0.316, 1.5,
  -0.316, 2.2,
  
  #8
  -0.316, 2.2,
  -0.316, 1.5,
  0.315, 1.5,
  0.315, 2.2,
  
  #9
  0.315, 2.2,
  0.315, 1.5,
  0.947, 1.5,
  0.947, 2.2,
  
  #11
  -1.420, 4.125,
  -1.420, 2.55,
  -0.947, 2.55,
  -0.947, 3.6,
  0, 3.6,
  0, 4.125,
  
  #12
  0, 4.125,
  0, 3.6,
  0.947, 3.6,
  0.947, 2.55,
  1.420, 2.55,
  1.420, 4.125, 
  
  #13
  -1.420, 2.55,
  -1.420, 0.975,
  0, 0.975,
  0, 1.5,
  -0.947, 1.5,
  -0.947, 2.55,
  
  #14
  0, 1.5,
  0, 0.975,
  1.420, 0.975,
  1.420, 2.55,
  0.947, 2.55,
  0.947, 1.5
) %>%
  bind_cols(zone = zone_vector)
```

データフレームの中身は以下の通りとなります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/b739486c-c0be-2e98-b4c6-ff628cd62a9a.png)

ホームベースのX座標、Y座標は以下の通りとなります。

```r
homebase <- tribble(
  ~ width, ~ height,
  -0.9, 0.5,
  -1.3, 0.2,
  0, 0,
  1.3, 0.2,
  0.9, 0.5
)
```

次に、以下のコードを実行すると13分割された投球コースとホームベースの可視化の完成です。ホームベースを可視化すれば、キャッチャー視点であることがすぐに分かります。

ポイントは、```inherit.aes = F```です。異なるデータフレームを別のレイヤーとして可視化する場合は本記述が必要となります。

```r
plot_judge_hr_result <- ggplot(positions) +
  aes(x = width, y = height, group = zone) +
  geom_polygon(color = "black", fill = "white") +
  geom_polygon(aes(x = width, y = height), data = homebase, inherit.aes = F, color = "black", fill = "white") +
  scale_x_continuous("Horizontal location(ft.)", limits = c(-2, 2)) +
  scale_y_continuous("Vertical location(ft.)", limits = c(0, 5))

plot_judge_hr_result
```

13分割された投球コースとホームベースを可視化することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/71a2114b-6017-2984-a703-1b906ab4c728.png)

### ホームラン時のボールの投球コースと球種をプロットする

最後に、ホームランを打った際のコースと球種を先ほどの13分割された投球コース上にプロットします。

```r
plot_judge_hr_result <- ggplot(positions) +
  aes(x = width, y = height, group = zone) +
  geom_polygon(color = "black", fill = "white") +
  geom_polygon(aes(x = width, y = height), data = homebase, inherit.aes = F, color = "black", fill = "white") + 
  geom_point(aes(x = plate_x, y = plate_z, color = pitch_name), data = df_judge_hr_result, inherit.aes = F) +
  scale_x_continuous("Horizontal location(ft.)", limits = c(-2, 2)) +
  scale_y_continuous("Vertical location(ft.)", limits = c(0, 5)) +
  labs(color = "Pitch Type") +
  ggtitle("Aaron Judge Homerun Result(2024)")

plot_judge_hr_result

plot_ohtani_hr_result <- ggplot(positions) +
  aes(x = width, y = height, group = zone) +
  geom_polygon(colour = "black", fill = "white") +
  geom_polygon(aes(x = width, y = height), data = homebase, inherit.aes = F, colour = "black", fill = "white") +
  geom_point(aes(x = plate_x, y = plate_z, color = pitch_name), data = df_ohtani_hr_result, inherit.aes = F ) +
  scale_x_continuous("Horizontal location(ft.)", limits = c(-2, 2)) +
  scale_y_continuous("Vertical location(ft.)", limits = c(0, 5)) +
  labs(color = "Pitch Type") +
  ggtitle("Shohei Ohtani Homerun Result(2024)")

plot_ohtani_hr_result
```

添付画像のような形で、可視化することができました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/2edbcc7b-3aa4-7042-d4cf-013a49601585.png)

加えて、2選手それぞれのグラフを横に並べて比較したいため、```patchwork```というライブラリを利用します。

凡例ラベルの表示は1つでよいため、片方のグラフで非表示にしておきます。

```r
plot_judge_hr_result <- ggplot(positions) +
  aes(x = width, y = height, group = zone) +
  geom_polygon(color = "black", fill = "white") +
  geom_polygon(aes(x = width, y = height), data = homebase, inherit.aes = F, color = "black", fill = "white") + 
  geom_point(aes(x = plate_x, y = plate_z, color = pitch_name), data = df_judge_hr_result, inherit.aes = F) +
  scale_x_continuous("Horizontal location(ft.)", limits = c(-2, 2)) +
  scale_y_continuous("Vertical location(ft.)", limits = c(0, 5)) +
  ggtitle("Aaron Judge") +
  theme(
    legend.position = "none", # 凡例を非表示
    plot.title = element_text(hjust = 0.5)
  ) 

plot_ohtani_hr_result <- ggplot(positions) +
  aes(x = width, y = height, group = zone) +
  geom_polygon(colour = "black", fill = "white") +
  geom_polygon(aes(x = width, y = height), data = homebase, inherit.aes = F, colour = "black", fill = "white") +
  geom_point(aes(x = plate_x, y = plate_z, color = pitch_name), data = df_ohtani_hr_result, inherit.aes = F ) +
  scale_x_continuous("Horizontal location(ft.)", limits = c(-2, 2)) +
  scale_y_continuous("Vertical location(ft.)", limits = c(0, 5)) +
  labs(color = "Pitch Type") +
  ggtitle("Shohei Ohtani") +
  theme(plot.title = element_text(hjust = 0.5))

plot_mvp_hr_result <- plot_judge_hr_result + plot_ohtani_hr_result +
  plot_annotation(title = "MVP Homerun Results Comparison(2024)")

plot_mvp_hr_result
```

以上で、可視化の完成となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/ce34120a-19e2-3eac-cd3d-bd0e10111576.png)


### ホームランのゾーン別割合

詳細な説明は割愛しますが、別の視点でも可視化を行ってみました。よりホームランの傾向が見えてきて、興味深いですね。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2290540/b47c5050-831f-b811-4c96-56a24240ae7e.png)

## おわりに

今回、Rのアウトプットを行うことで改めてRの基礎（特に前処理、可視化）を復習することができました。

また、私自身興味のあるMLBのデータを利用して、可視化した結果を確認する瞬間が最も楽しかったです。

今後も興味のあるデータで様々な可視化をしてみることで、業務に活用していきたいと思います。

## 参考

- [Rによるセイバーメトリクス入門](https://amzn.asia/d/dpGCjXH)
- [Analyzing Baseball Data with R (3e)](https://beanumber.github.io/abdwr3e/)
- [【小ネタ】ゾーン別○○を作るためのRコードを書いた｜TJ](https://note.com/11_tjr/n/nd0970f208355)