야구 분석/R

메이저리그 선수들의 커리어 살펴보기

sam_j_s 2024. 9. 8. 04:10
728x90
반응형

주제

 

미키 맨틀의 커리어 살펴보기

주제R 시스템은 데이터에 통계 모델을 적용하는 데 매우 적합합니다. 세이버메트릭스에서 자주 다루는 주제 중 하나는 MLB 데뷔부터 은퇴까지의 선수 시즌 타격, 수비 또는 투구 통계의 상승과

bbdiary03.tistory.com

저번에 미키 맨틀 선수의 커리어를 확인해 보았습니다. 이번에는 미키 맨틀 선수와 다른 메이저리그 선수들의 커리어를 같이 살펴보겠습니다.

 

타구 궤도 비교

사전 분석 작업

선수들의 타구 궤적을 비교할 때, 관련 있는 변수 중 하나는 선수의 수비 위치입니다. 포수와 같은 중요한 수비 포지션의 타격 기대치는 1루수와는 다릅니다. 같은 포지션을 가진 선수들의  궤적을 비교하려면, 수비 위치가 데이터베이스에 기록되어야 합니다. 이미 타격 데이터 프레임(batting) 생성하였고, 수비 데이터는 Lahman 패키지의 Fielding 데이터 프레임에 저장되어 있습니다.

 

야구 역사에서 많은 선수들이 짧은 커리어를 가졌기 때문에, 궤적을 연구할 때는 최소 타석 수를 가진 선수들만을 분석 대상으로 제한하는 것이 합리적으로 보입니다. 우리는 2000 타석 이상을 가진 선수들만 고려할 것입니다. 이렇게 하면 투수 및 짧은 커리어를 가진 다른 선수들의 타격 데이터를 제거할 수 있습니다. 이 서브 셋을 만들기 위해, dplyr 패키지의 group_by()와 wummarize() 함수를 사용하여 모든 선수들의 커리어 타석(AB_career)을 계산합니다. 그런 다음, inner_join() 함수를 사용하여 이 새로운 변수를 타격 데이터 프레임에 추가합니다. 마지막으로, filter() 함수를 사용하여 "최소 2000 타석"의 타자들로만 구성된 새로운 데이터 프레임 batting_2000을 만듭니다.

batting_2000 <- batting |> 
  group_by(playerID) |>
  summarize(AB_career = sum(AB, na.rm = TRUE)) |>
  inner_join(batting, by = "playerID") |>
  filter(AB_career >= 2000)

 

우리의 데이터 프레임에 수비 정보를 추가하려면, 주어진 선수의 주 수비 포지션을 찾아야 합니다. 각 포지션별로 해당 선수가 출전한 경기 수를 합산하고, 데이터 프레임 Positions는 각 선수가 가장 많이 출전한 포지션을 반환합니다. [각주:1]

Positions <- Fielding |> 
  group_by(playerID, POS) |>
  summarize(Games = sum(G)) |> 
  arrange(playerID, desc(Games)) |> 
  filter(POS == first(POS))

 

그런 다음 inner_join() 함수를 사용하여 이 새로운 수비 정보를 batting_2000 데이터 프레임과 결합합니다.

batting_2000 <- batting_2000 |>
  inner_join(Positions, by = "playerID")

 

 

커리어 통계 계산

우리는 커리어 통계를 기준으로 유사한 타자 그룹을 찾을 것입니다. 이를 위해 batting_2000 데이터 프레임의 각 선수에 대해 커리어 경기 수, 타석 수, 득점, 안타 수 등을 계산해야 합니다. 이는 group_by()와 summarize() 함수를 사용하여 편리하게 수행할 수 있습니다. R 코드에서는 across() 함수를 사용하여 벡터 vars에 정의된 다양한 타격 통계의 합계를 각 타자에 대해 계산합니다. 우리는 선수 ID 변수인 playerID와 새로운 커리어 변수를 포함한 새로운 데이터 프레임 C_totals을 만듭니다.

my_vars <- c("G", "AB", "R", "H", "X2B", "X3B",
             "HR", "RBI", "BB", "SO", "SB")

C_totals <- batting_2000 |>
  group_by(playerID) |>
  summarize(across(all_of(my_vars), ~ sum(.x, na.rm = TRUE)))

 

새로운 데이터 프레임에서, mutate() 함수를 사용하여 각 선수의 커리어 타율(AVG)과 장타율(SLG)을 계산합니다.

C_totals <- C_totals |>
  mutate(
    AVG = H / AB,
    SLG = (H - X2B - X3B - HR + 2 * X2B + 3 * X3B + 4 * HR) / AB
  )

 

