聰明不如鈍筆
총명불여둔필
assignment Kidult

최대한 친절하게 쓴 R로 데이터 더 빨리 처리하기(feat. data.table)

access_time 2018.09.10 13:43


R에는 느리다는 비판이 따라다닙니다. 틀린 말도 아닙니다. R는 메모리에 데이터를 불러들여 작동하는 인메모리(in-memory) 방식이기 때문에 한 번에 다를 수 있는 행과 열 숫자가 적을 수밖에 없고, 그 때문에 속도에서 손해를 볼 수밖에 없습니다.


R는 대신 누구나 기능을 추가할 수 있는 오픈 소스(open source) 프로그램이라 이런 단점을 극복하려는 시도도 계속 이어지고 있습니다. 그 대표 사례가 바로 이번 포스트에서 우리가 다룰 data.table입니다. 구글링을 잠깐만 해보셔도 이 패키지 속도를 칭찬하는 글이 인터넷에 얼마나 많은지 금방 아실 수 있을 겁니다.


이 포스트에서는 국제축구연맹(FIFA)에서 주관한 국가대표 축구 경기(A매치) 데이터를 가지고 1872년 이후 A매치에서 가장 많이 이긴 팀은 어디인지, 경기당 골이 가장 많은 팀은 어디인지 등을 알아보도록 하겠습니다. 


물론 검색 엔진에서 data.table을 입력해서 이 포스트에 찾아오신 분이라면 이미 R가 뭔지 잘 알고 계실 겁니다. 그래도 노파심에 설명드리자면 R 홈페이지는 이 프로그램을 통계 계산과 그래픽에 활용하는 무료 소프트웨어 환경(R is a free software environment for statistical computing and graphics)"이라고 설명할 수 있습니다.


아직 R를 설치하지 않으셨다면 누구나 이 링크에서 내려받아 사용하실 수 있습니다. 2018년 9월 현재 최신 버전은 3.5.1이며 마이크로소프트(MS) 윈도용은 이 페이지에서 다운로드 받으실 수 있습니다. 이 포스트는 '최대한 친절하게'를 목표로 하고 있지만 프로그램 설치법까지 모르시는 분이 아니 계실 것이라고 생각해 따로 설명하지는 않겠습니다.


설치가 끝나고 R를 실행하시면 아래처럼 진짜 막막한 화면이 나타납니다. 이 창을 'R 콘솔(Console)'이라고 부릅니다.



이 스크린샷에는 제가 미리 "print('Hello, World!')"라고 입력한 게 보입니다. R는 이렇게 '>' 뒤에 명령어(함수)를 입력하는 방식으로 작동합니다. (예상하시는 것처럼) 제가 쓴 명령어는 'Hello, World!'라는 표현을 화면에 출력해달라는 뜻입니다. 


이렇게 입력한 함수를 코드(code)라고 부르며 코드를 입력하는 과정이 바로 코딩(coding)입니다. 코딩은 프로그래밍이라고 부르기도 합니다. 사용자가 프로그래밍을 할 수 있도록 만든 소프트웨어는 프로그래밍 언어라고 부릅니다. 각 프로그래밍 언어 문법에 맞도록 코드를 입력하면 프로그램을 만들 수 있습니다. 이 그림에 나온 "Hello, World!" 프로그램은 코딩을 배울 때 맨 처음에 제일 많이 쓰는 예제입니다.


프로그래밍 언어 대부분은 누군가 미리 만들어 놓은 추가 기능을 내려 받아 활용할 수 있도록 하고 있습니다. R에서는 이렇게 추가 기능을 묶어 놓은 걸 패키지(package)라고 부릅니다. 이번 포스트에서 다룰 data.table이 바로 패키지입니다.


R에서 패키지를 설치할 때는 'install.packages()'라는 함수를 씁니다. 우리는 data.table을 내려받을 거니까 이렇게 입력하면 됩니다.

install.packages('data.table')

이렇게 치고 나서 엔터를 누르면 R는 어떤 (미러) 사이트에서 패키지를 내려받을지 물어볼 겁니다. 기본적으로 어떤 페이지에서 내려받아도 관계가 없습니다. 그냥 엔터를 치셔도 설치를 진행합니다.


패키지를 내려받았다고 곧바로 이 패키지에 들어 있는 함수를 활용할 수 있는 건 아닙니다. 이 패키지를 메모리에 읽어들이는 과정이 필요합니다. 이때 쓰는 함수는 'library()'입니다. 

library('data.table')

만약 이전에 install.packages()로 패키지를 내려받으신 적이 있다면 그다음에는 설치 과정 없이 바로 library()로 불러들여서 쓰시면 됩니다. 


여기까지 오다 보니 벌써 R 함수를 세 개 - print(), install.packages(), library() - 배우셨습니다. 별로 어렵지 않죠?



역대 A매치 데이터 불러오기

이제 data.table에 들어 있는 함수를 하나 배우겠습니다. 외부 파일에 들어 있는 데이터를 R 안으로 불러오는 'fread()' 함수입니다. 우리가 이번 포스트에서 쓸 파일은 아래에서 내려받으시면 됩니다.


results.csv


1872년부터 2018 러시아 월드컵까지 A 매치 경기 결과를 담고 있는 이 파일은 CSV(Comma Separated Value·쉼표로 구분한 값)라는 형태입니다. CSV는 기본적으로 텍스트 파일입니다. 표에서 열에 해당하는 내용을 쉼표로 구분했기 때문에 이런 이름이 붙었습니다. 컴퓨터에 MS 엑셀이 깔려 있다면 다른 엑셀 파일을 열듯이 이 파일을 열어 내용을 확인해 보실 수 있습니다.


R에는 기본적으로 CSV 파일을 불러올 수 있도록 'read.csv()'라는 함수가 들어있습니다. 그래서 이렇게 입력하면 이 파일을 R로 불러오실 수 있습니다.

read.csv('results.csv')

파일을 잘 불러왔나요? 혹시 아래 같은 에러 메시지를 출력하지 않았나요?


이런 에러가 떴다면 파일 위치를 지정하지 않았기 때문입니다. R는 별도로 파일 위치를 표시하지 않으면 작업 디렉토리(폴더)에 있는 파일을 불러오게 됩니다. MS 윈도에서는 '내 문서'가 기본 작업 폴더입니다. 만약 한 번도 작업 폴더를 바꾸신 적이 없다면 이 CSV 파일을 내 문서 안으로 옮기셔야 합니다.


물론 작업 폴더를 바꾸셔도 됩니다. 작업 폴더를 바꾸는 가장 기본적인 방법은 메뉴에서 File - Change dir...를 선택하는 겁니다. 그러면 작업 폴더를 바꿀 수 있도록 선택창이 뜹니다. 파일을 다른 위치에 받으셨다면 그리로 작업 폴더를 바꿔보세요. 명령어를 쓰시는 쪽이 편하시다면 'setwd()'를 활용하셔도 됩니다.


