<Learning spoons - 데이터 과학을 위한 R&통계> 수업 자료를 이용하였습니다.
출처 : 연합뉴스
2019년 프로야구 신한은행 마이카 KBO리그 정규시즌에서 제공하는 정보에 따르면, 2019년 한해 총 누적 관중은 728만6천8명으로 집계되었습니다. 비록 꽃샘추위와 미세먼지 등의 기상환경, 하위권의 지속되는 부진, 수준 낮은 실책성 플레이, 선수들의 인성 논란으로 800만명 관중 시대에서 점점 누적 관중 수가 줄고 있는 상황입니다.
하지만 타 스포츠에 비해 일찍이 지역을 기반으로 한 팬덤이 활성화되어 있어 명실상부 국민 스포츠로 자리를 잡고 있습니다. 또한 최근 코로나19로 인하여 2020년에 프로야구 무관중 경기와 ESPN 실시간 중계 등으로 다시금 한국 프로야구에 대한 관심이 집중되고 있는 상황입니다.
KBO에서는 지역별로 10개 구단이 활동 중입니다. 서울은 두산 베어스와 LG 트윈스, 키움 히어로즈(사진에는 넥센 히어로즈라고 명시됨), 인천은 SK 와이번즈, 수원은 KT 위즈로 수도권에 5팀이 활동 중입니다. 수도권 외 지역에서는, 대전은 한화 이글즈, 광주는 기아 타이거즈, 대구는 삼성 라이온즈, 부산은 롯데 자이언츠, 창원은 NC 다이노스가 있습니다.
이번 리포트에서는 KBReport에서 제공하는 2019년 프로야구 타자 스탯 데이터를 이용하여 데이터 분석을 진행합니다.
사용할 데이터 column(야구 용어 설명)은 다음과 같습니다.
| 구분 | 상세 내용 |
|---|---|
| 타석, 타수 | 타석에 들어선 횟수 중 볼넷, 사구, 희생타 등을 제외한 것이 타수 |
| 안타 | 단타, 2루타, 3루타, 홈런을 모두 합한 개수 |
| 득점, 타점 | 홈에 들어온 것이 득점, 안타를 쳐서 다른 선수가 득점하면 타점 |
| 볼넷, 삼진 | 볼 4개로 1루에 걸어들어가면 볼넷, 스트라이크 3개로 아웃되면 삼진 |
| BABIP | (안타-홈런) / (타수-삼진-홈런-희생타) |
| 타율 | 안타 / 타수 |
| 출루율 | (안타 + 볼넷 + 사구) / (타수 + 볼넷 + 사구 + 희생타) |
| 장타율 | (단타 + 2루타X2 + 3루타X3 + 홈런X4) / 타수 |
| OPS | 출루율 + 장타율 |
| WAR | 대체 선수 대비 승리 기여 #WAR 4 이상이면 핵심 선수로 인정 |
library(tidyverse)
## ── Attaching packages ──────────────────────────────────────────────────── tidyverse 1.3.0 ──
## ✓ ggplot2 3.3.2 ✓ purrr 0.3.3
## ✓ tibble 3.0.1 ✓ dplyr 1.0.0
## ✓ tidyr 1.0.2 ✓ stringr 1.4.0
## ✓ readr 1.3.1 ✓ forcats 0.5.0
## ── Conflicts ─────────────────────────────────────────────────────── tidyverse_conflicts() ──
## x dplyr::filter() masks stats::filter()
## x dplyr::lag() masks stats::lag()
library(readxl)
getwd()
## [1] "/Users/jinameliachoi/Documents"
setwd(dir = './nanodegree02/data')
list.files(pattern = '.xlsx')
## [1] "2019_KBO_Win.xlsx" "Apt_Price_Detail_2019_Win.xlsx"
## [3] "Seafood_Trade_201811.xlsx" "test.xlsx"
stat <- read_xlsx(path = '2019_KBO_Win.xlsx')
head(stat)
## # A tibble: 6 x 19
## 선수명 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 BABIP
## <chr> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 양의지 NC 118 459 390 138 20 61 68 48 43 4 0.354
## 2 김하성 Hero 139 625 540 166 19 112 104 70 80 33 0.328
## 3 최정 SK 141 606 503 147 29 86 99 70 92 3 0.303
## 4 샌즈 Hero 139 613 525 160 28 100 113 77 101 1 0.329
## 5 로하스 KT 142 578 521 168 24 68 104 49 120 4 0.377
## 6 박병호 Hero 122 532 432 121 33 92 98 78 117 0 0.302
## # … with 6 more variables: 타율 <dbl>, 출루율 <dbl>, 장타율 <dbl>, OPS <dbl>,
## # wOBA <dbl>, WAR <dbl>
타자 스탯 데이터의 구조와 행, 열 개수를 반환합니다.
str(object = stat)
## tibble [300 × 19] (S3: tbl_df/tbl/data.frame)
## $ 선수명: chr [1:300] "양의지" "김하성" "최정" "샌즈" ...
## $ 팀명 : chr [1:300] "NC" "Hero" "SK" "Hero" ...
## $ 경기 : num [1:300] 118 139 141 139 142 122 144 140 125 116 ...
## $ 타석 : num [1:300] 459 625 606 613 578 532 645 630 526 505 ...
## $ 타수 : num [1:300] 390 540 503 525 521 432 572 574 468 438 ...
## $ 안타 : num [1:300] 138 166 147 160 168 121 197 193 161 147 ...
## $ 홈런 : num [1:300] 20 19 29 28 24 33 15 6 1 13 ...
## $ 득점 : num [1:300] 61 112 86 100 68 92 87 91 89 72 ...
## $ 타점 : num [1:300] 68 104 99 113 104 98 88 68 45 65 ...
## $ 볼넷 : num [1:300] 48 70 70 77 49 78 61 45 41 61 ...
## $ 삼진 : num [1:300] 43 80 92 101 120 117 54 40 40 87 ...
## $ 도루 : num [1:300] 4 33 3 1 4 0 1 13 18 9 ...
## $ BABIP : num [1:300] 0.354 0.328 0.303 0.329 0.377 0.302 0.358 0.352 0.37 0.392 ...
## $ 타율 : num [1:300] 0.354 0.307 0.292 0.305 0.322 0.28 0.344 0.336 0.344 0.336 ...
## $ 출루율: num [1:300] 0.438 0.389 0.399 0.396 0.381 0.398 0.409 0.386 0.403 0.416 ...
## $ 장타율: num [1:300] 0.574 0.491 0.519 0.543 0.53 0.56 0.483 0.456 0.434 0.495 ...
## $ OPS : num [1:300] 1.012 0.88 0.918 0.939 0.911 ...
## $ wOBA : num [1:300] 0.441 0.389 0.403 0.411 0.396 0.414 0.397 0.381 0.382 0.405 ...
## $ WAR : num [1:300] 7.02 6.77 6.63 6.09 5.56 5.14 4.81 4.81 4.71 4.6 ...
dim(x = stat)
## [1] 300 19
colnames(x = stat)
## [1] "선수명" "팀명" "경기" "타석" "타수" "안타" "홈런" "득점"
## [9] "타점" "볼넷" "삼진" "도루" "BABIP" "타율" "출루율" "장타율"
## [17] "OPS" "wOBA" "WAR"
총 300명 타자 선수들의 데이터가 수집되어 있는 것을 확인하였습니다. 각 column 별 정보는 다음과 같습니다.
summary(object = stat)
## 선수명 팀명 경기 타석
## Length:300 Length:300 Min. : 1.00 Min. : 0.00
## Class :character Class :character 1st Qu.: 13.00 1st Qu.: 20.75
## Mode :character Mode :character Median : 45.00 Median : 85.00
## Mean : 58.42 Mean :185.42
## 3rd Qu.:107.75 3rd Qu.:368.75
## Max. :144.00 Max. :645.00
##
## 타수 안타 홈런 득점
## Min. : 0.0 Min. : 0.00 Min. : 0.00 Min. : 0.00
## 1st Qu.: 20.0 1st Qu.: 4.00 1st Qu.: 0.00 1st Qu.: 2.00
## Median : 74.0 Median : 15.50 Median : 1.00 Median : 8.50
## Mean :164.1 Mean : 43.81 Mean : 3.38 Mean : 21.82
## 3rd Qu.:338.0 3rd Qu.: 87.25 3rd Qu.: 4.00 3rd Qu.: 37.25
## Max. :574.0 Max. :197.00 Max. :33.00 Max. :112.00
##
## 타점 볼넷 삼진 도루
## Min. : 0.00 Min. : 0.00 Min. : 0.00 Min. : 0.0
## 1st Qu.: 1.00 1st Qu.: 1.00 1st Qu.: 6.00 1st Qu.: 0.0
## Median : 6.00 Median : 6.00 Median : 18.00 Median : 0.0
## Mean : 20.61 Mean :15.84 Mean : 31.97 Mean : 3.3
## 3rd Qu.: 35.00 3rd Qu.:26.00 3rd Qu.: 54.00 3rd Qu.: 3.0
## Max. :113.00 Max. :85.00 Max. :120.00 Max. :39.0
##
## BABIP 타율 출루율 장타율
## Min. :0.0000 Min. :0.0000 Min. :0.0000 Min. :0.0000
## 1st Qu.:0.2490 1st Qu.:0.1690 1st Qu.:0.2410 1st Qu.:0.2220
## Median :0.2950 Median :0.2350 Median :0.3000 Median :0.3130
## Mean :0.2727 Mean :0.2107 Mean :0.2738 Mean :0.2933
## 3rd Qu.:0.3280 3rd Qu.:0.2720 3rd Qu.:0.3400 3rd Qu.:0.3820
## Max. :1.0000 Max. :0.6670 Max. :0.6670 Max. :0.6670
## NA's :11 NA's :5 NA's :5 NA's :5
## OPS wOBA WAR
## Min. :0.0000 Min. :0.0000 Min. :-1.6100
## 1st Qu.:0.4720 1st Qu.:0.2260 1st Qu.:-0.1600
## Median :0.6140 Median :0.2880 Median :-0.0200
## Mean :0.5671 Mean :0.2625 Mean : 0.5935
## 3rd Qu.:0.7180 3rd Qu.:0.3285 3rd Qu.: 0.5500
## Max. :1.3340 Max. :0.6090 Max. : 7.0200
## NA's :5 NA's :5 NA's :5
stat 데이터프레임에서 선수명과 팀명 column 데이터를 살펴봅니다.
table(stat$선수명)
##
## 강경학 강로한 강민국 강민호 강백호 강승호 강진성
## 1 1 1 1 1 1 1
## 고명성 고승민 고장혁 고종욱 공민규 구본혁 구자욱
## 1 1 1 1 1 1 1
## 국해성 권희동 김강민 김경호 김규민 김대한 김도환
## 1 1 1 1 1 1 1
## 김동엽 김동한 김문호 김민성 김민수 김민식 김민하
## 1 1 1 1 2 1 1
## 김민혁 김병희 김사훈 김상수 김선빈 김성민 김성욱
## 1 1 1 1 1 1 1
## 김성현 김성훈 김승회 김영환 김용의 김웅빈 김은성
## 1 1 1 1 1 1 1
## 김응민 김인태 김인환 김재성 김재윤(타) 김재율 김재현
## 1 1 1 1 1 1 2
## 김재호 김재환* 김종민 김주찬 김주형 김준완 김준태
## 1 1 1 1 1 1 1
## 김지수 김진곤 김진형 김찬형 김창평 김창혁 김철호
## 1 1 1 1 1 1 1
## 김태군 김태균 김태근 김태연 김태진 김하성 김헌곤
## 1 1 1 1 1 1 1
## 김현수 김형준 김혜성 김호재 김회성 나경민 나성범
## 1 1 1 1 1 1 1
## 나종덕 나주환 나지완 남태혁 노수광 노시환 노진혁
## 1 1 1 1 1 1 1
## 러프 로맥 로하스 류승현 류지혁 류형우 모창민
## 1 1 1 1 1 1 1
## 문경찬(타) 문규현 문상철 문선재 민병헌 박건우 박경수
## 1 1 1 1 1 1 1
## 박계범 박동원 박민석 박민우 박병호 박상언 박석민
## 1 1 1 1 1 1 1
## 박세혁 박승규 박승욱 박용택 박유연 박정권 박정음
## 1 1 2 1 1 1 1
## 박준태 박준혁 박지규 박찬도 박찬호 박한결 박한이
## 1 1 1 1 1 1 1
## 박해민 박헌욱 배성근 배영섭 배정대 백동훈 백승민
## 1 1 1 1 1 1 1
## 백승현 백용환 백창수 베탄코트 변우혁 샌즈 서건창
## 1 1 1 1 1 1 1
## 서상우 서예일 손시헌 손아섭 손주인 송광민 송민섭
## 1 1 1 1 1 1 1
## 송성문 송승환 송준석 스몰린스키 신민재 신범수 신본기
## 1 1 1 1 1 1 1
## 신성현 신용수 심우준 아수아헤 안상현 안승한 안중열
## 1 1 1 1 1 1 1
## 안치영 안치홍 양성우 양우현 양원혁 양의지 양종민
## 1 1 1 1 1 1 1
## 예진원 오선우 오선진 오영수 오윤석 오재원 오재일
## 1 1 1 1 1 1 1
## 오정환 오준혁 오지환 오태곤 윌리엄슨 윌슨 유강남
## 1 1 1 1 1 1 1
## 유민상 유영준 유장혁 유재신 유한준 윤석민 윤수강
## 1 1 1 1 1 1 1
## 윤진호 윤해진 이대형 이대호 이동훈 이명기 이범호
## 1 1 1 1 1 2 1
## 이병규 이상호 이성곤 이성규 이성열 이성우 이승진
## 1 1 1 1 1 1 1
## 이우성 이원석 이원재 이유찬 이인행 이인혁 이재원
## 2 2 1 1 1 1 1
## 이정후 이정훈 이준수 이지영 이진영 이찬건 이창열
## 1 1 1 1 1 1 1
## 이창진 이천웅 이학주 이해창 이현동 이현석 이형종
## 1 1 1 1 1 1 1
## 이흥련 임병욱 임재현 임지열 장성우 장승현 장영석
## 1 1 1 1 1 1 1
## 장운호 장진혁 전민수 전민재 전병우 전준우 전준호
## 1 1 1 1 1 1 1
## 정근우 정범모 정병곤 정보근 정상호 정성종(타) 정수빈
## 1 1 1 1 1 1 1
## 정은원 정의윤 정주현 정준혁 정진기 정진호 정현
## 1 1 1 1 1 1 2
## 정훈 조셉 조용호 조홍석 주효상 지석훈 지성준
## 1 1 1 1 1 1 1
## 채은성 채태인 채현우 최경모 최민재 최선호 최승민
## 1 1 1 1 1 1 1
## 최승준 최영진 최원준 최윤석 최재훈 최정 최정민
## 1 1 1 1 1 1 1
## 최정용 최주환 최준우 최진행* 최항 최형우 추재현
## 1 1 1 1 1 1 1
## 터커 페게로 페르난데스 하주석 한동민 한동희 한승택
## 1 1 1 1 1 1 1
## 한준수 해즐베이커 허경민 허도환 허일 허정협 호잉
## 1 1 1 1 1 1 1
## 홍재호 홍창기 황대인 황윤호 황재균 황진수
## 1 1 1 1 1 1
table(x = stat$팀명)
## x
## Hero KIA KT LG NC SK 두산 롯데 삼성 한화
## 22 36 28 28 31 32 28 33 30 32
2가지 텍스트 전처리 과정이 필요합니다.
선수명에서 * 와 (타) 라는 불필요한 텍스트가 포함되어 있습니다. 제거하여 이름만 남깁니다.
팀명에서 키움 히어로즈는 스폰서명인 ’키움’이 아닌 ’Hero’로 나옵니다. ’키움’으로 변경합니다.
stat$선수명 <- str_remove(string = stat$선수명, pattern = '\\*$')
stat$선수명 <- str_remove(string = stat$선수명, pattern = '\\(타\\)')
stat$선수명 <- as.factor(x = stat$선수명)
print(x = stat$선수명)
## [1] 양의지 김하성 최정 샌즈 로하스 박병호
## [7] 페르난데스 이정후 박민우 강백호 러프 박건우
## [13] 최형우 로맥 전준우 박석민 황재균 최재훈
## [19] 민병헌 오재일 이천웅 박세혁 유한준 유강남
## [25] 터커 오지환 박동원 김재호 김재환 김현수
## [31] 안치홍 호잉 김상수 이재원 김선빈 서건창
## [37] 이창진 이형종 한동민 채은성 노진혁 허경민
## [43] 손아섭 이성열 이원석 정수빈 강민호 이학주
## [49] 구자욱 나성범 고종욱 이대호 박경수 김태균
## [55] 모창민 심우준 김혜성 정은원 김헌곤 윌슨
## [61] 정의윤 장성우 유민상 권희동 박계범 김민성
## [67] 김강민 박해민 아수아헤 페게로 최주환 조용호
## [73] 이명기 김성욱 이지영 박찬호 정근우 조셉
## [79] 황윤호 한승택 스몰린스키 이명기 김태진 강로한
## [85] 손시헌 윌리엄슨 장진혁 김찬형 김주찬 노수광
## [91] 류지혁 베탄코트 신범수 이흥련 송광민 김성현
## [97] 오선진 김민혁 임병욱 이준수 백용환 최진행
## [103] 최승민 송민섭 공민규 신본기 강민국 이성규
## [109] 김동한 지성준 김재현 박한이 정범모 이우성
## [115] 오태곤 김태연 김인태 김형준 이인혁 임재현
## [121] 최윤석 강경학 김태군 한준수 김도환 박지규
## [127] 이원재 고승민 김재성 김민하 홍창기 김지수
## [133] 장영석 예진원 최영진 신성현 최선호 이원석
## [139] 이정훈 김민수 최항 이동훈 김웅빈 하주석
## [145] 이유찬 지석훈 윤수강 김사훈 김진곤 채태인
## [151] 서상우 이범호 주효상 안치영 채현우 김주형
## [157] 박민석 오준혁 박유연 박상언 문경찬 추재현
## [163] 김재윤 정성종 양우현 김성민 김응민 이인행
## [169] 김은성 황진수 김준완 이대형 김재현 김창혁
## [175] 이찬건 최정용 정준혁 유재신 강진성 김종민
## [181] 박준혁 김철호 송승환 이현동 최민재 김회성
## [187] 변우혁 박정권 정병곤 문선재 서예일 윤해진
## [193] 이현석 최준우 류형우 나경민 황대인 김병희
## [199] 정현 김규민 전준호 유영준 박용택 박찬도
## [205] 송준석 남태혁 홍재호 박정음 이병규 이창열
## [211] 최승준 국해성 박헌욱 최정민 김민식 해즐베이커
## [217] 임지열 오영수 전민수 정진기 나지완 김재율
## [223] 박한결 고명성 고장혁 배성근 류승현 박승규
## [229] 허일 장승현 신민재 김호재 장운호 백창수
## [235] 김민수 손주인 문규현 강승호 김영환 정훈
## [241] 김창평 이진영 신용수 박승욱 배영섭 문상철
## [247] 허정협 김준태 김대한 박승욱 김경호 백승민
## [253] 이성곤 최경모 김문호 백승현 이성우 정보근
## [259] 정주현 안중열 배정대 김진형 정상호 박준태
## [265] 유장혁 정현 김인환 백동훈 정진호 이해창
## [271] 김성훈 안상현 오선우 이상호 안승한 구본혁
## [277] 김용의 오재원 오정환 송성문 이우성 양종민
## [283] 한동희 허도환 나주환 조홍석 전병우 윤석민
## [289] 윤진호 김동엽 오윤석 최원준 양성우 노시환
## [295] 나종덕 김태근 전민재 김승회 양원혁 이승진
## 293 Levels: 강경학 강로한 강민국 강민호 강백호 강승호 강진성 고명성 ... 황진수
stat$팀명 <- str_replace(string = stat$팀명,
pattern = 'Hero',
replacement = '키움')
stat$팀명 <- as.factor(x = stat$팀명)
table(x = stat$팀명)
## x
## KIA KT LG NC SK 두산 롯데 삼성 키움 한화
## 36 28 28 31 32 28 33 30 22 32
데이터프레임의 행 단위로 원소의 중복이 있는 지 확인합니다.
duplicated(x = stat) %>% sum()
## [1] 0
데이터 분석을 용이하게 하기 위해 column 명을 변환합니다.
colnames(x = stat)
## [1] "선수명" "팀명" "경기" "타석" "타수" "안타" "홈런" "득점"
## [9] "타점" "볼넷" "삼진" "도루" "BABIP" "타율" "출루율" "장타율"
## [17] "OPS" "wOBA" "WAR"
stat <- stat %>% rename(이름 = 선수명)
colnames(x = stat)
## [1] "이름" "팀명" "경기" "타석" "타수" "안타" "홈런" "득점"
## [9] "타점" "볼넷" "삼진" "도루" "BABIP" "타율" "출루율" "장타율"
## [17] "OPS" "wOBA" "WAR"
범주형 변수에 대한 데이터 전처리는 완료하였습니다.
수치형 변수의 경우에는, NA값이 존재하는 지 확인하고 NA값을 상황에 맞게 처리합니다.
is.na(x = stat) %>% sum()
## [1] 41
# column 별로 확인하기
library(purrr)
map_int(.x = stat, .f = function(x) is.na(x = x) %>% sum())
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진
## 0 0 0 0 0 0 0 0 0 0 0
## 도루 BABIP 타율 출루율 장타율 OPS wOBA WAR
## 0 11 5 5 5 5 5 5
# 결측값 시각화하기
par(family = 'AppleGothic')
library(mice)
##
## Attaching package: 'mice'
## The following objects are masked from 'package:base':
##
## cbind, rbind
md.pattern(x = stat, rotate.names = TRUE)
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 타율 출루율
## 289 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 6 1 1 1 1 1 1 1 1 1 1 1 1 1 1
## 5 1 1 1 1 1 1 1 1 1 1 1 1 0 0
## 0 0 0 0 0 0 0 0 0 0 0 0 5 5
## 장타율 OPS wOBA WAR BABIP
## 289 1 1 1 1 1 0
## 6 1 1 1 1 0 1
## 5 0 0 0 0 0 7
## 5 5 5 5 11 41
결측값 처리는 domain 지식을 가지고 평가해야 합니다. 무조건 대체하거나 삭제하는 것은 옳지 않은 방법입니다.
stat 데이터프레임에서 NA는 주로 타석에 거의 서지 않는 대타 출전 타자 데이터 행에서 나타납니다. 위와 같은 경우에는 KBO 전체 타자 평가에 영향을 미치지 않기 때문에 (주전이 아니기 때문에 허수인 경우가 존재) 삭제해도 무방합니다.
결측값의 경우에는 전체 데이터에서 비중이 5% 미만일 경우 삭제해도 괜찮습니다. 비중이 5% 미만인 지 확인합니다.
NApcnt <- map_dbl(.x = stat, .f = function(x) is.na(x = x) %>% mean())
locs <- which(x = NApcnt >= 0.05)
print(x = locs)
## named integer(0)
해당 식에서는 NA percent가 0.05를 넘는 행이 0으로 나오기 때문에 결측치가 들어있는 행 전체를 삭제합니다.
stat <- stat %>% filter(complete.cases(stat))
print(x = stat)
## # A tibble: 289 x 19
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 BABIP
## <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 양의지… NC 118 459 390 138 20 61 68 48 43 4 0.354
## 2 김하성… 키움 139 625 540 166 19 112 104 70 80 33 0.328
## 3 최정 SK 141 606 503 147 29 86 99 70 92 3 0.303
## 4 샌즈 키움 139 613 525 160 28 100 113 77 101 1 0.329
## 5 로하스… KT 142 578 521 168 24 68 104 49 120 4 0.377
## 6 박병호… 키움 122 532 432 121 33 92 98 78 117 0 0.302
## 7 페르난데… 두산 144 645 572 197 15 87 88 61 54 1 0.358
## 8 이정후… 키움 140 630 574 193 6 91 68 45 40 13 0.352
## 9 박민우… NC 125 526 468 161 1 89 45 41 40 18 0.37
## 10 강백호… KT 116 505 438 147 13 72 65 61 87 9 0.392
## # … with 279 more rows, and 6 more variables: 타율 <dbl>, 출루율 <dbl>,
## # 장타율 <dbl>, OPS <dbl>, wOBA <dbl>, WAR <dbl>
print(is.na(x = stat) %>% sum())
## [1] 0
마지막 전체 확인
str(object = stat)
## tibble [289 × 19] (S3: tbl_df/tbl/data.frame)
## $ 이름 : Factor w/ 293 levels "강경학","강로한",..: 160 69 265 132 87 103 276 204 102 5 ...
## $ 팀명 : Factor w/ 10 levels "KIA","KT","LG",..: 4 9 5 9 2 9 6 9 4 2 ...
## $ 경기 : num [1:289] 118 139 141 139 142 122 144 140 125 116 ...
## $ 타석 : num [1:289] 459 625 606 613 578 532 645 630 526 505 ...
## $ 타수 : num [1:289] 390 540 503 525 521 432 572 574 468 438 ...
## $ 안타 : num [1:289] 138 166 147 160 168 121 197 193 161 147 ...
## $ 홈런 : num [1:289] 20 19 29 28 24 33 15 6 1 13 ...
## $ 득점 : num [1:289] 61 112 86 100 68 92 87 91 89 72 ...
## $ 타점 : num [1:289] 68 104 99 113 104 98 88 68 45 65 ...
## $ 볼넷 : num [1:289] 48 70 70 77 49 78 61 45 41 61 ...
## $ 삼진 : num [1:289] 43 80 92 101 120 117 54 40 40 87 ...
## $ 도루 : num [1:289] 4 33 3 1 4 0 1 13 18 9 ...
## $ BABIP : num [1:289] 0.354 0.328 0.303 0.329 0.377 0.302 0.358 0.352 0.37 0.392 ...
## $ 타율 : num [1:289] 0.354 0.307 0.292 0.305 0.322 0.28 0.344 0.336 0.344 0.336 ...
## $ 출루율: num [1:289] 0.438 0.389 0.399 0.396 0.381 0.398 0.409 0.386 0.403 0.416 ...
## $ 장타율: num [1:289] 0.574 0.491 0.519 0.543 0.53 0.56 0.483 0.456 0.434 0.495 ...
## $ OPS : num [1:289] 1.012 0.88 0.918 0.939 0.911 ...
## $ wOBA : num [1:289] 0.441 0.389 0.403 0.411 0.396 0.414 0.397 0.381 0.382 0.405 ...
## $ WAR : num [1:289] 7.02 6.77 6.63 6.09 5.56 5.14 4.81 4.81 4.71 4.6 ...
dim(x = stat)
## [1] 289 19
먼저 선수들의 타석 분포를 알아봅니다.
range(x = stat$타석)
## [1] 1 645
breaks <- seq(from = 0, to = 650, by = 50)
cuts <- cut(x = stat$타석, breaks=breaks, include.lowest = TRUE)
hist(x = stat$타석,
freq = TRUE,
breaks = breaks,
col = 'gray30',
main = '타석 히스토그램',
xlab = '타석',
ylab = '빈도수',
family = 'AppleGothic')
cuts %>%
table() %>%
prop.table() %>%
round(digits = 4L) * 100
## .
## [0,50] (50,100] (100,150] (150,200] (200,250] (250,300] (300,350] (350,400]
## 39.45 11.76 6.92 3.46 6.57 2.08 3.11 3.11
## (400,450] (450,500] (500,550] (550,600] (600,650]
## 5.19 5.54 6.92 3.11 2.77
타석이 50미만인 선수가 전체에 40%를 차지하고 있어 비율이 맞지 않습니다. 이 데이터를 제외하고 50 타석 초과인 핵심 선수들로 데이터 분석을 진행합니다.
stat50 <- stat %>% filter(타석 > 50)
hist(x = stat50$타석, freq = TRUE, breaks = breaks,
xlim = c(0, 700),
ylim = c(0, 40),
col = 'gray30',
border = 'gray50',
labels = TRUE,
main = '타석 50 이상 선수의 히스토그램',
xlab = '타석', ylab = '빈도수',
family = 'AppleGothic')
OPS는 출루율과 장타율을 합친 지수로 0.9 이상이면 좋은 타자로 여깁니다.
선수들의 성적 분포가 어떻게 되어있는 지 확인하고, 성적이 좋은 선수와 부진한 선수의 차이가 어느정도인지 수치로 확인합니다.
분포가 한쪽으로 편중되어 있다면 전체 평균보다는 상위 몇 %만 따로 빼서 보는 것이 의미가 있다고 여깁니다.
range(x = stat50$OPS)
## [1] 0.266 1.088
breaks <- seq(from = 0.2, to = 1.1, by = 0.1)
hist(x = stat50$OPS, breaks = breaks,
ylim = c(0, 65),
col = 'pink', labels = TRUE)
library(moments)
print(skewness(x = stat50$OPS))
## [1] -0.1638144
print(kurtosis(x = stat50$OPS))
## [1] 3.677465
OPS 분포의 경우 타석 50 이상의 선수들 데이터로 확인한 결과, 특별히 왜도와 첨도에 이상이 없어보입니다. 성적이 고루 분포되어 있는 것으로 생각하고 진행해도 무방합니다.
해당 그래프에서 0.9에 기준선을 넣어 OPS 0.9 이상의 우수 타자의 명수를 확인합니다.
hist(x = stat50$OPS, breaks = breaks,
ylim = c(0, 65),
col = 'pink', labels = TRUE)
abline(v = 0.9, col = 'green')
8명으로 나타납니다! 해당 선수 이름을 확인해볼까요?
stat50 %>%
select(이름, 팀명, OPS) %>%
filter(OPS >= 0.9) %>%
arrange(desc(OPS))
## # A tibble: 8 x 3
## 이름 팀명 OPS
## <fct> <fct> <dbl>
## 1 나성범 NC 1.09
## 2 양의지 NC 1.01
## 3 박병호 키움 0.958
## 4 샌즈 키움 0.939
## 5 최정 SK 0.918
## 6 로하스 KT 0.911
## 7 강백호 KT 0.911
## 8 러프 삼성 0.911
팀내에서 주로 홈런과 같은 장타성 타구를 구사하는 선수들이 많이 보입니다! 다른 항목들도 같이 확인해봅니다.
stat50 %>%
select(이름, 팀명, 홈런, 출루율, 장타율, OPS) %>%
filter(OPS >= 0.9) %>%
arrange(desc(홈런), desc(OPS))
## # A tibble: 8 x 6
## 이름 팀명 홈런 출루율 장타율 OPS
## <fct> <fct> <dbl> <dbl> <dbl> <dbl>
## 1 박병호 키움 33 0.398 0.56 0.958
## 2 최정 SK 29 0.399 0.519 0.918
## 3 샌즈 키움 28 0.396 0.543 0.939
## 4 로하스 KT 24 0.381 0.53 0.911
## 5 러프 삼성 22 0.396 0.515 0.911
## 6 양의지 NC 20 0.438 0.574 1.01
## 7 강백호 KT 13 0.416 0.495 0.911
## 8 나성범 NC 4 0.443 0.645 1.09
OPS를 통해 선수들의 점수 분포를 간략하게 살펴보았습니다. 본격적으로 다양한 수치 비교에 앞서 column별 상관관계에 대해서 확인합니다.
par(family = 'AppleGothic')
pairs(x = stat50[, 5:11])
# corrplot 패키지 이용하기
library(corrplot)
## corrplot 0.84 loaded
par(family = 'AppleGothic')
corr <- cor(x = stat50[, 5:11])
corrplot(corr = corr,
method = 'ellipse')
corrplot.mixed(corr = corr)
분포가 선형관계 강한 상관관계를 보이는 항목도 있지만, 산점도 그림을 볼 때 깔대기 모양으로 선형관계라고는 보기 어려운 항목도 존재합니다.
이번에는 출루율과 장타율 간 상관관계를 알아보고 산점도로 나타냅니다.
plot(x = stat50$출루율, y = stat50$장타율, pch = 19, col = 'blue',
main = '출루율 vs 장타율',
family = 'AppleGothic')
출루율과 장타율은 산점도로 볼 때 선형관계에 있다고 판단할 수 있습니다. 위 산점도 그림에 앞서 살펴본 OPS 0.9 이상의 7명 선수를 표시해보겠습니다.
ops90 <- stat50 %>% filter(OPS >= 0.9)
plot(x = stat50$출루율, y = stat50$장타율, pch = 19, col = 'blue',
main = '출루율 vs 장타율',
family = 'AppleGothic')
points(x = ops90$출루율, y = ops90$장타율, pch = 4, col = 'red')
text(x = ops90$출루율, y = ops90$장타율, labels = ops90$이름,
pos = 2, col = 'gray50',
family = 'AppleGothic',
cex = 0.5)
이번에는 한 팀의 데이터만 가지고 산점도를 그립니다. 삼성라이온즈 데이터를 이용하여 출루율과 장타율 산점도를 그려보고, 선수들의 특성을 해당 그래프를 통해 알아봅시다!
먼저, 삼성라이온즈 팀의 stat50 데이터만 가져옵니다.
samsung_stat50 <- stat50 %>% filter(팀명 == '삼성')
samsung_stat50
## # A tibble: 17 x 19
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 BABIP
## <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 러프 삼성 133 568 472 138 22 80 101 80 87 6 0.312
## 2 김상수… 삼성 129 543 468 127 5 76 38 50 67 21 0.305
## 3 이원석… 삼성 111 455 395 97 19 44 76 43 74 2 0.252
## 4 강민호… 삼성 112 393 346 81 13 36 45 33 76 0 0.262
## 5 이학주… 삼성 118 436 385 101 7 42 36 32 89 15 0.321
## 6 구자욱… 삼성 122 526 475 127 15 66 71 38 88 11 0.296
## 7 김헌곤… 삼성 114 467 411 122 5 57 46 37 41 10 0.315
## 8 박계범… 삼성 58 204 168 43 4 26 25 22 45 5 0.32
## 9 박해민… 삼성 144 581 506 121 5 64 44 58 82 24 0.275
## 10 윌리엄슨… 삼성 40 168 154 42 4 19 15 13 50 0 0.376
## 11 공민규… 삼성 28 60 53 13 3 4 6 6 18 0 0.313
## 12 박한이… 삼성 30 85 74 19 2 6 13 9 17 0 0.304
## 13 김도환… 삼성 61 106 93 19 2 7 7 8 36 0 0.304
## 14 최영진… 삼성 96 275 251 63 5 29 20 14 47 4 0.291
## 15 송준석… 삼성 26 56 51 12 1 6 4 2 14 0 0.297
## 16 김성훈… 삼성 65 88 77 14 1 14 5 7 11 4 0.2
## 17 김동엽… 삼성 60 211 195 42 6 15 25 12 47 2 0.25
## # … with 6 more variables: 타율 <dbl>, 출루율 <dbl>, 장타율 <dbl>, OPS <dbl>,
## # wOBA <dbl>, WAR <dbl>
위 데이터프레임을 이용하여 출루율과 장타율 산점도를 그려보고, 2X2 매트릭스로 나눠서 살펴봅니다.
plot(x = samsung_stat50$출루율,
y = samsung_stat50$장타율,
pch = 18,
col = 'blue',
xlab = '출루율',
ylab = '장타율',
main = '삼성라이온즈 출루율 vs 장타율',
family = 'AppleGothic')
abline(v = mean(samsung_stat50$출루율),
h = mean(samsung_stat50$장타율),
col = 'gray50',
lty = 2)
삼성라이온즈 팀 내에서 출루율과 장타율 모두 우수한 선수는 누구일까요?
samsung_1 <- samsung_stat50 %>% filter(출루율 >= mean(출루율),
장타율 >= mean(장타율))
plot(x = samsung_stat50$출루율,
y = samsung_stat50$장타율,
pch = 18,
col = 'blue',
xlab = '출루율',
ylab = '장타율',
main = '삼성라이온즈 출루율 vs 장타율',
family = 'AppleGothic')
abline(v = mean(samsung_stat50$출루율),
h = mean(samsung_stat50$장타율),
col = 'gray50',
lty = 2)
points(x = samsung_1$출루율, y = samsung_1$장타율,
pch = 4, col = 'red')
text(x = samsung_1$출루율, y = samsung_1$장타율,
labels = samsung_1$이름,
pos = 1, col = 'gray50', cex = 0.5,
family = 'AppleGothic')
print(x = samsung_1)
## # A tibble: 7 x 19
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 BABIP
## <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 러프 삼성 133 568 472 138 22 80 101 80 87 6 0.312
## 2 이원석 삼성 111 455 395 97 19 44 76 43 74 2 0.252
## 3 구자욱 삼성 122 526 475 127 15 66 71 38 88 11 0.296
## 4 김헌곤 삼성 114 467 411 122 5 57 46 37 41 10 0.315
## 5 박계범 삼성 58 204 168 43 4 26 25 22 45 5 0.32
## 6 윌리엄슨… 삼성 40 168 154 42 4 19 15 13 50 0 0.376
## 7 공민규 삼성 28 60 53 13 3 4 6 6 18 0 0.313
## # … with 6 more variables: 타율 <dbl>, 출루율 <dbl>, 장타율 <dbl>, OPS <dbl>,
## # wOBA <dbl>, WAR <dbl>
구자욱, 이원석, 공민규, 윌리엄슨, 박계범, 김헌곤, 러프로 나타나네요.
러프나 구자욱, 이원석, 김헌곤은 4~6번으로 이어지는 중심타선에 주로 위치하는 선수로 지표에 맞게 타선이 구성되는 것을 확인할 수 있습니다. 타석 수로 볼 때도 450번 이상으로 주전으로 활동하는 장타성 핵심 선수입니다.
그에 반해 박계범, 공민규는 타석 수가 확연히 차이납니다. 주전은 아니지만 한방이 있는 선수들로 주로 대타 출전으로 팀 득점에 도움을 주는 선수라고 이해할 수 있습니다.
(윌리엄슨의 경우는 제대로 확인하기 어렵습니다. 2019년 시즌 중반 이후에 영입된 선수라 제대로 된 평가가 힘듭니다.)
장타율은 비교적 낮지만 출루율은 높은 선수는 누구일까요? 아마 테이블 세터 선수로 발이 빠르고 민첩한 선수가 있지 않을까 예상합니다.
samsung_2 <- samsung_stat50 %>% filter(출루율 >= mean(출루율),
장타율 <= mean(장타율))
plot(x = samsung_stat50$출루율,
y = samsung_stat50$장타율,
pch = 18,
col = 'blue',
xlab = '출루율',
ylab = '장타율',
main = '삼성라이온즈 출루율 vs 장타율',
family = 'AppleGothic')
abline(v = mean(samsung_stat50$출루율),
h = mean(samsung_stat50$장타율),
col = 'gray50',
lty = 2)
points(x = samsung_2$출루율, y = samsung_2$장타율,
pch = 4, col = 'red')
text(x = samsung_2$출루율, y = samsung_2$장타율,
labels = samsung_2$이름,
pos = 1, col = 'gray50', cex = 0.5,
family = 'AppleGothic')
print(x = samsung_2)
## # A tibble: 3 x 19
## 이름 팀명 경기 타석 타수 안타 홈런 득점 타점 볼넷 삼진 도루 BABIP
## <fct> <fct> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 김상수 삼성 129 543 468 127 5 76 38 50 67 21 0.305
## 2 이학주 삼성 118 436 385 101 7 42 36 32 89 15 0.321
## 3 박한이 삼성 30 85 74 19 2 6 13 9 17 0 0.304
## # … with 6 more variables: 타율 <dbl>, 출루율 <dbl>, 장타율 <dbl>, OPS <dbl>,
## # wOBA <dbl>, WAR <dbl>
예상대로 테이블 세터로 활약 중인 김상수 선수가 들어와있네요! 타석으로 확인할 때 김상수와 이학주가 주전으로 활동 중이라고 볼 수 있습니다. 재밌는 점은 두 선수 모두 유격수 포지션이네요. 역시 발이 빠르고 민첩한 것이 출루율을 높일 수 있나봅니다.
이번에는 다양한 항목을 10개 팀별로 비교해봅니다.
stat50에서 팀별 선수 명수는 어떻게 될까요?
stat50 %>%
group_by(팀명) %>%
summarise(선수 = n()) %>%
arrange(desc(x = 선수))
## `summarise()` ungrouping output (override with `.groups` argument)
## # A tibble: 10 x 2
## 팀명 선수
## <fct> <int>
## 1 롯데 22
## 2 KIA 21
## 3 NC 20
## 4 한화 18
## 5 LG 17
## 6 삼성 17
## 7 KT 16
## 8 SK 15
## 9 두산 15
## 10 키움 14
롯데가 22명으로 가장 많고, 키움의 경우 14명으로 가장 적습니다.
그렇다면 팀별 홈런 개수는 어떨까요?
stat50 %>%
group_by(팀명) %>%
summarise(팀홈런 = sum(홈런)) %>%
arrange(desc(x = 팀홈런))
## `summarise()` ungrouping output (override with `.groups` argument)
## # A tibble: 10 x 2
## 팀명 팀홈런
## <fct> <dbl>
## 1 NC 125
## 2 삼성 119
## 3 SK 113
## 4 키움 112
## 5 KT 103
## 6 LG 94
## 7 롯데 89
## 8 한화 87
## 9 두산 84
## 10 KIA 73
KIA가 73개로 가장 적으며 NC가 125개로 가장 많습니다.
팀별 선수 명수를 기준으로 홈런 개수의 비율을 나타내면 직관적으로 비교가 가능합니다.
HR_ratio <- stat50 %>%
group_by(팀명) %>%
summarise(홈런비율 = sum(홈런) / n()) %>%
arrange(desc(x = 홈런비율))
## `summarise()` ungrouping output (override with `.groups` argument)
print(x = HR_ratio)
## # A tibble: 10 x 2
## 팀명 홈런비율
## <fct> <dbl>
## 1 키움 8
## 2 SK 7.53
## 3 삼성 7
## 4 KT 6.44
## 5 NC 6.25
## 6 두산 5.6
## 7 LG 5.53
## 8 한화 4.83
## 9 롯데 4.05
## 10 KIA 3.48
키움의 경우, 선수 명수가 가장 적지만 홈런 개수가 많이 때문에 선수 1명당 홈런 비율이 5 이상으로 높습니다. KIA의 경우 선수는 많지만 홈런 개수가 가장 적었기 때문에 홈런 비율도 낮게 나옵니다.
팀별 최다 홈런과 선수 1명당 홈런 비율도 비슷하게 가는지 확인합니다.
HR_max <- stat50 %>%
group_by(팀명) %>%
summarise(최다홈런 = max(홈런)) %>%
arrange(desc(x = 최다홈런))
## `summarise()` ungrouping output (override with `.groups` argument)
print(x = HR_max)
## # A tibble: 10 x 2
## 팀명 최다홈런
## <fct> <dbl>
## 1 키움 33
## 2 SK 29
## 3 KT 24
## 4 롯데 22
## 5 삼성 22
## 6 두산 21
## 7 한화 21
## 8 NC 20
## 9 KIA 17
## 10 LG 16
숫자로 보기에는 한눈에 들어오지 않습니다. 그래프를 그려 비교해봅니다.
# 팀별 1인당 홈런 비율
range(x = HR_ratio$홈런비율)
## [1] 3.47619 8.00000
bp1 <- barplot(height = HR_ratio$홈런비율, names.arg = HR_ratio$팀명,
col = 'orange',
ylim = c(0, max(HR_ratio$홈런비율) * 1.1),
main = '팀별 1인당 홈런 비율',
family = 'AppleGothic')
text(x = bp1, y = HR_ratio$홈런비율, labels = round(HR_ratio$홈런비율,2),
pos = 3, font = 2)
# 팀별 최다 홈런 개수
bp2 <- barplot(height = HR_max$최다홈런, names.arg = HR_max$팀명,
col = 'green',
ylim = c(0, max(HR_max$최다홈런) * 1.1),
main = '팀별 최다 홈런',
family = 'AppleGothic')
text(x = bp2, y = HR_max$최다홈런, labels = round(HR_max$최다홈런, 2),
pos = 3, font = 2)
팀별 1인당 홈런 비율과 최다 홈런은 완전히 일치하지는 않지만 순위는 비슷하다고 볼 수 있습니다. 해당 두 그래프의 상위권 2팀의 경우 (키움, SK) OPS 0.9 이상의 선수가 최다 홈런 보유자가 아닐까 싶은데요. 확인해볼까요?
# 최다 홈런 선수 확인하기
stat50 %>%
select(이름, 팀명, 홈런) %>%
arrange(desc(x = 홈런))
## # A tibble: 175 x 3
## 이름 팀명 홈런
## <fct> <fct> <dbl>
## 1 박병호 키움 33
## 2 최정 SK 29
## 3 로맥 SK 29
## 4 샌즈 키움 28
## 5 로하스 KT 24
## 6 러프 삼성 22
## 7 전준우 롯데 22
## 8 오재일 두산 21
## 9 이성열 한화 21
## 10 양의지 NC 20
## # … with 165 more rows
33개 홈런의 주인공은 키움의 박병호 선수이고, 홈런 29개는 SK의 최정과 로맥 선수의 기록입니다.
팀별 안타 지표를 상자수염그림으로 나타냅니다.
par(family = 'AppleGothic')
boxplot(formula = 안타 ~ 팀명,
data = stat50,
main = '팀별 안타 분포',
family = 'AppleGothic')
abline(h = median(x = stat50$안타),
col = 'red', lwd = 2)
안타 지표를 볼 때 안타 중간값을 기준으로 볼 때, KT, NC, SK, 두산, 삼성, 키움이 좋은 안타 성적을 보이고 있습니다.
특히 키움의 경우 1사분위수가 중간값보다 높게 나올 정도로 전체적으로 선수들의 안타 개수가 다른 팀에 비해 상향평준화되어 있습니다.
마지막으로 팀별 OPS 지표를 상자수염그림으로 나타냅니다.
par(family = 'AppleGothic')
boxplot(formula = OPS ~ 팀명,
data = stat50,
main = '팀별 OPS 분포')
abline(h = median(x = stat50$OPS),
col = 'red', lwd = 2)
OPS 지표는 NC가 가장 우수하다고 해석할 수 있습니다. 팀 OPS 중간값도 평균 OPS 중간값보다 높고 최소 관측값과 최대 관측값 사이의 간격이 가장 좁습니다. 이는 팀 NC의 전체 선수의 OPS 값이 평균적으로 높게 형성되어 있다고 볼 수 있습니다.
2019 KBO 타자 데이터로 살펴 본 바는 다음과 같습니다.
타석 수 분포 확인
전체 선수 OPS 분포 확인
전체 column 간 상관관계 확인
출루율 vs 장타율 상관관계 확인
10개 팀 별 홈런 / 안타 / OPS 지표 확인
홈런, 안타, OPS 지표를 종합적으로 살펴볼 때,
2019년 KBO 10개 팀 중 전체 선수의 성적 분포가 고르며 상향 평준화되어 있는 팀은 키움, SK, NC 정도로 볼 수 있으며, 키움과 NC가 2019년 코리아 시즌에서 우승하지 못하더라도, 2020년에 가장 주목받는 팀이 될 것이라 예상합니다.