세상을 살다 보면 분명히 테이블 두 개를 합쳐야 하는 일이 생깁니다.
마이크로소프트(MS) 엑셀 사용에 익숙한 분이라면 vlookup() 함수 사용법을 알 때와 모를 때 퇴근 시간이 얼마나 달라지는지 경험해 보셨으리라 믿습니다.
R에서는 원래 merge() 함수가 같은 구실을 합니다. 그리고 이런 기본 함수가 있으면 Tidyverse 생태계에도 비슷한 구실을 하는 함수가 있게 마련.
이번 포스트에서는 테이블을 합칠 때 쓰면 좋은 소위 '두 테이블 동사(Two-Table Verbs)'를 알아보도록 하겠습니다.
두 테이블 동사는 크게 inner_join(), left_join(), right_join(), full_join() 등 네 가지로 나눌 수 있습니다. 이번 포스트에서는 여기에 필터링 기능까지 함께 제공하는 semi_join(), anti_join()까지 총 여섯 가지 동사를 알아보겠습니다.
일단 아래 카드에서 각 탭을 클릭하시면 각 동사가 어떤 방식으로 작동하는지 GIF 파일을 통해 확인해 보실 수 있습니다.
자, 이제 개념을 익히셨을 줄로 믿고 R로 실습을 해보겠습니다. 제일 먼저 할 일은 물론 tidyverse 패키지 불러오기.
library('tidyverse')
이어서 연습용 테이블 두 개를 만듭니다. 먼저 프로야구 역대 단일 시즌 최다 홈런 톱10 테이블(홈런)을 하나 만들고, 지난해(2018년) 기준 프로야구 10개 구단 팀(모기업) 이름과 애칭을 담은 테이블(팀)도 하나 만들겠습니다.
아, 혹시 모르시는 분께 말씀드리자면 R에서는 이렇게 MS 엑셀 시트처럼 테이블에 데이터를 저장하는 형식을 '데이터 프레임'이라고 부릅니다. 여기서 쓰는 Tibble은 Tidyverse 생태계에서 쓰는 데이터 프레임이라고 이해하시면 됩니다.
홈런 <- tibble(연도=c(2003, 1999, 2015, 2003, 2014, 2015, 2002, 2015, 2002, 2017),
선수=c('이승엽', '이승엽', '박병호', '심정수', '박병호', '나바로', '이승엽', '테임즈', '심정수', '최정'),
홈런=c(56, 54, 53, 53, 52, 48, 47, 47, 46, 46),
팀=c('삼성', '삼성', '넥센', '현대', '넥센', '삼성', '삼성', 'NC', '현대', 'SK'))
팀 <- tibble(팀=c('KIA', 'KT', 'LG', 'NC', 'SK', '넥센', '두산', '롯데', '삼성', '한화'),
애칭=c('타이거즈', '위즈', '트윈스', '다이노스', '와이번스', '히어로즈', '베어스', '자이언츠', '라이온즈', '이글스'))
이렇게 입력하면 어떤 테이블이 나오는지 한 번 확인해 볼까요?
홈런
## # A tibble: 10 x 4 ## 연도 선수 홈런 팀 ## <dbl> <chr> <dbl> <chr> ## 1 2003 이승엽 56 삼성 ## 2 1999 이승엽 54 삼성 ## 3 2015 박병호 53 넥센 ## 4 2003 심정수 53 현대 ## 5 2014 박병호 52 넥센 ## 6 2015 나바로 48 삼성 ## 7 2002 이승엽 47 삼성 ## 8 2015 테임즈 47 NC ## 9 2002 심정수 46 현대 ## 10 2017 최정 46 SK
팀
## # A tibble: 10 x 2 ## 팀 애칭 ## <chr> <chr> ## 1 KIA 타이거즈 ## 2 KT 위즈 ## 3 LG 트윈스 ## 4 NC 다이노스 ## 5 SK 와이번스 ## 6 넥센 히어로즈 ## 7 두산 베어스 ## 8 롯데 자이언츠 ## 9 삼성 라이온즈 ## 10 한화 이글스
제일 먼저 R 기본 함수인 merge()로 이 두 테이블을 합쳐 보겠습니다.
merge(홈런, 팀)
## 팀 연도 선수 홈런 애칭 ## 1 NC 2015 테임즈 47 다이노스 ## 2 SK 2017 최정 46 와이번스 ## 3 넥센 2015 박병호 53 히어로즈 ## 4 넥센 2014 박병호 52 히어로즈 ## 5 삼성 1999 이승엽 54 라이온즈 ## 6 삼성 2002 이승엽 47 라이온즈 ## 7 삼성 2003 이승엽 56 라이온즈 ## 8 삼성 2015 나바로 48 라이온즈
분명 두 테이블에 자료가 10줄씩 있었는데 8줄만 살아 남았습니다. 현대 (유니콘스)라는 팀이 이미 사라졌기 때문에 심정수가 남긴 기록이 사라졌습니다.
또 한 가지 문제점이 있다면 팀 이름 순서로 자료 선수도 바뀌었다는 것. 만약 홈런 테이블에 애칭만 붙이고 싶었다면 다시 정렬을 해야 하는 번거로움이 있습니다.
그럼 이번에는 inner_join()을 한번 써볼까요?
inner_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 8 x 5 ## 연도 선수 홈런 팀 애칭 ## <dbl> <chr> <dbl> <chr> <chr> ## 1 2003 이승엽 56 삼성 라이온즈 ## 2 1999 이승엽 54 삼성 라이온즈 ## 3 2015 박병호 53 넥센 히어로즈 ## 4 2014 박병호 52 넥센 히어로즈 ## 5 2015 나바로 48 삼성 라이온즈 ## 6 2002 이승엽 47 삼성 라이온즈 ## 7 2015 테임즈 47 NC 다이노스 ## 8 2017 최정 46 SK 와이번스
일단 홈런 테이블 순서는 유지했는데 이번에도 심정수 기록이 모두 사라졌습니다. 기본적으로 왼쪽 테이블(홈런) 순서를 유지한 걸 제외하면 merge()와 같은 결과물입니다.
사실 이게 꼭 나쁘다고만 볼 수는 없습니다. 경우에 따라서는 양쪽에 데이터가 모두 있을 때만 합치고 싶을 때도 있을 테니까요.
아직 동사가 많이 남아 있으니까 10줄을 모두 남겨주는 함수도 있겠죠? 이번에는 left_join()을 써봅시다.
left_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 10 x 5 ## 연도 선수 홈런 팀 애칭 ## <dbl> <chr> <dbl> <chr> <chr> ## 1 2003 이승엽 56 삼성 라이온즈 ## 2 1999 이승엽 54 삼성 라이온즈 ## 3 2015 박병호 53 넥센 히어로즈 ## 4 2003 심정수 53 현대 <NA> ## 5 2014 박병호 52 넥센 히어로즈 ## 6 2015 나바로 48 삼성 라이온즈 ## 7 2002 이승엽 47 삼성 라이온즈 ## 8 2015 테임즈 47 NC 다이노스 ## 9 2002 심정수 46 현대 <NA> ## 10 2017 최정 46 SK 와이번스
이번에는 왼쪽 10줄이 모두 살아 남았습니다. 나머지는 모두 애칭을 제대로 찾아 붙였고 심정수가 자리잡고 있는 두 줄에만 자료를 찾을 수 없다(<NA>)는 표시가 들어갔습니다.
이제 오른쪽 테이블을 기준으로 또 합쳐 봅시다.
right_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 14 x 5 ## 연도 선수 홈런 팀 애칭 ## <dbl> <chr> <dbl> <chr> <chr> ## 1 NA <NA> NA KIA 타이거즈 ## 2 NA <NA> NA KT 위즈 ## 3 NA <NA> NA LG 트윈스 ## 4 2015 테임즈 47 NC 다이노스 ## 5 2017 최정 46 SK 와이번스 ## 6 2015 박병호 53 넥센 히어로즈 ## 7 2014 박병호 52 넥센 히어로즈 ## 8 NA <NA> NA 두산 베어스 ## 9 NA <NA> NA 롯데 자이언츠 ## 10 2003 이승엽 56 삼성 라이온즈 ## 11 1999 이승엽 54 삼성 라이온즈 ## 12 2015 나바로 48 삼성 라이온즈 ## 13 2002 이승엽 47 삼성 라이온즈 ## 14 NA <NA> NA 한화 이글스
이번에는 오른쪽 자료가 전부 등장했습니다. 단일 시즌 홈런 톱10 선수를 배출하지 못한 KIA, KT, LG, 두산, 롯데, 한화도 전부 이름을 드러냈습니다. 대신 심정수가 빠졌습니다. 팀 테이블에는 현대가 없었으니까요.
자, 그럼 마지막으로 full_join()도 가보겠습니다.
full_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 16 x 5 ## 연도 선수 홈런 팀 애칭 ## <dbl> <chr> <dbl> <chr> <chr> ## 1 2003 이승엽 56 삼성 라이온즈 ## 2 1999 이승엽 54 삼성 라이온즈 ## 3 2015 박병호 53 넥센 히어로즈 ## 4 2003 심정수 53 현대 <NA> ## 5 2014 박병호 52 넥센 히어로즈 ## 6 2015 나바로 48 삼성 라이온즈 ## 7 2002 이승엽 47 삼성 라이온즈 ## 8 2015 테임즈 47 NC 다이노스 ## 9 2002 심정수 46 현대 <NA> ## 10 2017 최정 46 SK 와이번스 ## 11 NA <NA> NA KIA 타이거즈 ## 12 NA <NA> NA KT 위즈 ## 13 NA <NA> NA LG 트윈스 ## 14 NA <NA> NA 두산 베어스 ## 15 NA <NA> NA 롯데 자이언츠 ## 16 NA <NA> NA 한화 이글스
이제 모든 자료가 다 등장했습니다. 심정수가 몸담았던 현대는 여전히 애칭이 없고, 홈런 톱10을 배출하지 못한 팀은 연도와 선수가 비어 있지만 자료 자체가 빠진 건 없습니다.
그럼 이 중에서 양쪽 테이블에 모두 자료가 있는 것만 뽑고 싶을 때는 어떻게 하면 될까요? 그때 쓰는 동사가 바로 semi_join()입니다.
semi_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 8 x 4 ## 연도 선수 홈런 팀 ## <dbl> <chr> <dbl> <chr> ## 1 2003 이승엽 56 삼성 ## 2 1999 이승엽 54 삼성 ## 3 2015 박병호 53 넥센 ## 4 2014 박병호 52 넥센 ## 5 2015 나바로 48 삼성 ## 6 2002 이승엽 47 삼성 ## 7 2015 테임즈 47 NC ## 8 2017 최정 46 SK
결과를 보면 확인할 수 있는 것처럼 semi_join()은 다른 테이블에도 자료가 있는지 없는지만 알려줄 뿐 실제 데이터를 합치는 건 아닙니다.
anti_join()은 거꾸로 다른 테이블에는 자료가 없는 결과만 보여줍니다. 심정수가 나오겠죠? 이번에도 실제로 데이터를 합치지는 않습니다.
anti_join(홈런, 팀)
## Joining, by = "팀"
## # A tibble: 2 x 4 ## 연도 선수 홈런 팀 ## <dbl> <chr> <dbl> <chr> ## 1 2003 심정수 53 현대 ## 2 2002 심정수 46 현대
종합하면 vlookup()과 기능적으로 제일 비슷한 녀석은 left_join()이겠지만 데이터 특성에 따라서 나머지 동사도 쓸 때가 있을 겁니다.
아, 그리고 연습에서는 기준이 되는 열(변수)이 전부 '팀'이라 따로 지정하지 않았는데 여러 열 중에 따로 필요한 게 있으시다면 *_join() 안에 by를 쓰시면 됩니다. 그러니까 left_join(홈런, 팀, by='팀')처럼 쓰시면 됩니다.
그럼 Will you join R?
댓글,