이렇게 폴더 설정이 끝났다면 이번에는 read.csv()가 정상 작동할 겁니다.

read.csv('results.csv')

파일 내용이 주루룩 나타났나요? 그렇다면 성공하신 겁니다. 그런데 read.csv()는 그저 파일을 읽어오라는 뜻이기 때문에 한번 화면에 파일을 출력하고 나면 파일 내용이 메모리에서 사리지고 맙니다. 그래서 컴퓨터 메모리에 빈 방(공간)을 만들어 이 내용을 담아둬야 합니다. 프로그래밍에서는 이런 방을 '변수'라고 부릅니다. R에서 변수에 어떤 값을 넣을 때는 '<-' 또는 '=' 기호를 씁니다.


우리는 축구(soccer)를 줄인 'sc'라는 변수에 이 데이터를 넣도록 하겠습니다. 그러면 이렇게 쓰면 될 겁니다.

sc <- read.csv('results.csv')

이번에는 아무 반응이 없는 게 정상입니다. 대신 데이터 맨 첫 여섯 줄만 보여주는 'head()' 함수를 쓰시면 내용이 잘 들어와 있는 걸 확인하실 수 있습니다.

head(sc)
##         date     team opponent team_score opponent_score tournament
## 1 1872-11-30 Scotland  England          0              0   Friendly
## 2 1872-11-30  England Scotland          0              0   Friendly
## 3 1873-03-08  England Scotland          4              2   Friendly
## 4 1873-03-08 Scotland  England          2              4   Friendly
## 5 1874-03-07 Scotland  England          2              1   Friendly
## 6 1874-03-07  England Scotland          1              2   Friendly

위에서는 fread()로 읽기로 해놓고 왜 read.scv()를 썼냐고요? 둘을 비교해 보려는 이유입니다. 

sc_dt <- fread('results.csv')

R에는 어떤 작업을 할 때 시간이 얼마나 걸리는지 예상 시간을 알려주는 'system.time()'이라는 함수도 들어 있습니다. 제 컴퓨터에서 read.csv()와 fread()를 각각 써서 이 파일을 불러들이는 시간은 이렇게 비교할 수 있습니다.