그런 다음 커리어 통계 데이터 프레임 C_totals와 수비 데이터 프레임 Positions를 병합합니다. 각 수비 포지션에는 관련 값이 있으며, case_when() 함수를 사용하여 각 포지션(POS)에 대해 값(Value_POS)을 정의합니다. 이러한 값은 Bill James가 James (1994)에서 도입하였으며, Baseball-Reference의 유사도 점수 페이지에 표시되어 있습니다.

C_totals <- C_totals |>
  inner_join(Positions, by = "playerID") |>
  mutate(
    Value_POS = case_when(
      POS == "C" ~ 240,
      POS == "SS" ~ 168,
      POS == "2B" ~ 132,
      POS == "3B" ~ 84,
      POS == "OF" ~ 48,
      POS == "1B" ~ 12, 
      TRUE ~ 0
    )
  )

 

 

유사도 점수 계산

Bill James는 커리어 통계를 기반으로 선수들 간의 비교를 용이하게 하기 위해 유사도 점수 개념을 도입했습니다. 두 타자를 비교할 때, 기본 점수 1000점에서 각 통계 항목의 차이에 따라 점수를 차감합니다. 다음 항목마다 1점을 차감합니다: (1) 20경기 차이, (2) 75타석 차이, (3) 10 득점 차이, (4) 15안타 차이, (5) 5루타 차이, (6) 4 삼루타 차이, (7) 2 홈런 차이, (8) 10타점 차이, (9) 25 볼넷 차이, (10) 150 삼진 차이, (11) 20 도루 차이, (12) 타율 0.001 차이, (13) 장타율 0.002 차이. 또한, 두 선수의 수비 포지션 값 차이의 절댓값도 차감합니다.

 

similar() 함수는 커리어 통계와 수비 포지션에 기반한 유사도 점수를 사용하여 주어진 선수와 가장 유사한 선수들을 찾습니다. 특정 선수의 ID와 유사한 선수의 수(주어진 선수를 포함)를 입력하면, 유사도 점수가 감소하는 순서로 정렬된 선수 통계 데이터 프레임이 출력됩니다.

similar <- function(p, number = 10) {
  P <- C_totals |> 
    filter(playerID == p)
  C_totals |> 
    mutate(
      sim_score = 1000 -
        floor(abs(G - P$G) / 20) -
        floor(abs(AB - P$AB) / 75) -
        floor(abs(R - P$R) / 10) -
        floor(abs(H - P$H) / 15) -
        floor(abs(X2B - P$X2B) / 5) -
        floor(abs(X3B - P$X3B) / 4) -
        floor(abs(HR - P$HR) / 2) -
        floor(abs(RBI - P$RBI) / 10) -
        floor(abs(BB - P$BB) / 25) -
        floor(abs(SO - P$SO) / 150) -
        floor(abs(SB - P$SB) / 20) - 
        floor(abs(AVG - P$AVG) / 0.001) - 
        floor(abs(SLG - P$SLG) / 0.002) -
        abs(Value_POS - P$Value_POS)
    ) |>
    arrange(desc(sim_score)) |> 
    slice_head(n = number)
}

 

예를 들기 위해, 미키 맨틀과 가장 유사한 5명의 선수를 찾고자 한다고 가정해 보겠습니다. Mantle의 선수 ID는 벡터 mantle_id에 저장되어 있습니다. similar() 함수를 사용하여 입력으로 mantle_id와 6을 제공합니다.

similar(mantle_id, 6)

선수 ID를 확인한 결과, 커리어 타격 통계와 포지션 측면에서 유사한 5명의 선수는 프랭크 토마스, 에디 매튜스, 마이크 슈미트, 게리 셰필드, 그리고 세미 소사입니다.

 

 

나이, 출루율, 장타율 그리고 OPS 정의하기

유사한 타자 그룹의 타격 궤적을 맞추고 그래프 화하려면, 각 선수에 대해 모든 시즌의 나이와 OPS 통계가 필요합니다. Lahman Batting 테이블을 사용할 때의 복잡성 중 하나는, 한 시즌 동안 여러 팀에서 활동한 타자에 대해 별도의 타격 기록이 사용된다는 점입니다. stint 변수는 여러 팀에서 활동한 선수에게 다른 값(1, 2, …)을 부여합니다. 다음 코드는 group_by()와 summarize() 함수를 사용하여 이러한 여러 기록을 각 선수의 각 연도별로 단일 행으로 결합합니다. 또한, 타격 측정값인 SLG, OBP, OPS를 계산합니다. (앞서 HBP와 SF의 결측값을 제로로 대체했으므로, OBP와 OPS 변수의 계산에는 결측값이 없습니다.)

