05-03 04:21
Notice
Recent Posts
Recent Comments
관리 메뉴

Scientific Computing & Data Science

[Data Science / Baseball] Lahman 데이터를 이용한 야구 데이터 분석 Part 2. 본문

Data Science/ Baseball Data Analysis

[Data Science / Baseball] Lahman 데이터를 이용한 야구 데이터 분석 Part 2.

cinema4dr12 2017. 3. 4. 21:38

Lahman 데이터를 이용한 야구 데이터 분석 Part 2.

QUESTIONS

Q1각 10년 단위로 경기 당 평균 홈런 수는 몇 개인가?

Q2. 각 10년 단위로 보았을 때 삼진 수와 홈런 수는 상관관계가 있을까?


본 포스팅에서는 Lahman 데이터 분석을 위한 준비를 했던 지난 글에 이어 본격적으로 데이터 분석을 하는 해보도록 한다.

CRC Press의 "Analyzing Baseball Data with R"이 제시하는 질문에 답하는 유형으로 진행할 것이다.

한 가지 첨언하면, 훌륭한 데이터 과학자의 자질 중 하나는 끊임없이 질문하고 그 질문에 데이터로 답을 할 수 있는 것임을 명심하자.


Q1. 각 10년 단위로 경기 당 평균 홈런 수는 몇 개인가?

이 질문에 답을 하려면 "Teams" 데이터를 불러온다.

read.csv() 함수를 이용하여 불러와도 되고,

R CODE:

teams <- read.csv("../data/baseballdatabank-master/core/Teams.csv")


지난 글의 분석 환경 설정과 같이 MongoDB로부터 불러올 수 있다:

R CODE:

base::source('./ImportCollection.R', echo=FALSE) teams <- ImportCollection("Teams")


각 10년 주기로 경기 당 평균 홈런 수를 계산하려면,

  • 우선 10년 단위로 데이터 집합을 분리하고

  • 분리된 각 데이터 집합에 대한 전체 홈런 수를 각 데이터 집합 내 총 경기 수로 나누면 된다.


그러면 먼저 할 일이 명확해진다: 즉, 10년 단위로 데이터 집합을 분리하는 일이다.

먼저 teams의 Field를 살펴보자:

> names(teams)
 [1] "yearID"         "teamID"         "franchID"       "divID"          "Rank"           "G"              "W"             
 [8] "L"              "DivWin"         "WCWin"          "LgWin"          "WSWin"          "R"              "AB"            
[15] "H"              "X2B"            "X3B"            "HR"             "BB"             "SO"             "SB"            
[22] "RA"             "ER"             "ERA"            "CG"             "SHO"            "SV"             "IPouts"        
[29] "HA"             "HRA"            "BBA"            "SOA"            "E"              "FP"             "name"          
[36] "park"           "BPF"            "PPF"            "teamIDBR"       "teamIDlahman45" "teamIDretro"    "CS"            
[43] "lgID"           "DP"             "attendance"     "Ghome"          "HBP"            "SF"

과 같다.

yearID가 해당년도를 의미하는데, 데이터가 기록된 연도의 범위를 살펴보면,

> min(teams$yearID)
[1] 1871
> max(teams$yearID)
[1] 2016

1871년부터 2016년까지 기록되어 있음을 알 수 있다.

해당년도가 가령 1990년대에 속하는지를 확인하려면, 해당년도를 1990으로 나눈 나머지가 0~9에 있는지 확인하면 된다. 해당년도 1989년이라면,

> 1989 %% 1990
[1] 1989

0~9에 존재하지 않으므로 해당년도는 1990년도에 속하지 않는다. 반면 해당년도가 1990년 및 1996년이라면,

> 1990 %% 1990
[1] 0
> 1996 %% 1990
[1] 6

으로 0~9에 속하므로 해당년도는 1990년대에 속한다고 할 수 있다.

따라서, 다음과 같이 decade sequence를 만들고,

> seq_range <- base::seq(from = 1870, to = 2010, by = 10)
> seq_range
 [1] 1870 1880 1890 1900 1910 1920 1930 1940 1950 1960 1970 1980 1990 2000 2010

teams 데이터의 각 해당년도를 seq_range로부터 하나씩 불러와 나눈 나머지가 0~9에 속하는지 확인하면 된다.

R CODE:

# decades sequence seq_range <- base::seq(from = 1870, to = 2010, by = 10) for(i in seq_range) { decade <- teams$yearID %% i sub_teams <- base:: subset(teams, (0 <= decade) & (decade <= 9)) }

위와 같이 코드를 작성하면 sub_teams는 각 decade로 분리된 데이터 집합을 갖게 된다.