system.time(sc <- read.csv('results.csv'))
##    user  system elapsed 
##    0.17    0.00    0.18
system.time(sc_dt <- fread('results.csv')
##    user  system elapsed 
##    0.01    0.00    0.01

그러니까 read.csv()를 쓰면 0.18초가 걸리고 data.table을 쓰면 0.01초로 18분의 1 정도밖에 걸리지 않는다는 뜻입니다. 지금은 3.9mb 정도 되는 작은 파일을 열여서 미미한 차이지만 정말 '빅데이터'를 다루면 무시 못할 차이가 될 겁니다.


또 fread()로 파일을 불러오면 (당연히) '데이터 테이블'이라는 형식으로 메모리에 존재하게 됩니다. 이 형식이 재미있는 점 가운데 하나는 파일 이름만 쳐도 자동으로 위, 아래 다섯 줄씩만 보여준다는 점입니다. 보통은 파일 내용 전부를 보여주는데 말입니다. (콘솔에 변수 이름만 치는 건 그냥 변수 내용을 출력해달라는 뜻입니다.)

sc <- sc_dt #앞으로 타이핑을 줄이려고 sc_dt를 sc에 넣었습니다.
sc
##              date     team opponent team_score opponent_score
##     1: 1872-11-30 Scotland  England          0              0
##     2: 1872-11-30  England Scotland          0              0
##     3: 1873-03-08  England Scotland          4              2
##     4: 1873-03-08 Scotland  England          2              4
##     5: 1874-03-07 Scotland  England          2              1
##    ---                                                       
## 79334: 2018-07-07   Russia  Croatia          2              2
## 79335: 2018-07-07  England   Sweden          2              0
## 79336: 2018-07-07  Croatia   Russia          2              2
## 79337: 2018-07-10   France  Belgium          1              0
## 79338: 2018-07-10  Belgium   France          0              1
##            tournament
##     1:       Friendly
##     2:       Friendly
##     3:       Friendly
##     4:       Friendly
##     5:       Friendly
##    ---               
## 79334: FIFA World Cup
## 79335: FIFA World Cup
## 79336: FIFA World Cup
## 79337: FIFA World Cup
## 79338: FIFA World Cup

고생 많으셨습니다. 이제 정말 data.table을 공부할 준비가 끝이 났습니다. 


만약 fread()로 불러들여야 하는데 실수로 read.scv()를 쓰셨을 때는 어떻게 해야 할까요? 물론 다시 fread()를 쓰셔도 되지만 as.data.table()을 활용하셔도 됩니다. 이렇게 말입니다.

sc <- as.data.table(sc)

이런 경우뿐만 아니라 데이터 프레임 형태로 된 자료를 데이터 테이블로 바꾸고자 하실 때는 언제든 이렇게 쓰시면 됩니다.



DT[i, j, by][ ][ ]…

data.table은 기본적으로 대괄호([ ]) 안에 [행, 열, 그룹 조건]을 입력하는 방식으로 작동합니다. 행과 열을 지정하는 건 read.csv()로 불러왔을 때 기본 데이터 형식인 데이터 프레임(data.frame)과 마찬가지 방식입니다.


예를 들어 sc[1,]은 sc에서 첫 번째 행을 출력하라는 뜻입니다. 그러면 위에서 보신 것처럼 스코틀랜드가 1987년 11월 30일에 잉글랜드를 상대한 경기가 나와야 할 겁니다.

sc[1,]
##          date     team opponent team_score opponent_score tournament
## 1: 1872-11-30 Scotland  England          0              0   Friendly

숫자로 행 번호만 쓸 수 있는 건 아닙니다. 만약 한국(이 데이터에서는 'Korea Republic') 결과만 확인하고 싶을 때는 이렇게 쓰면 됩니다.

sc[team=='Korea Republic', ]
##            date           team  opponent team_score opponent_score
##   1: 1949-01-02 Korea Republic     China          2              3
##   2: 1949-01-16 Korea Republic   Vietnam          3              3
##   3: 1949-01-25 Korea Republic     Macau          5              1
##   4: 1950-04-16 Korea Republic    Taiwan          3              1
##   5: 1953-04-05 Korea Republic Hong Kong          0              4
##  ---                                                              
## 833: 2018-06-07 Korea Republic   Bolivia          0              0
## 834: 2018-06-11 Korea Republic   Senegal          0              2
## 835: 2018-06-18 Korea Republic    Sweden          0              1
## 836: 2018-06-23 Korea Republic    Mexico          1              2
## 837: 2018-06-27 Korea Republic   Germany          2              0
##          tournament
##   1:       Friendly
##   2:       Friendly
##   3:       Friendly
##   4:       Friendly
##   5:       Friendly
##  ---               
## 833:       Friendly
## 834:       Friendly
## 835: FIFA World Cup
## 836: FIFA World Cup
## 837: FIFA World Cup

여기서 주의해야 할 건 R에서는 A와 B가 같다고 할 때 'A=B'가 아니라 'A==B'처럼 표시한다는 점입니다. 위에서 보신 것처럼 A=B는 A라는 변수에 B라는 값을 담으라는 뜻이기에 그렇습니다.


조건 여러 개를 동시에 줄 수도 있습니다. 한번 역대 한일전 결과만 뽑아 볼까요? 한일전에서는 팀은 그대로고 상대팀(opponent)만 일본(Japan)으로 설명하면 됩니다.

head(sc[team=='Korea Republic' & opponent=='Japan', ])
##          date           team opponent team_score opponent_score
## 1: 1954-03-07 Korea Republic    Japan          5              1
## 2: 1954-03-14 Korea Republic    Japan          2              2
## 3: 1959-09-05 Korea Republic    Japan          0              0
## 4: 1959-09-06 Korea Republic    Japan          3              1
## 5: 1960-11-06 Korea Republic    Japan          2              1
## 6: 1961-06-11 Korea Republic    Japan          2              0
##                      tournament
## 1: FIFA World Cup qualification
## 2: FIFA World Cup qualification
## 3:           Merdeka Tournament
## 4:           Merdeka Tournament
## 5: FIFA World Cup qualification
## 6: FIFA World Cup qualification

R에서는 그리고(and)를 나타낼 때 '&' 기호를 씁니다. 혹은(or)은 '|'입니다. 만약 sc[team=='Korea Republic' | opponent=='Japan', ]라고 쓰면 팀이 한국이거나 상대팀이 일본인 모든 경기를 출력해 줄 겁니다.


열도 행과 마찬가지 방식으로 고를 수 있습니다. 상대팀이 세 번째 열에 있으니까 지금까지 한국과 A매치에서 맞붙은 나라만 뽑아 달라는 명령은 이렇게 쓸 수 있습니다.

sc[team=='Korea Republic', 3]
##       opponent
##   1:     China
##   2:   Vietnam
##   3:     Macau
##   4:    Taiwan
##   5: Hong Kong
##  ---          
## 833:   Bolivia
## 834:   Senegal
## 835:    Sweden
## 836:    Mexico
## 837:   Germany

열 이름을 쓰는 것도 가능합니다. 이렇게 써보겠습니다.

head(sc[team=='Korea Republic', opponent])
## [1] "China"     "Vietnam"   "Macau"     "Taiwan"    "Hong Kong" "Taiwan"

어? 이상합니다. 이건 열 이름을 그냥 그대로 쓰면 R 기본 데이터 형식인 벡터(vector)로 결과를 출력하기 때문입니다. 데이터 테이블 형태로 출력하려면 '.()'을 써야 합니다.

sc[team=='Korea Republic', .(opponent)]
##       opponent
##   1:     China
##   2:   Vietnam
##   3:     Macau
##   4:    Taiwan
##   5: Hong Kong
##  ---          
## 833:   Bolivia
## 834:   Senegal
## 835:    Sweden
## 836:    Mexico
## 837:   Germany

열을 출력할 때는 계산 결과를 포함할 수 있습니다. R에서는 평균을 구할 때 'mean()'이라는 함수를 씁니다. 예를 들어 팀 득점(team_score)을 평균내려면 mean(team_score)라고 쓰면 되는 것. 한국 평균 득점과 실점을 구하려면 어떻게 써야 할까요?

sc[team=='Korea Republic', .(평균득점=mean(team_score), 평균실점=mean(opponent_score))]
##    평균득점  평균실점
## 1: 1.761051 0.9127838

여기서 또 알 수 있는 건 쉼표로 구분하면 여러 열을 동시에 불러오거나 계산할 수 있다는 사실입니다. 더 나아가서 상대팀에 따른 평균 득·실점을 알고 싶다면? 네, 아직 한번도 쓰지 않은 by를 쓸 차례입니다.

sc[team=='Korea Republic', .(평균득점=mean(team_score), 평균실점=mean(opponent_score)), by=.(opponent)]
##              opponent 평균득점  평균실점
##   1:            China 1.300000 0.8333333
##   2:          Vietnam 2.666667 0.8333333
##   3:            Macau 4.500000 1.0000000
##   4:           Taiwan 2.777778 0.8888889
##   5:        Hong Kong 2.230769 0.8461538
##  ---                                    
## 113:             Mali 3.000000 1.0000000
## 114:           Russia 1.333333 2.3333333
## 115:        Venezuela 3.000000 1.0000000
## 116:          Moldova 1.000000 0.0000000
## 117: Northern Ireland 1.000000 2.0000000

이런 결과가 나오면 꼭 줄을 세우고 싶다는 생각이 드는 게 본능이죠? 만약 한국이 평균득점을 많이 올린 나라 순서로 줄을 세우고 싶을 때는 어떻게 하면 될까요?


물론 방법이 꼭 이것 하나뿐인 건 아니지만 체이닝(chaining)을 활용하시면 편합니다. 체이닝은 이름 그대로 [ ] 사슬로 코드를 연결하는 걸 뜻합니다. 이렇게 말입니다.

sc[team=='Korea Republic', .(평균득점=mean(team_score), 평균실점=mean(opponent_score)), by=.(opponent)][order(평균득점, decreasing = TRUE)]
##      opponent 평균득점 평균실점
##   1:     Guam      9.0      0.0
##   2:    Sudan      8.0      0.0
##   3:    Yemen      6.0      0.0
##   4: Mongolia      6.0      0.0
##   5:     Laos      5.6      0.0
##  ---                           
## 113:  Tunisia      0.0      0.5
## 114:    Chile      0.0      1.0
## 115:  Denmark      0.0      0.0
## 116:  Belarus      0.0      1.0
## 117:     Peru      0.0      0.0

일단 첫 번째 [ ]는 앞에서 본 그대로입니다. 여기에 '[order(평균득점,="" decreasing="TRUE)]'를 추가했습니다. 이건 먼저 자료 정리가 끝난 데이터 테이블에 뒷 부분을 다시 적용하라는 뜻입니다.


order()는 R에서 데이터를 정렬할 때 쓰는 함수입니다. 그래서 이 명령어는 평균득점 순서로 정렬하되 내림차순을 따르라('decreasing = TRUE')는 뜻이 됩니다. 혹시 여기서 재미있는 것 하나 발견 못하셨나요?


데이터 테이블 DT[행, 열, by] 순서로 쓴다고 했는데 [order(평균득점, decreasing = TRUE)]에는 쉼표(,)가 하나도 없습니다. 이건 data.table에서 행만 호출할 때는 쉼표가 필요없기 때문입니다. 저 명령어는 [order(평균득점, decreasing = TRUE), ]와 100% 똑같은 의미입니다.


같은 기능을 하는 코드라면 조금이라도 더 짧게 쓰는 게 효율적이기 때문에 여기서는 줄여쓴 겁니다. 그런 의미에서 저 코드는 이렇게 쓸 수도 있습니다. order() 함수에서는 변수 이름 앞에 마이너스(-)를 붙이면 그 변수를 기준으로 내림차순 정렬을 하라는 뜻입니다. 그래서 이렇게 써도 우리가 살펴본 것과 같은 결과를 출력합니다.

sc[team=='Korea Republic', .(평균득점=mean(team_score), 평균실점=mean(opponent_score)), by=.(opponent)][order(-평균득점)]

여기까지 잘 따라오셨을 줄로 믿고 이제 난도를 높이겠습니다. 다음 문제는 현재까지 A매치에서 가장 많이 이긴 10개 나라를 찾아보는 겁니다.



열을 추가할 때는 ':='

이걸 알아보려고 할 때 제일 큰 문제는 지금 우리가 보고 있는 데이터에는 경기 결과(승무패)가 없다는 점입니다. 대신 득점(team_score)과 실점(opponent_score)은 있습니다. 이걸 알면 승무패를 구하는 건 어렵지 않은 일입니다. 득점이 실점보다 크면 승이고, 똑같으면 무, 작으면 패니까요.


이렇게 상황에 따라 다른 값을 출력해야 할 때는 조건문이라는 걸 씁니다. 엑셀에서는 if() 함수가 이 구실을 합니다. R에도 if() 함수가 있지만 사용법이 조금 까다롭습니다. 만약 '결과'라는 변수에 승무패를 넣는다고 가정할 때 if() 함수를 쓰면 이렇게 나타내야 합니다.

결과 <- if(team_score>opponent_score){'승'} else{if(team_score==opponent_score){'무'} else{'패'}}

엑셀을 쓸 때하고 달리 중괄호{}와 else를 계속 붙여줘야 하는 것. 그래서 ifelse()라는 함수가 따로 있습니다. 이걸 쓰면 이렇게 됩니다.

결과 <- ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', '패'))

이걸 데이터 테이블에 적용하려면 이렇게 쓸 수 있습니다.

sc[, .(결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', '패')))]
##        결과
##     1:   무
##     2:   무
##     3:   승
##     4:   패
##     5:   승
##    ---     
## 79334:   무
## 79335:   승
## 79336:   무
## 79337:   승
## 79338:   패

문제는 이렇게 쓰면 결과를 출력하기는 하는데 따로 이 결과를 저장하지는 않는다는 점입니다. 'sc'라고 입력하시면 처음에 우리가 불러온 그대로 보일 겁니다. 이걸 데이터에 붙이려면 '='가 아니라 ':='를 쓰면 됩니다. 대신 .()는 지워주셔야 합니다.

sc[, 결과:=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', '패'))]

이번에는 계산 결과를 따로 열로 붙일 뿐 아무 결과도 출력하지 않습니다. 그래서 sc를 쳐서 확인하면 경기 결과가 들어간 걸 볼 수 있습니다.

sc
##              date     team opponent team_score opponent_score
##     1: 1872-11-30 Scotland  England          0              0
##     2: 1872-11-30  England Scotland          0              0
##     3: 1873-03-08  England Scotland          4              2
##     4: 1873-03-08 Scotland  England          2              4
##     5: 1874-03-07 Scotland  England          2              1
##    ---                                                       
## 79334: 2018-07-07   Russia  Croatia          2              2
## 79335: 2018-07-07  England   Sweden          2              0
## 79336: 2018-07-07  Croatia   Russia          2              2
## 79337: 2018-07-10   France  Belgium          1              0
## 79338: 2018-07-10  Belgium   France          0              1
##            tournament 결과
##     1:       Friendly   무
##     2:       Friendly   무
##     3:       Friendly   승
##     4:       Friendly   패
##     5:       Friendly   승
##    ---                    
## 79334: FIFA World Cup   무
## 79335: FIFA World Cup   승
## 79336: FIFA World Cup   무
## 79337: FIFA World Cup   승
## 79338: FIFA World Cup   패

우리 목표는 제일 많이 이긴 나라를 구하는 거니까 '승'이 많은 나라를 뽑으면 되겠죠? 그러면 이 경기 결과를 by로 묶으면 될 겁니다.

sc[, .(승=sum(결과=='승')), by=.(team)]
##                  team  승
##   1:         Scotland 361
##   2:          England 557
##   3:            Wales 197
##   4: Northern Ireland 158
##   5:              USA 281
##  ---                     
## 290:       Kárpátalja   5
## 291:           Barawa   2
## 292:         Cascadia   3
## 293:     Matabeleland   2
## 294:          Kabylia   1

나오기는 잘 나왔는데 승이 많은 순서대로 정렬을 하지 않았습니다. 어떻게 하면 되는지 아시죠?

sc[, .(승=sum(결과=='승')), by=.(team)][order(-승)][1:10]
##               team  승
##  1:         Brazil 610
##  2:        England 557
##  3:        Germany 546
##  4:      Argentina 513
##  5:         Sweden 490
##  6: Korea Republic 439
##  7:        Hungary 427
##  8:         Mexico 422
##  9:          Italy 412
## 10:         France 403

맨 끝에 붙은 [1:10]은 1부터 10까지 출력하라는 뜻입니다. 콘솔에 그냥 1:51이라고 입력하시면 '[1] 1 2 3 4 5'가 뜹니다.


네, 그렇습니다. 한국은 FIFA 역사상 A 매치에서 여섯 번째로 많이 이긴 나라입니다. (예전에 다섯 번째로 많이 이겼다고 기사를 쓴 적이 있는데 당시 데이터가 잘못된 모양입니다.)


위에서는 := 사용법을 설명드리려고 이렇게 썼지만 물론 결과 열을 꼭 저장해야 하는 건 아닙니다. 아래처럼 입력하시면 결과 열이 사라집니다.  (열을 어떻게 삭제하는지도 아셨겠죠?)

sc[, 결과:=NULL]

그리고 나서 아래처럼 입력하시면 승은 물론 '무'와 '패'까지 같이 보입니다.

sc[, .(team, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][, .(승=sum(결과=='승'), 무=sum(결과=='무'), 패=sum(결과=='패')), by=.(team)][order(-승)][1:10]
##               team  승  무  패
##  1:         Brazil 610 192 155
##  2:        England 557 239 186
##  3:        Germany 546 191 196
##  4:      Argentina 513 238 206
##  5:         Sweden 490 219 289
##  6: Korea Republic 439 214 184
##  7:        Hungary 427 201 284
##  8:         Mexico 422 200 219
##  9:          Italy 412 221 152
## 10:         France 403 175 241

이렇게 한줄에 다 쓰면 코드가 너무 길어 직관적으로 이해하기가 쉽지 않기 때문에 위에서는 단계를 나눈 것뿐입니다. 물론 계산 결과를 열로 따로 빼놓는 게 다음 작업에 도움이 되는 경우도 얼마든지 있을 수 있고 말입니다.


기왕 여기까지 구했으니 잠깐 쉬어가는 의미로 한일전 결과만 뽑아 볼까요? 위에서 살펴본 것처럼 한일전 결과를 알고 싶을 때는 team과 opponent만 잘 지정하면 됩니다.

sc[team=='Korea Republic' & opponent =='Japan', .(team, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][, .(승=sum(결과=='승'), 무=sum(결과=='무'), 패=sum(결과=='패')), by=team]
##              team 승 무 패
## 1: Korea Republic 31 21 11

역대 A매치에서는 한국이 31승 21승 11패로 일본에 앞서 있다는 사실을 이 코드를 가지고 확인할 수 있습니다.


중국은 어떨까요? 중국 축구는 유독 한국에 약해서 공한증(恐韓症)이라는 표현이 있(었)을 정도입니다. 한중전 결과를 알고 싶을 때는 위에 있는 코드에서 opponent만 중국(China)으로 바꾸면 그만입니다.

sc[team=='Korea Republic' & opponent =='China', .(team, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][, .(승=sum(결과=='승'), 무=sum(결과=='무'), 패=sum(결과=='패')), by=team]
##              team 승 무 패
## 1: Korea Republic 15 12  3

한국이 중국에 패한 세 경기만 따로 빼고 싶을 때는 어떻게 할까요?

sc[team=='Korea Republic' & opponent =='China', .(team, date, tournament, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][결과=='패']
##              team       date                   tournament 결과
## 1: Korea Republic 1949-01-02                     Friendly   패
## 2: Korea Republic 2010-02-10            EAFF Championship   패
## 3: Korea Republic 2017-03-23 FIFA World Cup qualification   패

이해하시겠죠? 이번에는 date와 tournament 열을 포함해서 언제 어떤 대회에서 패했는지도 알아봤습니다. 중국 축구팬 관점에서는 1949년에 이긴 다음 2010년이 되어서야 다시 한국전 승리를 맛봤으니 공한증이라는 표현을 쓸 만도 합니다.



키 들어 갑니다.

지금까지는 모든 열을 똑같이 대했습니다. 이번 포스트에서 제일 중요한 열은 사실 'team'이었는데 말입니다. data.table은 이럴 때 'setkey()' 함수를 써서 team을 키(key) 열로 지정할 수 있도록 하고 있습니다.

setkey(sc, team)

이렇게 키 열을 지정하고 나면 data.table은 이 열에 따라 미리 자료를 정렬합니다. 자연스럽게 데이터 정렬에 들어가는 시간이 줄어들기 때문에 데이터 처리 속도도 더 빨라집니다.


이번에는 나라 이름을 ABC 순서로 정렬했을 겁니다. 실제로 데이터를 확인해 보면 압하지야가 제일 먼저 나오는 걸 확인할 수 있습니다.

head(sc[, 1:3])
##          date     team       opponent
## 1: 2014-06-01 Abkhazia      Occitania
## 2: 2014-06-02 Abkhazia          Sápmi
## 3: 2014-06-04 Abkhazia  South Ossetia
## 4: 2014-06-05 Abkhazia        Padania
## 5: 2014-06-07 Abkhazia      Occitania
## 6: 2016-05-29 Abkhazia Chagos Islands

또 이렇게 키 열 설정이 끝나면 이 열을 검색할 때는 == 기호를 쓰지 않아도 됩니다. 예컨대 지금까지는 한국 결과만 보고 싶을 때 sc[team=='Korea Republic']이라고 썼지만 이제는 sc['Korea Republic']이라고 써도 같은 결과를 얻을 수 있습니다.

head(sc['Korea Republic', 1:3])
##          date           team  opponent
## 1: 1949-01-02 Korea Republic     China
## 2: 1949-01-16 Korea Republic   Vietnam
## 3: 1949-01-25 Korea Republic     Macau
## 4: 1950-04-16 Korea Republic    Taiwan
## 5: 1953-04-05 Korea Republic Hong Kong
## 6: 1953-04-08 Korea Republic    Taiwan

setkey()에는 여러 열을 지정할 수도 있습니다. 정렬 기준이 꼭 하나일 필요는 없으니까요. 현재 자료는 나라 이름이 첫 번째 기준이고, 경기 날짜가 두 번째 기준이라고 할 수 있습니다. (원래 데이터가 날짜 기준 정렬 상태였기 때문에 그렇습니다.)


아래처럼 입력하면 나라 이름 다음으로 상대팀이 기준이 되기 때문에 데이터 순서도 바뀝니다.

setkey(sc, team, opponent)
head(sc['Korea Republic', 1:3])
##          date           team  opponent
## 1: 1983-09-20 Korea Republic   Algeria
## 2: 1985-12-13 Korea Republic   Algeria
## 3: 1987-08-07 Korea Republic   Algeria
## 4: 2014-06-22 Korea Republic   Algeria
## 5: 2006-03-01 Korea Republic    Angola
## 6: 1986-06-02 Korea Republic Argentina

이번 포스트에서 우리가 쓰고 있는 데이터는 크기가 별로 크지 않아서 속도 변화를 체감하기는 어렵습니다. 데이터가 크면 클수록 이 차이가 커지니까 크고 무거운 데이터를 처리하실 일이 있을 때는 setkey() 함수를 사용하시는 걸 잊지 마세요.



롤링 조인(Rolling Join)!

데이터를 처리하다 보면 여러 데이터를 하나로 합치고 싶을 때가 있습니다. data.table에서는 그냥 [ ] 안에 데이터를 넣는 것만으로 데이터를 합칠 수 있도록 하고 있습니다.


만약 어떤 쇼핑몰 업체에서 장바구니에 넣은 날짜와 실제로 구매한 날짜를 따로 따로 관리한다고 가정해 보겠습니다.

장바구니 <- data.table(장바구니날짜=c('2018-04-01', '2018-06-02', '2018-06-30', '2018-07-15', '2018-08-01'),
사람=c('a', 'b', 'c', 'd', 'e'))
결제 <- data.table(결제날짜=c('2018-05-01', '2018-06-01', '2018-07-01', '2018-08-01', '2018-09-01'),
사람=c('a', 'b', 'b', 'c', 'd'))
장바구니
##    장바구니날짜 사람	
## 1:   2018-04-01    a
## 2:   2018-06-02    b
## 3:   2018-06-30    c
## 4:   2018-07-15    d
## 5:   2018-08-01    e
결제
##      결제날짜 사람
## 1: 2018-05-01    a
## 2: 2018-06-01    b
## 3: 2018-07-01    b
## 4: 2018-08-01    c
## 5: 2018-09-01    d

이 코드에서 data.table은 다음에 나오는 데이터를 데이터 테이블 형식으로 만들라는 뜻이고, c()는 여러 값을 하나로 묶으라(concatenate)는 뜻입니다. 


그러면 두 데이터를 묶을 때는 어떻게 해야 할까요? 이때 핵심은 사람을 기준으로 합치라고 R에게 알려주는 것. 그럴 때도 setkey() 함수를 쓰면 됩니다.

setkey(장바구니, 사람)
setkey(결제, 사람)
장바구니[결제]
##    장바구니날짜 사람   결제날짜
## 1:   2018-04-01    a 2018-05-01
## 2:   2018-06-02    b 2018-06-01
## 3:   2018-06-02    b 2018-07-01
## 4:   2018-06-30    c 2018-08-01
## 5:   2018-07-15    d 2018-09-01

그냥 순전히 조금 더 보기 좋으라고 열 순서를 바꿔보겠습니다.

장바구니[결제][, c(2, 1, 3)]
##    사람 장바구니날짜   결제날짜
## 1:    a   2018-04-01 2018-05-01
## 2:    b   2018-06-02 2018-06-01
## 3:    b   2018-06-02 2018-07-01
## 4:    c   2018-06-30 2018-08-01
## 5:    d   2018-07-15 2018-09-01

일단 여기서 확인할 수 있는 건 사람 가운데 e가 사라졌다는 점입니다. 왜 그럴까요? 데이터를 자세히 보시면 e는 장바구니에 물건을 넣기는 했지만 아직 결제는 하지 않았습니다. 그래서 결제날짜를 기준으로 데이터를 합쳤을 때는 e가 사라지게 됩니다. 


거꾸로 장바구니 날짜를 기준으로 하면 e가 살아남습니다. 데이터를 합칠 때 무엇을 기준으로 삼을 것인지에 따라 조건을 달리하면 되는 겁니다. (아래에서 <NA>는 'Not Applicable' 또는 'Not Available'을 줄인 말로 해당 값이 아예 없다는 뜻입니다.)

결제[장바구니][, c(2, 3, 1)]
##    사람 장바구니날짜   결제날짜
## 1:    a   2018-04-01 2018-05-01
## 2:    b   2018-06-02 2018-06-01
## 3:    b   2018-06-02 2018-07-01
## 4:    c   2018-06-30 2018-08-01
## 5:    d   2018-07-15 2018-09-01
## 6:    e   2018-08-01       <NA>

이 결과가 옳을까요? 두 번째, 세 번째 줄을 보면 b는 6월 2일에 장바구니에 넣은 물건을 6월 1일과 7월 1일에 결제했다고 나옵니다. 6월 2일에 장바구니에 담은 걸 6월 1일에 결제할 수는 없으니 6월 1일에는 그냥 물건을 사고, 다음날 장바구니에 넣은 걸 7월 1일에 결제했다고 보는 게 우리 상식에 부합합니다.


data.table은 이럴 때 도움이 될 수 있도록 롤링 조인(rolling join)이라는 기능을 포함하고 있습니다. 이 기능을 사용하려면 데이터를 합칠 때 'roll=T(RUE)'라고 입력하면 됩니다. 그렇다고 현재 코드에 곧바로 roll=T만 입력하면 엉뚱한 결과가 나옵니다.

장바구니[결제, roll=T][, c(2, 3, 1)]
##    사람   결제날짜 장바구니날짜
## 1:    a 2018-05-01   2018-04-01
## 2:    b 2018-06-01   2018-06-02
## 3:    b 2018-07-01   2018-06-02
## 4:    c 2018-08-01   2018-06-30
## 5:    d 2018-09-01   2018-07-15


아직eh R가 날짜도 따져야 한다는 걸 모르기 때문입니다. 그러면 어떻게 해야 할까요? 네, seykey()를 쓰면 됩니다.

setkey(장바구니, 사람, 장바구니날짜)
setkey(결제, 사람, 결제날짜)
장바구니[결제, roll=T]
##    장바구니날짜 사람
## 1:   2018-05-01    a
## 2:   2018-06-01    b
## 3:   2018-07-01    b
## 4:   2018-08-01    c
## 5:   2018-09-01    d

아직도 이상합니다. 왜 그럴까요? 열 이름이 장바구니날짜, 결제날짜로 서로 달라서 R가 둘을 합쳐야 한다는 생각을 못하기 때문입니다. 각 데이터에 '날짜'라는 이름으로 열을 만들어 두 데이터를 합치는 기준으로 삼아 보겠습니다.

장바구니[, 날짜:=장바구니날짜]
결제[, 날짜:=결제날짜]

setkey()를 한 번 더 써야 한다는 것 알고 계시죠?

setkey(장바구니, 사람, 날짜)
setkey(결제, 사람, 날짜)

이제 결과를 확인하면 이렇게 됩니다.

장바구니[결제, roll=T]
##    장바구니날짜 사람       날짜   결제날짜
## 1:   2018-04-01    a 2018-05-01 2018-05-01
## 2:         <NA>    b 2018-06-01 2018-06-01
## 3:   2018-06-02    b 2018-07-01 2018-07-01
## 4:   2018-06-30    c 2018-08-01 2018-08-01
## 5:   2018-07-15    d 2018-09-01 2018-09-01

6월 1일에 b가 결제한 건 장바구니를 이용하지 않았다는 걸 잘 잡아냈습니다.


이렇게 NA 데이터를 지우고 싶을 때는 na.omit()라는 함수를 쓰시면 됩니다. (영어 낱말 'omit'이 '생략한다'는 뜻입니다.) 이렇게 쓰면 저 두 번째 행을 지울 수 있습니다. (열 순서도 바꿨습니다.)

na.omit(장바구니[결제, roll=T])[, c(2, 1, 3)]
##    사람 장바구니날짜       날짜
## 1:    a   2018-04-01 2018-05-01
## 2:    b   2018-06-02 2018-07-01
## 3:    c   2018-06-30 2018-08-01
## 4:    d   2018-07-15 2018-09-01

na.omit()는 NA가 들어 있는 모든 행을 지웁니다. 특정 열에서 NA가 있을 때만 지우고 싶을 때는 'cols=열 이름'을 쓰시면 됩니다. 예컨대 na.omit(장바구니[결제, roll=T])[, c(2, 1, 3)], cols='장바구니날짜')라고 쓰시면 위에 있는 코드와 똑같이 작동합니다.



FIFA 랭킹을 붙여보자

이렇게 길게 롤링 조인에 대해 설명한 건 A 매치 데이터에 FIFA 랭킹을 붙이고 싶었기 때문입니다. 각 국가 FIFA 랭킹은 한 번 발표하고 나면 그 다음 발표가 있을 때까지 변화가 없습니다. 장바구니에 넣어두면 (특정한 삭제 규칙이 없는 한) 결제하기 전까지 계속 장바구니에 물건이 들어있는 것과 마찬가지입니다.


FIFA 랭킹을 붙이려면 일단 데이터를 불러와야겠죠? 아래 있는 CSV 파일은 FIFA에서 랭킹 제도를 처음 도입한 1993년 8월 8일부터 러시아 월드컵 이전인 올해 6월 7일까지 각국 FIFA 랭킹을 담고 있습니다.


fifa_ranking.csv


어떻게 읽는지 다들 아시죠? 우리는 rk라는 변수에 이 데이터를 넣도록 하겠습니다.

rk <- fread('fifa_ranking.csv')

A매치 데이터는 1872년 11월 30일 경기 결과부터 들어 있는데 FIFA 랭킹은 1993년 8월 8일이 처음입니다. 필요 없는 데이터를 빼고 시작하는 게 낫겠죠? A매치 데이터에서 날짜(date) 열이 1993년 8월 8일 이후인 경기 결과만 남겨놓겠습니다.

sc <- sc[date>=1993-08-08]

rk에는 날짜를 나타내는 열 이름이 date가 아니라 rank_date입니다. 두 데이터에 각각 코드(code)라는 열을 만들어 코드로 쓰겠습니다.

sc[, code:=date]
rk[, code:=rank_date]

키 열도 지정해야죠? A 매치 데이터에는 나라 이름이 team 열에, FIFA 랭킹 데이터에는 country_full 열에 들어 있습니다.

setkey(sc, team, code)
setkey(rk, country_full, code)

물론 이 때도 열 이름이 다르기 때문에 별도로 열을 만드셔도 됩니다. 여기서 그냥 놔둔 건 이 두 열을 합쳐도 별 이상이 없기 때문입니다.


일단 두 데이터를 합치는 건 'rk[sc,="" roll="T]'라고 입력하면 될 겁니다. 그런데 이 결과 중에는 (새로 FIFA 회원국이 됐다든지 하는 이유로) 랭킹이 없는 팀도 있습니다. 그래서 na.omit() 함수를 쓰고 필요한 열만 따로 빼겠습니다.

na.omit(rk[sc, roll=T][, .(date, country_full, rank, opponent, team_score, opponent_score)])
##              date country_full rank     opponent team_score opponent_score
##     1: 2003-03-16  Afghanistan  203   Kyrgyzstan          2              1
##     2: 2003-03-18  Afghanistan  203        Nepal          0              4
##     3: 2003-11-19  Afghanistan  198 Turkmenistan          0             11
##     4: 2003-11-23  Afghanistan  198 Turkmenistan          0              2
##     5: 2005-11-09  Afghanistan  199   Tajikistan          0              4
##    ---                                                                    
## 38977: 2018-03-21     Zimbabwe  108       Zambia          2              2
## 38978: 2018-03-24     Zimbabwe  108       Angola          2              2
## 38979: 2018-04-18     Zimbabwe  110     Botswana          0              1
## 38980: 2018-06-03     Zimbabwe  113     Botswana          1              1
## 38981: 2018-06-06     Zimbabwe  113      Lesotho          0              0


이렇게 쓰고 나니 상대 팀 FIFA 랭킹도 같이 있으면 좋겠다는 생각이 듭니다. 일단 이 결과를 rk_sc에 넣고 고민해보겠습니다. (이번에는 tournament 열을 포함해 어떤 대회인지 남겼습니다.)

rk_sc <- na.omit(rk[sc, roll=T][, .(date, country_full, rank, opponent, team_score, opponent_score, tournament, code)])

어려울 거 없습니다. 위에서 FIFA 랭킹을 붙일 때 한 걸 한 번 더 반복하면 됩니다. 이전에는 team 열을 country_full에 대응했지만 이번에는 opponent 열이 들어간다는 것만 차이점입니다. 자료를 합쳤을 때 헷갈리지 않도록 열 이름을 바꾸고 시작해 보겠습니다.

names(rk_sc)[2] <- 'team'

이렇게 쓰면 rk_sc에 있는 두 번째 열 이름을 'team'으로 바꾸라는 뜻입니다. 세 번째 열 rank를 team_rank로 바꾸려면 어떻게 해야 할까요?

names(rk_sc)[3] <- 'team_rank'

이름이 잘 바뀌었는지 확인해 보시고 싶을 때는 '<-' 앞 부분만 치시면 그만입니다.

c(names(rk_sc)[2], names(rk_sc)[3])
## [1] "team"      "team_rank"

이렇게 정리를 하셨으면 키 열을 설정해야겠죠?

setkey(rk_sc, opponent, code)

FIFA 랭킹 데이터는 위에서 설정한 그대로 둬도 무방하기 때문에 그냥 넘어갑니다. 이제 합치기만 하면 됩니다. 이번에도 필요할 열만 원하는 순서대로 뽑겠습니다. 그리고 다시 이 결과를 rk_sc에 넣는 것까지.

rk_sc <- na.omit(rk[rk_sc, roll=T][, .(date, team, team_rank, country_full, rank, team_score, opponent_score, tournament)])

여기서 rank 열은 상대 팀 FIFA 랭킹이니까 이름을 opponent_rank로 바꾸겠습니다. country_full도 원래대로 'opponent'로 환원.

names(rk_sc)[5] <- 'opponent_rank'
names(rk_sc)[4] <- 'opponent'

이 정도면 이제 결과를 확인해 봐도 되겠죠?

head(rk_sc[, 1:5])
##          date         team team_rank    opponent opponent_rank
## 1: 2003-03-18        Nepal       163 Afghanistan           203
## 2: 2003-11-19 Turkmenistan       122 Afghanistan           198
## 3: 2003-11-23 Turkmenistan       122 Afghanistan           198
## 4: 2005-11-09   Tajikistan       143 Afghanistan           199
## 5: 2005-12-07     Maldives       147 Afghanistan           198
## 6: 2005-12-09     Pakistan       168 Afghanistan           198


이제 A 매치에서 맞대결 벌인 두 팀 랭킹을 모두 알게 됐으니 재미삼아 몇 가지 해보겠습니다.


FIFA 랭킹 1위에 이름을 올리고 있던 나라는 어떤 성적을 올렸을까요? 위에서 썼던 코드를 다시 활용하면 된다는 거 아직 기억하시죠?

rk_sc[team_rank==1, .(결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][, .(승=sum(결과=='승'), 무=sum(결과=='무'), 패=sum(결과=='패'))]
##     승 무 패
## 1: 217 63 54


한국이 FIFA 랭킹 1위 팀을 상대로 어떤 성적을 기록했는지 보려면?

rk_sc[team=='Korea Republic' & opponent_rank==1, .(결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][, .(승=sum(결과=='승'), 무=sum(결과=='무'), 패=sum(결과=='패'))]
##     승 무 패
## 1:  2  0  7


언제 어떤 경기에서 누구를 이겼을까요?

rk_sc[team=='Korea Republic' & opponent_rank==1, .(date, tournament, opponent, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][결과=='승']
##          date     tournament opponent 결과
## 1: 1999-03-28       Friendly   Brazil   승
## 2: 2018-06-27 FIFA World Cup  Germany   승


2018 러시아 월드컵 때 한국은 FIFA 랭킹 57위였습니다. 이보다 랭킹이 낮은 팀이 1위 팀을 이긴 건 몇 번일까요?

rk_sc[opponent_rank==1, .(team, date, team_rank, tournament, opponent, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][결과=='승'][team_rank>=57][order(-team_rank)]
##              team       date team_rank                   tournament
## 1:        Bolivia 2017-03-28        97 FIFA World Cup qualification
## 2:         Poland 2014-10-11        70      UEFA Euro qualification
## 3:      Australia 2001-06-01        68           Confederations Cup
## 4:   South Africa 2013-11-19        61                     Friendly
## 5: Korea Republic 2018-06-27        57               FIFA World Cup
##     opponent 결과
## 1: Argentina   승
## 2:   Germany   승
## 3:    France   승
## 4:     Spain   승
## 5:   Germany   승


네, 그렇습니다. 한국은 지금까지 FIFA 월드컵 본선에서 FIFA 랭킹 1위를 꺾은 가장 랭킹이 낮은 나라입니다.


그러면 가장 FIFA 랭킹 차이가 큰 팀을 꺾어 본 나라는 어디일까요?

rk_sc[, .(date, team, team_rank, opponent, opponent_rank, rank_diff=team_rank-opponent_rank, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][결과=='승'][team_rank>=57][order(-rank_diff)][1:5]
##          date          team team_rank opponent opponent_rank rank_diff
## 1: 2014-11-14 Faroe Islands       187   Greece            18       169
## 2: 2017-06-09       Andorra       186  Hungary            33       153
## 3: 2008-03-26    Montenegro       175   Norway            27       148
## 4: 2010-10-10         Niger       154    Egypt             9       145
## 5: 2017-06-06         Malta       182  Ukraine            37       145
##    결과
## 1:   승
## 2:   승
## 3:   승
## 4:   승
## 5:   승


페로 제도가 FIFA 랭킹 187위였던 2014년 11월 14일 경기에서 당시 18위 그리스를 이긴 게 지금까지 최고 기록입니다.


이게 무슨 경기인지만 알고 싶을 때는 어떻게 하면 될까요?

rk_sc[team=='Faroe Islands' & date=='2014-11-14', .(tournament)]
##                 tournament
## 1: UEFA Euro qualification

정답은 유럽축구연맹(UEFA) 유로 예선이었습니다.



마무리는 dplyr?


tidyverse 생태계에 속한 dplyr라는 패키지를 아시는 분이라면 data.table이 이 패키지와 닮은 구석이 많다고 생각하실지도 모르겠습니다. 한번 dplyr로 같은 작업이 가능한지 알아보겠습니다.


(여기서는 두 패키지가 닮은 점만 말씀드리는 거라 dplyr에 대해서는 자세히 설명드리지 않겠습니다. 이 패키지가 조금 더 궁금하신 분은 '최대한 친절하게 쓴 R로 데이터 뽑아내기(feat. dplyr)' 포스트를 참고하시면 도움이 될지 모릅니다.)


dplyr도 패키지니까 역시 깔고 로드.

install.packages('dplyr')
library('dplyr')

FIFA 랭킹 1위 팀을 꺾은 랭킹이 제일 낮은 나라 10곳을 뽑는 코드는 이렇게 쓸 수 있습니다.

rk_sc %>%
filter(opponent_rank==1) %>%
mutate(결과=ifelse(team_score>opponent_score, '승',
ifelse(team_score==opponent_score, '무', '패'))) %>%
filter(결과=='승') %>%
arrange(desc(team_rank)) %>%
select(date, team, team_rank, opponent) %>%
head(10)

똑같은 결과를 출력하는 data.table 코드는 이렇습니다.

rk_sc[opponent_rank==1, .(date, team, team_rank, opponent, 결과=ifelse(team_score>opponent_score, '승', ifelse(team_score==opponent_score, '무', "패")))][결과=='승'][order(-team_rank)][1:10]

개인 취향에 따라 어떤 분은 dplyr 코드가 더 깔끔하고 직관적으로 보일 수도 있습니다. 그리고 이 정도 크기 자료에서는 dplyr가 data.table보다 속도가 아주 늦다고 하기도 어렵습니다. 실제로 system.time()으로 측청해 보면 제 컴퓨터 기준으로 두 방식 모두 0.01초밖에 걸리지 않습니다. 그러니까 취향과 상황에 따라 어떤 패키지가 더 나은지 달라질 수 있다는 뜻입니다.


저는 개인적으로 롤링 조인이 필요할 때만 data.table을 활용하는 편입니다. (적어도 제가 아는 한 현재까지는) dplyr에서는 이런 기능을 구현할 수 없기 때문입니다. data.table만 할 수 있는 기능이 있다면 그건 data.table을 활용하고 (제 손에 더 익은) dplyr이 편할 때는 dplyr를 쓰면 그만이니까요.


아, 이번 포스트에서 data.table에 들어 있는 모든 기능을 다 다룬 건 아닙니다. 한 가지 마지막으로 언급하자면 마지막 행은 '.N' 구할 수 있습니다. 예를 들어 아래처럼 치면 rk_sc에서 첫 번째 행과 마지막 행을 출력합니다.

rk_sc[c(1, .N)]
##          date    team team_rank    opponent opponent_rank team_score
## 1: 2003-03-18   Nepal       163 Afghanistan           203          4
## 2: 2018-06-06 Lesotho       150    Zimbabwe           113          0
##    opponent_score                  tournament
## 1:              0 AFC Asian Cup qualification
## 2:              0                  COSAFA Cup


그밖에 특정한 열과 행을 선택하는 .SD와 .SDcols도 있는데 이를 제대로 활용하시려면 'lapply()' 함수 사용이 필요해 초보자용인 이 포스트에서는 다루지 않았습니다. 이 포스트에서 빼먹은 내용이거나 추가로 이해가 필요하신 분은 아래 커닝 페이퍼가 도움이 될 겁니다. (내려받으시면 큰 크기로 확인이 가능합니다.)



이 포스트에서 잘 이해가 가지 않거나 잘못된 내용이 있다면 댓글 등으로 알려주세요. 같이 공부하면서 바로잡도록 하겠습니다. 긴 글 읽으시느라 고생하셨습니다.


댓글, 0

account_circle
vpn_key
web

security

mode_edit
Kidult | 카테고리 다른 글 더 보기