batting_2000 <- batting_2000 |> 
  group_by(playerID, yearID) |>
  summarize(
    G = sum(G), AB = sum(AB), R = sum(R),
    H = sum(H), X2B = sum(X2B), X3B = sum(X3B),
    HR = sum(HR), RBI = sum(RBI), SB = sum(SB),
    CS = sum(CS), BB = sum(BB), SH = sum(SH),
    SF = sum(SF), HBP = sum(HBP),
    AB_career = first(AB_career),
    POS = first(POS)
  ) |>
  mutate(
    SLG = (H - X2B - X3B - HR + 2 * X2B + 3 * X3B + 4 * HR) / AB,
    OBP = (H + BB + HBP) / (AB + BB + HBP + SF),
    OPS = SLG + OBP
  )

 

따라서, 각 시즌에 대한 선수의 타격 통계가 단일 행으로 기록된 새로운 버전의 batting_2000 데이터 프레임을 생성합니다.

 

다음 작업은 모든 시즌에 대한 모든 선수의 나이를 얻는 것입니다. 앞서 3.7절에서 특정 선수의 MLB 출생 연도를 계산하기 위해 유사한 기법을 사용했음을 기억하세요. 여기서는 모든 선수의 출생 연도를 계산하고, inner_join() 함수를 사용하여 이 출생 연도 정보를 타격 데이터와 병합합니다. 이제 모든 선수의 출생 연도가 확보되었으므로, 시즌 연도와 출생 연도의 차이로 새로운 변수 Age를 정의할 수 있습니다.

batting_2000 <- batting_2000 |>
  inner_join(People, by = "playerID") |>
  mutate(
    Birthyear = if_else(
      birthMonth >= 7, birthYear + 1, birthYear
    ),
    Age = yearID - Birthyear
  )

 

작은 복잡성은 출생 연도가 몇몇 19세기 선수들에 대해 기록되지 않아 Age 변수가 누락된 경우가 있다는 점입니다. 따라서, drop_na() 함수를 사용하여 나이 기록이 누락된 항목을 제외하고, 업데이트된 데이터 프레임 batting_2000은 Age 변수가 있는 선수들만 포함되게 됩니다.

batting_2000 |> drop_na(Age) -> batting_2000

 

 

타격 궤적 시각화

비슷한 선수 그룹이 주어지면, plot_trajectories() 함수를 작성하여 각 선수의 타격 궤적에 대해 2차 곡선을 맞추고, 비교를 용이하게 하는 그래프를 작성합니다. 이 함수는 관심 있는 선수의 이름, 비교할 선수 수(관심 있는 선수를 포함), 그리고 다중 패널 그래프의 열 수를 입력으로 받습니다.

 

plot_trajectories() 함수는 먼저 People 데이터 프레임을 사용하여 해당 선수의 ID를 찾습니다. 이후 similar() 함수를 사용하여 선수 ID 벡터 player_list를 찾습니다. Batting_new 데이터 프레임은 선수 목록에 있는 선수들만의 시즌 타격 통계로 구성됩니다. 그래프 작성은 ggplot2 패키지를 사용하여 수행됩니다. geom_smooth() 함수의 formula 인수를 사용하여 $ y\sim x + I(X^2)$ 모든 선수의 Age와 Fit에 대한 궤적 곡선을 생성합니다. facet_wrap() 함수의 ncol 인수를 사용하여 이러한 궤적을 별도의 패널에 배치하며, 다중 패널 디스플레이의 열 수는 함수의 인수로 지정된 값입니다.

plot_trajectories <- function(player, n_similar = 5, ncol) { 
  flnames <- unlist(str_split(player, " "))
  
  player <- People |> 
    filter(nameFirst == flnames[1], nameLast == flnames[2]) |>
    select(playerID)

  player_list <- player |>
    pull(playerID) |>
    similar(n_similar) |>
    pull(playerID)
  
  Batting_new <- batting_2000 |> 
    filter(playerID %in% player_list) |>
    mutate(Name = paste(nameFirst, nameLast))
  
    ggplot(Batting_new, aes(Age, OPS)) + 
      geom_smooth(
        method = "lm",
        formula = y ~ x + I(x^2),
        linewidth = 1.5
      ) +
      facet_wrap(vars(Name), ncol = ncol) + 
      theme_bw()
}

 