그러면, 각 decade에 대한 총 경기수는 어떻게 될까?

teams의 Field에서 경기수와 관련된 Field는 GGhome이 있다. G는 해당 팀이 치룬 총 경기 수이며, Ghome은 해당 팀의 홈경기 수이다.

그러면 해당년도에 치뤄진 모든 경기를 계산하려면 G의 합계를 계산해야 할까, 아니면 Ghome의 합계를 계산해야 할까? 가령, 팀A와 팀B가 해당년도에 3번의 맞대결을 펼쳤는데 2경기는 팀A의 홈에서, 1경기는 팀B의 홈에서 치뤄졌다고 가정해 보자. 이 경우 팀A에게 있어 G는 3이며, Ghome은 2이다. 팀B에게 있어 G는 3이며, Ghome은 1이다. 이 때 팀A와 팀B의 G를 합치면 6이 되고, Ghome을 합치면 3이 된다. G가 6이 되는 것은 당연히도 팀A와 팀B가 대결을 펼쳤음에도 각자의 경기를 계산한 것이기 때문이므로, 해당년도에 치뤄진 경기의 합계는 Ghome을 합산하면 된다.

다음과 같이 decadehr의 Feature를 갖는 Data Frame을 초기화해보자:

R CODE:

# initialize avg_hrs: average homeruns for each decade avg_hrs_decade <- base::data.frame(matrix(ncol = 2, nrow = 1)) base::names(avg_hrs_decade) <- base::c("decade", "hr")

초기화 된 avg_hrs_decade는 다음과 같다:

> avg_hrs_decade
  decade hr
1     NA NA


이 Data Frame은 각 Decade별로 평균 홈런 수를 저장하기 위한 것이며, 그 코드는 다음과 같다.

R CODE:

rowIndex <- 0 for(i in seq_range) { decade <- teams$yearID %% i sub_teams <- base:: subset(teams, (0 <= decade) & (decade <= 9)) num_games <- base::sum(sub_teams$G) / 2 num_hrs <- base::sum(sub_teams$HR) avg_hrs <- num_hrs / num_games rowIndex <- rowIndex + 1 avg_hrs_decade[rowIndex,] <- c(i, avg_hrs) }


계산돤 결과는 다음과 같다:

> avg_hrs_decade
   decade        hr
1    1870 0.1782373
2    1880 0.4308711
3    1890 0.5028022
4    1900 0.2737213
5    1910 0.3402131
6    1920 0.8028889
7    1930 1.0918691
8    1940 1.0470265
9    1950 1.6857928
10   1960 1.6395589
11   1970 1.4916187
12   1980 1.6198063
13   1990 1.9148375
14   2000 2.1468033
15   2010 1.9677192


이제 이 avg_hrs_decade에 저장된 값을 시각화 해 보자. 시각화 도구는 R의 Plotting 패키지 중 하나인 ploly를 활용하도록 한다. plotly를 불러온다:

R CODE:

if (! ("plotly" %in% rownames(installed.packages()))) { install.packages("plotly") } library(plotly)


다음 코드는 x축을 avg_hrs_decade의 decade로, y축을 hr로 line과 marker를 함께 표시하는 그래프를 표시하는 것이다:

R CODE:

# plotting with Plotly p <- plotly::plot_ly(data = avg_hrs_decade, x = ~decade, y = ~hr, type = 'scatter', mode = 'lines+markers', line = list(color = 'rgb(205, 12, 24)',width = 3)) %>% layout(title = "Average Homeruns per Game each Decade", xaxis = list(title = "Decade"), yaxis = list (title = "Average Homeruns")) # print results print(p)


이 코드를 실행하면 다음 Plot을 출력한다.



Plot을 보면 알 수 있듯이 10년 단위로 시간이 흐를수록 대체적으로 홈런 수가 증가함을 알 수 있다. 1900년대 이전까지는 불과 1경기 당 0.5개의 홈런 수가 2000년도에 경기 당 2.14개까지 증가했다.


Q1.에 대한 전체 코드는 다음과 같다:

R CODE for Q1:

