현직자 인터뷰와 전문가 추산에 따르면 데이터 과학자는 전체 작업 시간 가운데 50~80%를 멋대로 생긴 데이터를 수집하고 준비하는 재미없는 일에 쓴다. ─ 뉴욕타임스 "For Big-Data Scientists, ‘Janitor Work’ Is Key Hurdle to Insight"
맞습니다. 세상에 괜히 '데이터 노가다(土方)'라는 표현이 존재하는 게 아닙니다.
그래서 세상에는 거꾸로 이 노가다를 조금이라도 덜 수고스럽게 만들어주는 도구도 존재합니다.
예컨대 tidyverse 패키지에 들어 있는 tidyr는 데이터를 롱 폼에서 와이드 폼 또는 그 반대로 '깔끔하게' 바꿀 수 있도록 도와줍니다.
그런데 사실 데이터 구조 변경마저도 너무 거창합니다.
실제로 데이터를 하다 보면 아주 사소하지만 번거롭고 귀찮은 작업을 진행해야 할 때도 많습니다.
이럴 때 도움을 받을 수 있는 패키지가 바로 janitor입니다. (영어 낱말 janitor는 '잡역부'라는 뜻입니다.)
janitor 패키지는 △열 이름을 깔끔하게 정리하고 △중복 데이터를 손쉽게 고를 수 있도록 도와주며 △분할표(contingency table) 작성에도 도움을 줍니다.
새 패키지가 등장했으니 먼저 패키지를 (설치하고) 불러오는 과정을 거쳐야 합니다.
이에 앞서 늘 그렇듯 tidyverse도 같이 (설치하고) 불러오도록 하겠습니다.
마지막으로 '스포츠 레퍼런스'에서 데이터를 크롤링해 예제로 쓸 수 있도록 rvest 패키지도 (설치하고) 불러옵니다.
#install.packages('tidyverse')
library('tidyverse')
## -- Attaching packages --------------------------------------- tidyverse 1.3.1 --
## v ggplot2 3.2.1 v purrr 0.3.2 ## v tibble 2.1.3 v dplyr 0.8.3 ## v tidyr 0.8.3 v stringr 1.4.0 ## v readr 1.3.1 v forcats 0.3.0
## -- Conflicts ------------------------------------------ tidyverse_conflicts() -- ## x dplyr::filter() masks stats::filter() ## x dplyr::lag() masks stats::lag()
#install.packages('janitor')
library('janitor')
## ## Attaching package: 'janitor'
## The following objects are masked from 'package:stats': ## ## chisq.test, fisher.test
#install.packages('rvest')
library('rvest')
## ## Attaching package: 'rvest'
## The following object is masked from 'package:readr': ## ## guess_encoding
'풋볼 레퍼런스'에서 지난해 K리그1 팀 기록을 긁어와 첫 번째 예제 데이터로 쓰겠습니다.
이 데이터를 가져 와 k_league_1이라는 변수에 넣어 놓는 코드는 이렇게 쓰면 됩니다.
read_html('https://fbref.com/en/comps/55/3930/2019-K-League-1-Stats') %>%
html_node(xpath = '//*[@id="stats_squads_standard_for"]') %>%
html_table() -> k_league_1
혹시 이 크롤링 코드가 이해가 가지 않으시면 '최대한 친절하게 쓴 R 크롤러 만들기' 포스트가 도움이 될 수 있습니다.
k_league_1을 열어 보면 열 이름이 엉망이라는 사실을 알 수 있습니다.
k_league_1
## # A tibble: 13 x 20 ## `` `` `` `` `Playing Time` `Playing Time` `Playing Time` ## <chr> <chr> <chr> <chr> <chr> <chr> <chr> ## 1 Squad # Pl Age "Poss" MP Starts Min ## 2 Daegu 31 25.8 "" 38 418 3,420 ## 3 FC Seoul 28 27.4 "" 38 418 3,420 ## 4 Gangwon 29 27.7 "" 38 418 3,420 ## 5 Gyeongnam 33 28.3 "" 38 418 3,420 ## 6 Incheon United 41 26.0 "" 38 418 3,420 ## 7 Jeju United 38 27.8 "" 38 418 3,420 ## 8 Jeonbuk 31 28.5 "" 38 418 3,420 ## 9 Pohang 31 26.2 "" 38 418 3,420 ## 10 Sangju Sangmu 40 26.5 "" 38 418 3,420 ## 11 Seongnam 35 26.5 "" 38 418 3,420 ## 12 Suw Bluewings 40 27.4 "" 38 418 3,420 ## 13 Ulsan Hyundai 29 29.1 "" 38 418 3,420 ## # ... with 13 more variables: Playing Time <chr>, Performance <chr>, ## # Performance <chr>, Performance <chr>, Performance <chr>, Performance <chr>, ## # Performance <chr>, Performance <chr>, Per 90 Minutes <chr>, ## # Per 90 Minutes <chr>, Per 90 Minutes <chr>, Per 90 Minutes <chr>, ## # Per 90 Minutes <chr>
우리가 예상하는 열 이름이 첫 행에도 들어 있는 상태입니다.
이런 일이 생긴 건 풋볼 레퍼런스에서 표 제목이 두 줄이기 때문입니다.
이번 데이터에서는 첫 줄보다 두 번째 줄이 열 이름에 더 잘 어울린다는 사실을 확인할 수 있습니다.
마이크로소프트(MS) 엑셀 데이터를 불러올 때도 이렇게 열 이름을 두 줄로 나타날 때가 있습니다.
이럴 때는 특정 행 데이터를 열 이름으로 바꿔주는 row_to_names() 함수를 쓰면 됩니다.
우리는 첫 줄을 열 이름으로 올리면 되니까 'row_number = 1'이라고 옵션을 주면 됩니다.
k_league_1 %>%
row_to_names(row_number = 1)
## Warning in row_to_names(., row_number = 1): Row 1 does not provide unique names. ## Consider running clean_names() after row_to_names().
## # A tibble: 12 x 20 ## Squad `# Pl` Age Poss MP Starts Min `90s` Gls Ast `G-PK` PK ## <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> ## 1 Daegu 31 25.8 "" 38 418 3,420 38.0 46 30 43 3 ## 2 FC Seoul 28 27.4 "" 38 418 3,420 38.0 52 36 48 4 ## 3 Gangwon 29 27.7 "" 38 418 3,420 38.0 55 29 51 4 ## 4 Gyeongn~ 33 28.3 "" 38 418 3,420 38.0 42 22 38 4 ## 5 Incheon~ 41 26.0 "" 38 418 3,420 38.0 33 17 30 3 ## 6 Jeju Un~ 38 27.8 "" 38 418 3,420 38.0 44 22 39 5 ## 7 Jeonbuk 31 28.5 "" 38 418 3,420 38.0 69 45 66 3 ## 8 Pohang 31 26.2 "" 38 418 3,420 38.0 49 34 44 5 ## 9 Sangju ~ 40 26.5 "" 38 418 3,420 38.0 48 30 40 8 ## 10 Seongnam 35 26.5 "" 38 418 3,420 38.0 29 16 25 4 ## 11 Suw Blu~ 40 27.4 "" 38 418 3,420 38.0 46 30 41 5 ## 12 Ulsan H~ 29 29.1 "" 38 418 3,420 38.0 69 38 66 3 ## # ... with 8 more variables: PKatt <chr>, CrdY <chr>, CrdR <chr>, Gls <chr>, ## # Ast <chr>, G+A <chr>, G-PK <chr>, G+A-PK <chr>
일단 열 이름을 바꾸는 데 성공했습니다.
에러 메시지가 뒤따르는 건 'Gls', 'Ast', 'G-PK'라는 열 이름이 누적 기록과 90분 환산 기록에 모두 들어 있기 때문입니다.
메시지에서 지시하는 대로 clean_names() 함수를 적용해보겠습니다.
이 결과를 다시 k_league_1 변수에 넣는 작업까지 같이 진행합니다.
(아직도 '<-' 기호로만 자료를 넣을 수 있다고 알고 계시는 건 아니시죠?)
k_league_1 %>%
row_to_names(row_number = 1) %>%
clean_names() -> k_league_1
## Warning in row_to_names(., row_number = 1): Row 1 does not provide unique names. ## Consider running clean_names() after row_to_names().
k_league_1
## # A tibble: 12 x 20 ## squad number_pl age poss mp starts min x90s gls ast g_pk pk ## <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> <chr> ## 1 Daegu 31 25.8 "" 38 418 3,420 38.0 46 30 43 3 ## 2 FC Se~ 28 27.4 "" 38 418 3,420 38.0 52 36 48 4 ## 3 Gangw~ 29 27.7 "" 38 418 3,420 38.0 55 29 51 4 ## 4 Gyeon~ 33 28.3 "" 38 418 3,420 38.0 42 22 38 4 ## 5 Inche~ 41 26.0 "" 38 418 3,420 38.0 33 17 30 3 ## 6 Jeju ~ 38 27.8 "" 38 418 3,420 38.0 44 22 39 5 ## 7 Jeonb~ 31 28.5 "" 38 418 3,420 38.0 69 45 66 3 ## 8 Pohang 31 26.2 "" 38 418 3,420 38.0 49 34 44 5 ## 9 Sangj~ 40 26.5 "" 38 418 3,420 38.0 48 30 40 8 ## 10 Seong~ 35 26.5 "" 38 418 3,420 38.0 29 16 25 4 ## 11 Suw B~ 40 27.4 "" 38 418 3,420 38.0 46 30 41 5 ## 12 Ulsan~ 29 29.1 "" 38 418 3,420 38.0 69 38 66 3 ## # ... with 8 more variables: p_katt <chr>, crd_y <chr>, crd_r <chr>, ## # gls_2 <chr>, ast_2 <chr>, g_a <chr>, g_pk_2 <chr>, g_a_pk <chr>
메시지는 사라지지 않았지만 문제 자체는 해결한 상태입니다.
90분 쪽에는 '_2'가 붙어서 열 이름이 gls_2, ast_2, g_pk_2라고 바뀐 겁니다.
이를 통해 열 이름이 전부 소문자로 바뀌었다는 사실도 알 수 있습니다.
특수 문자(#)도 사라졌고 숫자로 시작하는 열 이름도 `90s`에서 'x90s'로 달라졌습니다.
이렇게 clean_names()는 이름 그래도 열 이름을 보기 좋고 입력하기에도 편한 형태로 바꿔주는 구실을 합니다.
개인적으로는 clean_names() 함수 하나만으로도 janitor 패키지를 이용해야 할 이유가 충분하다고 생각합니다.
대문자를 입력하려고 시프트를 누르는 일만 줄어들어도 세상에 얼마나 편해지는지 모릅니다.
janitor 패키지에는 중복 데이터를 알려주는 get_dupes() 함수도 들어 있습니다.
이번에는 '베이스볼 레퍼런스'에서 지난해 프로야구 팀 타격 기록을 불러와서 예제 데이터로 쓰겠습니다.
(어른들 사정으로 베이스볼 레퍼런스는 크롤링 코드가 조금 더 복잡한 형태입니다.)
'https://www.baseball-reference.com/register/league.cgi?id=6a2bec36' %>%
read_html() %>%
paste0(collapse = '') %>%
str_remove_all(c('<!--|-->')) %>%
read_html() %>%
html_node('table#league_batting') %>%
html_table() -> kbo_batting
kbo_batting
## # A tibble: 11 x 27 ## Finals Aff BatAge `R/G` G PA AB R H `2B` `3B` HR ## <chr> <lgl> <dbl> <dbl> <int> <int> <int> <int> <int> <int> <int> <int> ## 1 Kiwoom He~ NA 26.5 5.42 144 5658 4991 780 1405 251 38 112 ## 2 Doosan Be~ NA 29.5 5.11 144 5670 4913 736 1364 235 31 84 ## 3 NC Dinos NA 28.7 4.68 144 5631 4968 674 1383 249 26 128 ## 4 SK Wyverns NA 30.8 4.55 144 5541 4919 655 1290 218 14 117 ## 5 KT Wiz NA 28.2 4.51 144 5582 4965 650 1375 187 20 103 ## 6 LG Twins NA 29.5 4.45 144 5536 4928 641 1316 233 15 94 ## 7 Samsung L~ NA 28.8 4.32 144 5533 4866 622 1245 230 26 122 ## 8 Hanwha Ea~ NA 28.9 4.22 144 5482 4882 607 1250 223 16 88 ## 9 Kia Tigers NA 28.1 4.2 144 5505 4874 605 1286 248 19 76 ## 10 Lotte Gia~ NA 29.3 4.01 144 5488 4919 578 1231 214 22 90 ## 11 League To~ NA 28.8 4.55 1440 55626 49225 6548 13145 2288 227 1014 ## # ... with 15 more variables: RBI <int>, SB <int>, CS <int>, BB <int>, ## # SO <int>, BA <dbl>, OBP <dbl>, SLG <dbl>, OPS <dbl>, TB <int>, GDP <int>, ## # HBP <int>, SH <int>, SF <int>, IBB <int>
get_dupes() 함수를 쓰실 때는 괄호 안에다 어떤 열이 겹치는지 확인해 달라고 알려줘야 합니다.
도루(SB) 숫자가 똑같은 팀이 있는지 알아보는 코드는 이렇게 쓸 수 있습니다.
물론 clean_names() 함수를 쓰고 나면 열 이름도 그냥 소문자로 치면 됩니다.
kbo_batting %>%
clean_names() %>%
get_dupes('sb')
## # A tibble: 2 x 28 ## sb dupe_count finals aff bat_age r_g g pa ab r h ## <int> <int> <chr> <lgl> <dbl> <dbl> <int> <int> <int> <int> <int> ## 1 87 2 NC Dinos NA 28.7 4.68 144 5631 4968 674 1383 ## 2 87 2 Kia Tigers NA 28.1 4.2 144 5505 4874 605 1286 ## # ... with 17 more variables: x2b <int>, x3b <int>, hr <int>, rbi <int>, ## # cs <int>, bb <int>, so <int>, ba <dbl>, obp <dbl>, slg <dbl>, ops <dbl>, ## # tb <int>, gdp <int>, hbp <int>, sh <int>, sf <int>, ibb <int>
NC와 KIA가 똑같이 도루를 72개씩 기록했다는 사실을 확인할 수 있습니다.
기준 여러 개가 똑같은 자료를 찾고 싶을 때는 get_dupes() 안에 열 이름을 계속 붙여 넣기면 하면 됩니다.
분할표는 특정한 범수를 기준으로 관측값이 몇 개가 있는지 알려주는 표입니다.
janitor 패키지에서는 tabyl() 함수가 분할표를 그리는 기능을 담당합니다.
R 기본 데이터 세트 mtcars에 실린더(cyl) 숫자별 차량이 몇 대나 들어 있는지는 이렇게 쓸 수 있습니다.
mtcars %>%
tabyl(cyl)
>## cyl n percent ## 4 11 0.34375 ## 6 7 0.21875 ## 8 14 0.43750
기준이 여러 개여도 좋습니다. 실린더와 기어(gear) 숫자별로 차량 대수를 구해보겠습니다.
mtcars %>%
tabyl(cyl, gear)
## cyl 3 4 5 ## 4 1 8 2 ## 6 2 4 1 ## 8 12 0 2
아, 마지막으로 소개해 드릴 함수가 하나 더 있습니다.
MS 엑셀 데이터를 불러오다 보면 엑셀에서는 분명 날짜가 날짜였는데 R에서는 평범한 숫자로 바뀔 때가 있습니다.
이건 엑셀에서 1900년 1월 1일 = 1이라고 기준을 정해 날짜를 계산하는 방식을 채택하고 있기 때문입니다.
오늘(2020년 1월 31일)은 이날로부터 4만3861일이 지난 날이라 43861에 해당합니다.
excel_number_to_date() 함수에 이 숫자를 넣으면 오늘 날짜로 바꿀 수 있습니다.
excel_numeric_to_date(43861)
## [1] "2020-01-31"
이 함수를 쓰실 일이 그리 많을 것 같지는 않지만 이런 함수야 말로 알고 계실 때와 모를 때 '노가다' 수준이 천지차이가 아닐까요?
그럼 모두들 Happy Tidyversing -_-)/
댓글,