다음은 plot_trajectories() 함수 사용의 몇 가지 예입니다. 그림 1에서는 Mickey Mantle의 궤적을 그의 가장 유사한 다섯 타자들과 비교합니다.

plot_trajectories("Mickey Mantle", 6, 2)

그림 1 . 미키 맨틀과 다섯 명의 유사 타자들의 추정 커리어 궤적

 

그림 2에서는 데릭 지터의 OPS 궤적을 여덟 명의 유사 선수들과 비교합니다. 이 경우 ggplot2 객체가 변수 dj_plot에 저장되어 있음을 주목하세요. (곧 이 객체에서 관련 데이터를 추출할 수 있음을 알 수 있습니다.)

dj_plot <- plot_trajectories("Derek Jeter", 9, 3)
dj_plot

그림 2 . 데릭 지터와 여덟 명의 유사 타자들의 추정 커리어 궤적

 

그림 1과 2를 살펴보면, 이러한 궤적들 간에 두드러진 차이점이 있음을 알 수 있습니다.

  • 에디 매튜스, 프랭크 토마스. 미키 맨틀, 로베르토 알로마와 같은 선수들은 커리어 초기에 정점을 찍은 것으로 보입니다.
  • 반면, 마이크 슈미트, 크레이그 비지오, 훌리오 프랑코와 같은 선수들은 30대에 정점을 찍었습니다.
  • 선수들은 궤적의 형태에서도 차이를 보입니다. 예를 들어 폴 몰리터는 상대적으로 평평한 궤적을 가진 반면, 로베르토 알로마는 높은 곡률을 가진 궤적을 보였습니다.

이러한 궤적들은 정점 나이, 최대 값, 그리고 곡률로 요약할 수 있습니다. 우선, dj_plot$data 구성 요소에는 Jeter와 유사 선수 그룹의 타격 데이터가 포함되어 있습니다. group_split() 함수를 사용하여 데이터를 각 선수별로 하나의 요소를 가진 리스트로 분할합니다. 그런 다음, map()을 사용하여 각 선수에 대해 2차 모델을 맞춥니다. broom 패키지의 tidy() 함수는 회귀 계수를 깔끔한 형식으로 회복하는 데 도움을 줍니다.

 

출력 데이터 프레임 regressions는 각 선수에 대한 회귀 추정치를 포함합니다.

library(broom)
data_grouped <- dj_plot$data |>
  group_by(Name)
player_names <- data_grouped |>
  group_keys() |>
  pull(Name)
regressions <- data_grouped |>
  group_split() |>
  map(~lm(OPS ~ I(Age - 30) + I((Age - 30) ^ 2), data = .)) |>
  map(tidy) |>
  set_names(player_names) |>
  bind_rows(.id = "Name")
regressions |>
  slice_head(n = 6)

 

다음으로, summarize() 함수를 regressions와 함께 사용하여 모든 선수에 대한 요약 통계, 즉 정점 나이, 최대 값, 곡률을 찾습니다. 이 계산은 Jeter와 여덟 명의 유사 선수들에 대해 설명된 바 있습니다.

S <- regressions |> 
  group_by(Name) |> 
  summarize(
    b1 = estimate[1],
    b2 = estimate[2],
    Curvature = estimate[3],
    Age_max = round(30 - b2 / Curvature / 2, 1),
    Max = round(b1 - b2 ^ 2 / Curvature / 4, 3)
  )

 

아홉 명의 선수 궤적 간의 차이를 이해하는 데 도움을 주기 위해, ggplot() 함수를 사용하여 정점 나이와 곡률 통계의 산점도를 구성합니다. geom_label_repel() 함수는 선수 레이블을 추가하는 데 사용됩니다.

library(ggrepel)
ggplot(S, aes(Age_max, Curvature, label = Name)) + 
  geom_point() + geom_label_repel()

그림 3 . 데릭 지처와 그와 가장 유사한 여덟 명의 선수들에 대한 추정 정점 나이와 곡률 통계

 

그림 3은 알로마가 일찍 정점을 찍었고, 프랑코와 몰리터는 늦은 나이에 정점을 찍었음을 명확히 보여줍니다. 또한, 알로마와 개링거는 가장 큰 곡률을 보였으며, 이는 이들이 정점 이후 성능이 빠르게 하락했음을 나타냅니다.

  1. 드물게 두 개 이상의 포지션에서 동일한 경기 수를 기록한 경우, first() 함수는 첫 번째 포지션을 선택합니다. [본문으로]

반응형

'야구 분석/R'의 다른글

  • 현재글 메이저리그 선수들의 커리어 살펴보기

관련글