base::source('./ImportCollection.R', echo=FALSE) if (! ("plotly" %in% rownames(installed.packages()))) { install.packages("plotly") } library(plotly) ####################################################################### # Question 1 ####################################################################### # import "Teams" from database teams <- ImportCollection("Teams") # initialize avg_hrs: average homeruns for each decade avg_hrs_decade <- base::data.frame(matrix(ncol = 2, nrow = 1)) base::names(avg_hrs_decade) <- base::c("decade", "hr") # decades sequence seq_range <- base::seq(from = 1870, to = 2010, by = 10) rowIndex <- 0 for(i in seq_range) { decade <- teams$yearID %% i sub_teams <- base:: subset(teams, (0 <= decade) & (decade <= 9)) num_games <- base::sum(sub_teams$G) / 2 num_hrs <- base::sum(sub_teams$HR) avg_hrs <- num_hrs / num_games rowIndex <- rowIndex + 1 avg_hrs_decade[rowIndex,] <- c(i, avg_hrs) } # plotting with Plotly p <- plotly::plot_ly(data = avg_hrs_decade, x = ~decade, y = ~hr, type = 'scatter', mode = 'lines+markers', line = list(color = 'rgb(205, 12, 24)',width = 3)) %>% layout(title = "Average Homeruns per Game each Decade", xaxis = list(title = "Decade"), yaxis = list (title = "Average Homeruns")) # print results print(p) print(avg_hrs_decade)


Q2. 각 10년 단위로 보았을 때 삼진 수와 홈런 수는 상관관계가 있을까?

홈런타자일수록 삼진개수가 많다고 한다. 그렇다면 10년 주기로 보았을 때 삼진 수와 홈런 수가 과연 상관이 있는지 궁금해진다. Q1.과 비슷한 방식으로 10년 단위로 본 삼진 수와 홈런 수의 상관관계를 알아보자.

Q1.에서 불러온 teams를 활용하되, "decade", "hr"(homeruns), "so"(strikeout)의 3개의 필드를 갖는 so_hr_decade라는 Data Frame을 초기화한다:

R CODE:

# initialize so_hr_decade: average strikeouts & homeruns for each decade so_hr_decade <- base::data.frame(matrix(ncol = 3, nrow = 1)) base::names(so_hr_decade) <- base::c("decade", "hr", "so")


Q1.과 동일하게 1870년대부터 2010년대까지, 10년 단위로 경기 당 홈런(hr) 수와 삼진(so) 수를 계산할 것이므로 seq_range라는 Vector 변수에 다음과 같이 이 기간 동안의 10년 단위 값을 저장한다:

> seq_range <- base::seq(from = 1870, to = 2010, by = 10)
> seq_range
 [1] 1870 1880 1890 1900 1910 1920 1930 1940 1950 1960 1970 1980 1990 2000 2010


for loop을 통해 seq_range에서 10년 단위로 subset 데이터를 추출할 것인데 문제가 좀 있다. 가령, 1900년대의 데이터를 추출한 후, 총 삼진 수를 계산해 보자:

> i <- 1900
> decade <- teams$yearID %% i
> sub_teams <- base:: subset(teams, (0 <= decade) & (decade <= 9))
> num_sos <- base::sum(sub_teams$SO)
> num_sos
[1] NA


삼진 수를 출력하였더니, "NA"가 출력된다. 이유는 다음 코드를 보면 알 수 있다:

> sub_teams$SO
  [1] 272 278 383 408 343 374 321 318 377 282 449 519 337 532 584 326 346 384 575 344 549 493 540 340 429 375 489
 [28] 481 381 565 465 356 287 530 293 481 446 327 438 296 561  NA  NA 537  NA  NA 595 526  NA 465 513  NA  NA 539
 [55]  NA 463 570  NA  NA 586  NA  NA 714 635  NA 548 605  NA  NA 609  NA 759  NA  NA  NA  NA  NA  NA  NA  NA  NA
 [82]  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA
[109]  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA
[136]  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA  NA


위와 같이 NA가 존재하기 때문이다. 이를 유실 데이터(Missing Data)라고 하는데, 사실 이와 같은 경우는 데이터를 다루다 보면 흔히 접하게 되는 문제이다. 유실된 데이터를 무시하고 유효한 데이터가 있는 행(Row)들만 추출하기 위해 R의 함수인 complete.cases()를 이용할 수 있다:

> good <- stats::complete.cases(sub_teams$SO)
> good
  [1]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [19]  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE  TRUE
 [37]  TRUE  TRUE  TRUE  TRUE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE
 [55] FALSE  TRUE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE  TRUE FALSE  TRUE  TRUE FALSE FALSE  TRUE FALSE  TRUE
 [73] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
 [91] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[109] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[127] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
[145] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
> complete_set <- sub_teams[good,]
> complete_set$SO
 [1] 272 278 383 408 343 374 321 318 377 282 449 519 337 532 584 326 346 384 575 344 549 493 540 340 429 375 489 481
[29] 381 565 465 356 287 530 293 481 446 327 438 296 561 537 595 526 465 513 539 463 570 586 714 635 548 605 609 759


유효한 데이터가 있는 행들만 추출하여 각 해당 년대의 총 경기 수와 삼진 수를 이용하여 경기 당 삼진 수를 계산한다:

