요즘 데이터 좀 만진다고 하는 사람들 사이에서 R 이야기가 참 많이 들립니다. R로 만든 시각화 자료를 발견하는 것도 아주 흔한 일이 되고 있습니다. R란 무엇이고, 어떻게 활용할 수 있을까요?
(알파벳 'R'는 10명이면 10명 모두 /알/이라고 발음하기 쉽지만 국립국어원 외국어 표기법에 따른 표기는 '아르'입니다. 따라서 R를 이름으로 쓰는 이 프로그램도 마찬가지로 읽어야 합니다. 이 포스트에서 '은/는', '을/를' 등이 어색해 보일 때가 있는 건 아르 기준이기 때문입니다.)
R 공식 홈페이지는 '도대체 R가 무엇이냐'고 묻는 이들에게 "R는 통계 계산과 그래픽에 활용하는 무료 소프트웨어 환경(R is a free software environment for statistical computing and graphics)"이라는 정의를 내놓고 있습니다. 이것만으로 뭐가 뭔지 잘 모르겠으니 일단 무작정 내려 받아 보겠습니다.
여기 다운로드 링크를 클릭하시면 마이크로소프트(MS) 윈도용 최신 버전(이 글을 쓰는 2017년 3월 12일 기준 3.3.3) 설치 파일을 내려받으실 수 있습니다. 그 다음에 보통 프로그램을 설치하실 때처럼 진행하시면 됩니다.
(혹시 자기 컴퓨터가 운영체제·OS가 32비트인지 64비트인지 모르시겠다면 이 MS 홈페이지 링크를 참조하시면 도움이 될 겁니다.)
설치가 끝나고 R를 실행하면 아래 같은 화면이 뜹니다. R도 '프로그래밍 언어'니까 'Hello, World 코드'부터 입력했습니다. Hello, World 코드는 문자 그대로 "Hello, World"를 출력하는 코드로 프로그래밍 언어를 배울 때 제일 먼저 실습하는 과제로 유명합니다. 당연히 이 글을 읽는 분들도 똑같이 입력하면 같은 결과를 얻으실 수 있습니다.
일단 R를 설치하셔야 코드를 따라서 붙여 넣을 수 있기에 간단히 소개했습니다만, 이 글은 R 기초를 알려드리는 게 아니라 R로 분석하는 과정을 보여드리는 게 목적입니다. 기초 공부가 하시고 싶은 분은 더북(TheBook)에서 인터넷에 무료로 공개 중인 'R을 이용한 데이터 처리 & 분석 실무' 등을 참고하시면 될 듯합니다.
R가 이렇게 인기를 끌게 된 제일 큰 이유를 저는 '패키지(packages)'라고 생각합니다. 패키지는 이름 그대로 특정한 기능을 담은 함수 덩어리입니다. 이 글을 쓰고 있는 현재 CRAN(The Comprehensive R Archive Network)에 올라와 있는 패키지는 1만252개입니다. 오히려 선택지가 너무 많아서 어떤 걸 써야 할지 모르는 수준입니다.
또 (이건 어느 언어나 마찬가지지만) 소스를 그대로 붙여 넣기만 하면 똑같은 결과가 나온다는 점도 R 인기를 끌어올렸습니다. 특히 R는 전문 개발자보다 학자 같은 아마추어가 코딩(프로그래밍)을 하는 일이 많기 때문에 '소스 최적화'가 덜 된 상태일 때가 많습니다. 이 때문에 역설적으로 비전문가가 코드를 뜯어 보기 편한 느낌을 줍니다.
자, 그러면 한번 실제로 패키지를 끌어 모아서 R 코딩을 해보겠습니다. 목표는 이 링크와 이 링크를 참조해 아래 지도를 그리는 겁니다.
이 지도는 지난해(2016년) 전 세계에서 승객이 가장 많았던 공항 50군데를 나타내고 있습니다. 다른 그래픽 소프트웨어를 하나도 쓰지 않고 R만으로 이런 지도를 어떻게 그리는 걸까요? 일단 정답은 이겁니다.
2018년 10월 현재 구글 지도 정책 변경으로 아래 코드를 작동하시려면 구글 API 키가 필요합니다. ggmap '404 forbidden' 에러 해결하기 포스트를 따라 하시면 이 키를 발급받는 데 도움이 될 겁니다.
#install.packages('rvest')
#install.packages('ggplot2')
#install.packages('ggmap')
library('rvest')
library('ggplot2')
library('ggmap')
html.airports <- read_html("https://en.wikipedia.org/wiki/List_of_busiest_airports_by_passenger_traffic")
df <- html_table(html_nodes(html.airports, "table")[[1]], fill = TRUE)
colnames(df)[6] <- 'total'
df$total <- gsub('\\[[0-9]+\\]', '', df$total)
df$total <- gsub(',', '', df$total)
df$total <- as.numeric(df$total)
gc <- geocode(df$Airport)
df <- cbind(df, gc)
world <- map_data('world')
ggplot(df, aes(x=lon, y=lat)) +
geom_polygon(data=world, aes(x=long, y=lat, group=group), fill='grey75', color='grey70') +
geom_point(color='dark red', alpha=.25, aes(size=total)) +
geom_point(color='dark red', alpha=.75, shape=1, aes(size=total)) +
theme(legend.position='none')
간단하죠? 이 16줄에 인터넷에서 정보를 가져오는 것부터 데이터를 정리해 그림으로 나타내는 것까지 모두 들어 있습니다. 이걸 그래도 R에 붙여 넣으시면 저 그림이 짠 하고 나오는 겁니다. 어떻게 한 건지 한번 뜯어 볼까요?
인터넷에 있는 데이터를 가져오는 가장 기본적인 방식은 역시 '복붙(복사하기+붙여넣기)'일 겁니다. 만약 데이터가 하이퍼텍스트 마크업 언어(HTML)로 된 웹 페이지 안에 표(table)로 돼 있다면 이걸 복사한 다음 MS 엑셀 등에 붙여 넣는 게 자료 수집 첫 단계일 겁니다.
R에서는 그럴 필요가 없습니다. 'rvest'라는 패키지를 써서 인터넷에 있는 자료를 자동으로 긁어올 수 있기 때문입니다. 이런 작업을 흔히 '웹 크롤링(web crawling)' 또는 '웹 스크래핑(web scrapping)'이라고 표현합니다. 그러면 R에 rvest를 설치해 볼까요?
install.packages('rvest')
패키지를 설치할 때는 "install.pacakges('패키지 이름')" 형태로 명령문을 입력하면 됩니다. 단, 패키지를 내려받았다고 이 패키지를 곧바로 쓸 수 있는 건 아닙니다. 패키지를 쓰려면 따로 불러주는 과정이 필요합니다. 이렇게 말입니다.
library('rvest')
이 패키지는 웹 크롤링을 한다고 했으니까 실제로 위키피디아에 있는 데이터를 긁어보겠습니다.
html.airports <- read_html('https://en.wikipedia.org/wiki/List_of_busiest_airports_by_passenger_traffic')
R에서 '<-' 또는 '='는 왼쪽 변수에 오른쪽 내용을 넣겠다는 뜻입니다. 예를 들어 'a <- 1+2'라고 입력하고 나서 그다음에 a를 입력하면 '3'이라고 나오는 방식입니다.
그러면 위에 쓴 건 무슨 뜻일까요? html.airports라는 변수에 위키피디아 링크를 읽어서 집어 넣으라는 의미입니다. 이런 명령을 쓸 수 있는 건 rvest 덕입니다. 만약 rvest 없이 read_html을 쳤다면 "에러: 객체 'read_html'을 찾을 수 없습니다"하고 나왔을 겁니다.
마찬가지로 rvest에 있는 함수를 아래처럼 조합하면 우리가 원하는 자료만 가지고 올 수 있습니다. 우리가 자료를 가져 오려는 페이지 소스를 확인해 보면(구글 '크롬'에서 F12를 누르면 소스를 확인할 수 있습니다) 첫 번째 테이블(table) 그러니까 HTML 코드에 처음 나오는 테이블에 관련 정보가 들어 있습니다.
rvest에는 테이블을 그대로 가져오는 함수 html_table이 있으니 이렇게 입력해 테이블을 df 함수에 담습니다. (df는 Data Frame을 줄인 말입니다.)
df <- html_table(html_nodes(html.airports, "table")[[1]], fill = TRUE)
데이터가 잘 들어왔는지 확인할 때는 head를 활용하면 됩니다. head는 첫 여섯 줄만 보여줍니다. (이걸 굳이 소스 코드에 남길 필요는 없으니 정리 과정에서는 뺐습니다.)
head(df)
Rank Airport Location Country Code\n(IATA/ICAO) Total\npassengers Rank\nChange %\nChange 1 1 Hartsfield<U+2013>Jackson Atlanta International Airport Atlanta, Georgia United States ATL/KATL 104,171,935[1] 2.6% 2 2 Beijing Capital International Airport Chaoyang-Shunyi, Beijing China PEK/ZBAA 94,393,454[2] 5.0% 3 3 Dubai International Airport Garhoud, Dubai United Arab Emirates DXB/OMDB 83,654,250[3] 7.2% 4 4 Los Angeles International Airport Los Angeles, California United States LAX/KLAX 80,921,527[4] 3 8.0% 5 5 Tokyo Haneda Airport <U+014C>ta, Tokyo Japan HND/RJTT 79,520,000[5] 5.6% 6 6 O'Hare International Airport Chicago, Illinois United States ORD/KORD 77,960,588[6] 2 1.3%
예, 잘 들어와 있습니다. 그렇다고 이 데이터를 곧바로 분석하기에는 아직 덜 익은 상태. 데이터를 수집했으면 그다음으로 데이터를 정리해야 합니다.
특정 열을 선택할 때는 변수 뒤에 [숫자]를 붙여주면 됩니다. 예를 들어 이 데이터에서 공항 이름만 선택하고 싶으면 df[2]라고만 입력하면 그만입니다.
head(df[2])
Airport 1 Hartsfield<U+2013>Jackson Atlanta International Airport 2 Beijing Capital International Airport 3 Dubai International Airport 4 Los Angeles International Airport 5 Tokyo Haneda Airport 6 O'Hare International Airport
'head(df$Airport)'라고 쳐도 위하고 똑같은 결과가 나옵니다. 그러니까 $다음에 항목(열) 이름을 붙이면 해당 열만 뽑아볼 수 있는 겁니다. 열 이름은 'colnames' 함수로 확인할 수 있습니다.
colnames(df)
[1] "Rank" "Airport" "Location" [4] "Country" "Code\n(IATA/ICAO)" "Total\npassengers" [7] "Rank\nChange" "%\nChange"
최종 결과물(지도)에서 원 크기가 나타내는 건 총 이용자 숫자입니다. 그런데 열 이름이 'Total\npassengers'로 치기 어렵게 돼 있습니다. 편하게 'total'로 바꾸려면 이렇게 입력하면 됩니다. 이렇게 바꾸지 않아도 Total\npassengers를 입력하면 코드는 똑같이 돌아갑니다. 한 줄이라도 더 줄이시려면 필요없는 내용입니다.
colnames(df)[6] <- "total"
이러면 승객 숫자는 df$total에 들어 있겠죠? 문제는 주석이 그대로 따라오면서 total 끝에 "[n]"이 붙어 있다는 것. 실제로 확인하면 이렇게 보입니다.
df$total
[1] "104,171,935[1]" "94,393,454[2]" "83,654,250[3]" "80,921,527[4]" [5] "79,520,000[5]" "77,960,588[6]" "75,711,130[7]" "70,516,000[8]" [9] "66,002,414[9]" "65,933,145[10]" "65,670,697[11]" "63,625,664[12]" [13] "60,786,937[13]" "60,119,215[14]" "59,732,147[15]" "58,956,288[16]" [17] "58,698,039[17]" "58,266,515[18]" "57,765,397[19]" "55,892,428[20]" [21] "55,631,385[21]" "54,145,000[22]" "53,106,505[23]" "52,624,000[24]" [25] "50,420,583[25]" "47,435,640[26]" "46,039,037[27]" "45,736,700[28]" [29] "44,680,555[29]" "44,584,603[30]" "44,422,022[31]" "44,335,198[32]" [33] "44,154,693[33]" "43,383,528[34]" "43,119,628[7]" "42,296,322[35]" [37] "42,261,309[36]" "41,980,339[37]" "41,975,090[38]" "41,923,399[39]" [41] "41,870,000[40]" "41,738,524[41]" "41,710,254[42]" "41,615,689[43]" [45] "40,460,135[44]" "40,351,331[45]" "39,516,782[46]" "39,053,652[47]" [49] "37,517,957[48]" "37,322,843[49]"
여기서 정리하는 방식은 두 가지가 있습니다. 하나는 일일이 [n]를 빼고 소스에 입력하는 겁니다. 예를 들어 df$totla[1] <- "104,171,935"처럼 말입니다.
이렇게 하면 너무 오래 걸리니까 정규 표현식을 통해 [n] 부분을 없애 줍니다. gsub는 gsub('바뀔 표현', '바꿀 표현', 변수) 형태로 씁니다.
R는 정규 표현식을 쓸 때 '['로 열고 ']'로 닫습니다. 아래 [0-9]는 몯느 숫자를 가리키는 표현입니다. +는 숫자가 한 개 이상이라는 뜻이고요.
그럼 '[' 자체는 어떻게 표현할까요? '\\'다음에 '['가 오면 그 문자 자체라는 뜻입니다. ']'도 마찬가지로 '\\]'로 씁니다. 쉼표도 남아 있으니 같은 방식으로 없앱니다.
df$total <- gsub('\\[[0-9]+\\]', '', df$total)
df$total <- gsub(',', '', df$total)
아직 끝이 아닙니다. 기술(기초) 통계를 알려주는 summary 함수를 df$total에 적용하면 쿨하게 '문자열(character)'이라고 나옵니다. 그래서 as.numeric을 써서 이게 숫자라는 걸 또 알려줘야 합니다. 그다음 summary 함수를 적용해 보면 숫자로 바뀐 걸 알 수 있습니다.
df$total <- as.numeric(df$total)
summary(df$total)
Min. 1st Qu. Median Mean 3rd Qu. Max.37320000 42050000 48930000 54470000 60620000 104200000
고생하셨습니다. 이제 비로소 데이터 정리가 모두 끝나고 그림을 그릴 준비가 됐습니다.
ggplot2는 R 그래픽을 대표한다고 해도 과언이 아닐 만큼 인기를 끌고 있는 패키지고, ggmap은 ggplot2를 활용해 지도를 (편하게) 그릴 수 있도록 도와주는 패키지입니다. 불러오겠습니다.
library('ggplot2')
library('ggmap')
ggmap에는 geocode라는 함수가 있는데 특정 위치가 어떤 위도·경도에 자리잡고 있는지 알려주는 구실을 합니다. 공항 이름만 넣어주면 자동으로 경도 위도를 알려줍니다. 우리는 gc 변수를 따로 만들었으니까 지금까지 가지고 있던 데이터와 위·경도 자료가 따로 있겠죠?
cbind 함수를 활용해서 합쳐 줍니다. (같은 공항을 다룬 자료에서 열이 늘어나는 거니까 옆으로 더하는 cbind를 씁니다. 위아래로 묶을 때는 rbind를 쓰시면 됩니다.) ggmap에 있는 map_data로 세계 지도까지 불러오면 일단 지도를 그릴 준비는 끝입니다.
gc <- geocode(df$Airport)
df <- cbind(df, gc)
world <- map_data('world')
ggplot는 메인 함수(ggplot) 뒤에 필요로 하는 그래픽 요소를 더하는 방식으로 씁니다. (이 cheat sheet를 참조하시면 좋습니다.)
먼저 세계지도를 도형(geom_polygon) 모양으로 그려놓습니다.
그다음 x축은 경도(lon), y축은 위도(lat)로 지정해 점(geom_point)을 찍으면 공항 위치를 나타낼 수 있습니다. 점 크기는 이용객 숫자를 기준으로 합니다. 'alpha'는 투명도를 나타내는 매개변수입니다.
진하게 그린 점(원)은 가운데를 비워서(shape=1) 외곽선이 따로 있는 것처럼 보이게 만듭니다.
이걸 코드로 만들면 아래처럼 나타납니다.
ggplot(df, aes(x=lon, y=lat)) +
geom_polygon(data=world, aes(x=long, y=lat, group=group), fill="grey75", color="grey70") +
geom_point(color="dark red", alpha=.25, aes(size=total)) +
geom_point(color="dark red", alpha=.75, shape=1, aes(size=total)) +
theme(legend.position='none')
이 코드는 각 항목마다 줄을 나눠서 여러 줄인 것처럼 보이지만 사실 한 줄을 나눠 쓴 데 지나지 않습니다. 이렇게 나누지 않고 옆으로 계속 이어 써도 결과는 마찬가지로 나타납니다.
R를 모르는 상태에서 저런 지도를 그려야 한다면 어떻게 할까요?
첫 번째 방법은 세계지도 파일을 하나 내려받아서 눈대중으로 최대한 비슷한 위치에 직접 그리는 거겠죠? 저처럼 게을러서 그 정도 '노가다'를 감당하기 힘든 분이라면 MS 엑셀을 떠올리실 겁니다. 실제 이 아래 그림은 제가 엑셀로 똑같은 지도를 그려본 결과물입니다.
나쁘지 않죠? 그런데 저는 이 그림을 그리면서 R 자료를 그대로 엑셀로 가져와 시각화 작업만 진행했습니다. R에서 그린 세계지도를 '그림 영역' 배경으로 설정하고 축 범위를 잘 맞춘 다음 '분산형' 차트 중에서 '거품형' 그래프를 활용해 경도, 위도를 그렸습니다.
만약 R가 없었다면 제일 먼저 위키피디아 페이지에 가서 자료를 복사해 엑셀에 붙여 넣는 것부터 시작했을 겁니다. 그럴 땐 또 꼭 깨지는 내용이 있습니다.
이용객 숫자에 주석이 딸려오는 건 find, left, value 함수를 섞어서 해결하면 됩니다. 그리고 공항 위치(경·위도)를 알아야 하니까 주소를 좌표로 바꿔주는 사이트에 하나 하나 공항 정보를 입력한 다음에 열을 따로 만들어 옮겼을 겁니다.
이어 지도 투영법에 따라 상대적 위치가 변할 수 있으니 가장 평면적인 지도 사진을 찾아 헤매야 했을 터. 그리고 또 열심히 축 조정을 해야 했을 겁니다.
그게 잘못됐다는 건 아니지만 R로 하는 편이 훨씬 빠르고 효율적입니다. 만약 경우에 따라 '이런 데이터는 전처리까지는 MS 엑셀(이나 기타 프로그램)이 더 편하다'고 하시면 물론 그 프로그램을 활용하셔도 됩니다.
거꾸로 데이터 처리는 R가 편하지만 시각화는 MS 엑셀이 더 편하다고 생각하시는 경우에도 마찬가지입니다. 단, 그 경우에도 R라는 새로운 도구를 하나 더 알고 있는 게 결코 나쁜 일이 되지는 않을 겁니다.
R을 본격적으로 공부해 보고 싶으신 분은 '최대한 친절하게 쓴 R' 시리즈를 훑어 보셔도 나쁘지 않을 거라고 감히 제안드려 봅니다.
댓글,