R CODE:

complete_set <- sub_teams[stats::complete.cases(sub_teams$SO),] num_games <- base::sum(complete_set$G) / 2 num_sos <- base::sum(complete_set$SO) avg_sos <- num_sos / num_games


홈런의 경우도 마찬가지 과정을 통해 계산한다 (사실 홈런의 경우, 유실 데이터가 없긴하다).

R CODE:

complete_set <- sub_teams[stats::complete.cases(sub_teams$HR),] num_games <- base::sum(complete_set$G) / 2 num_hrs <- base::sum(complete_set$HR) avg_hrs <- num_hrs / num_games


for loop를 통해 각 decade에 대하여 hr(경기 당 평균 홈런 수)와 so(경기 당 평균 삼진 수)를 각각 계산하여 so_hr_decade에 저장하고, plotly 라이브러리를 이용하여 시각화해보자:

R CODE:

# plotting with Plotly p <- plotly::plot_ly(data = so_hr_decade, x = ~decade, y = ~hr, name = "Average Homeruns per Game", type = 'scatter', mode = 'lines+markers', line = list(color = 'rgb(205, 12, 24)', width = 3)) %>% add_trace(y = ~so, name = "Average Strikeouts per Game", line = list(color = 'rgb(22, 96, 167)', width = 4)) %>% layout(title = "Average Homeruns & Strikeouts per Game each Decade", xaxis = list(title = "Decade"), yaxis = list (title = "Average Homeruns & Strikeouts")) # print results print(p)


플롯팅 결과는 아래 이미지와 같다:


항상 그런 것은 아니지만, 대체적으로 평균 홈런 수와 평균 삼진 수는 함께 증가하는 것으로 보인다.

이러한 관계를 좀 더 명확히 알아보기 위해 R에서 상관관계(Correlation)를 계산하는 함수인 cor()을 이용하여 상관관계 값을 출력해 보자:

> stats::cor(x = so_hr_decade[,2:3], use="complete.obs", method="pearson")
          hr        so
hr 1.0000000 0.8873489
so 0.8873489 1.0000000


참고로, person은 상관관계를 계산하는 방법 중 하나이며, 출력된 결과를 통해 알 수 있듯이 hrso 사이에는 꽤 높은 상관관계(0.8873489)가 있음을 알 수 있다.


Q2.에 대한 답변을 하는 과정의 이해를 돕기 위해 전체 R 코드를 수록한다:

R CODE for Q2:

base::source('./ImportCollection.R', echo=FALSE) if (! ("plotly" %in% rownames(installed.packages()))) { install.packages("plotly") } library(plotly) ####################################################################### ## Question 2 ####################################################################### # import "Teams" from database teams <- ImportCollection("Teams") # initialize so_hr_decade: average strikeouts & homeruns for each decade so_hr_decade <- base::data.frame(matrix(ncol = 3, nrow = 1)) base::names(so_hr_decade) <- base::c("decade", "hr", "so") # decades sequence seq_range <- base::seq(from = 1870, to = 2010, by = 10) rowIndex <- 0 for(i in seq_range) { decade <- teams$yearID %% i sub_teams <- base:: subset(teams, (0 <= decade) & (decade <= 9)) complete_set <- sub_teams[stats::complete.cases(sub_teams$HR),] num_games <- base::sum(complete_set$G) / 2 num_hrs <- base::sum(complete_set$HR) avg_hrs <- num_hrs / num_games complete_set <- sub_teams[stats::complete.cases(sub_teams$SO),] num_games <- base::sum(complete_set$G) / 2 num_sos <- base::sum(complete_set$SO) avg_sos <- num_sos / num_games rowIndex <- rowIndex + 1 so_hr_decade[rowIndex,] <- base::c(i, avg_hrs, avg_sos) print(so_hr_decade) } # plotting with Plotly p <- plotly::plot_ly(data = so_hr_decade, x = ~decade, y = ~hr, name = "Average Homeruns per Game", type = 'scatter', mode = 'lines+markers', line = list(color = 'rgb(205, 12, 24)', width = 3)) %>% add_trace(y = ~so, name = "Average Strikeouts per Game", line = list(color = 'rgb(22, 96, 167)', width = 4)) %>% layout(title = "Average Homeruns & Strikeouts per Game each Decade", xaxis = list(title = "Decade"), yaxis = list (title = "Average Homeruns & Strikeouts")) # print results print(p) print(so_hr_decade) # correlation between homeruns and strikeouts stats::cor(x = so_hr_decade[,2:3], use="complete.obs", method="pearson")


by Geol Choi | 

Comments