[DBGUIDE 연재] Data Munging With R

금번 연재는 data.table, plyr, sqldf대한 소개와 더불어 R base포함된 비슷한 역할을 하는 함수들에 대해서 설명한다.

다음 회가 대망의 ggplot2인데 아마도 단일 회의 연재로서 역대 최장의 양을 자랑할거라 생각한다.

<연재주제> R 기반의 데이터 시각화

<이번 연재제목> R데이터 다루기
<필자> 전희원 | 넥스알에서 데이터 사이언티스트로 일하고 있다.

<연재순서>

1회: R로 하는 데이터 시각화의 시작

2회: R 프로그래밍 맛보기

3회: R데이터 다루기(data munging with R) (data.table, plyr, sqldf 패키지 비교·이용)

4회: ggplot2를 이용한 R 시각화

5회: Inkscape를 활용한 그래프 후처리

 

지난 연재에서는 R의 문법에 대한 개략적인 소개를 했다. 아마도 R이라는 언어가 어떤 성격의 언어인지 느낌은 갖고 있을 거란 생각을 해본다. 사실 ’시각화를 하자는데, 왜 이리 해야 할 것들이 많아?’ 하고 생각하는 독자들도 있을 거란 생각을 해본다. 실제로 이런 작업이 필요하다. 왜냐면 시각화 패키지들은 그 나름대로 입력 데이터 형식과 문법이 있기 때문이다. 따라서 그것에 맞추어 데이터를 제공해 줘야 원하는 시각화가 가능하다.

이번 연재에서 소개할 내용은 R로 하는 데이터 먼징(data munging)이다. 사실 먼징(munging)이라는 말은 전처리, 파싱, 필터링과 같이 데이터를 이리저리 핸들링하는 행위를 의미하는 단어다. 그런데 원고를 쓰는 이 시간에도 워드프로세서는 사전에 없는 단어라고 munging 아래에 빨간 줄을 그어 놓는다. 이 단어는 컴퓨터로 데이터를 처리하는 사람들 사이에서 많이 쓰이는 따끈따끈한 신조어다.

R은 데이터 분석과 시각화에 강한 언어다. 게다가 언어 자체의 데이터 먼징 기능도 출중하지만, 여러 패키지의 도움으로 원본(raw) 데이터를 여러 가지 분석가가 원하는 형태로 아주 쉽게 변형이 가능하다. 따라서 시각화를 위한 데이터를 쉽게 만들 수 있고, 모델링을 위한 기반 데이터로 만들기도 매우 쉽다.

이번 회에서는 기본적인 R의 데이터 먼징 기능을 먼저 살펴보고, 동일한 작업을 좀더 직관적이며 빠르게 수행할 수 있는 여러 패키지를 살펴본다. 이를 위해 data.table, sqldf, plyr 패키지를 살펴볼 예정이다.

data.table은 R에 기본적으로 탑재된 data.frame을 상속한 클래스로서 data.frame의 거의 모든 연산을 수행하며, 추가적으로 좀더 간편한 인터페이싱을 제공하는 패키지다. 필자는 이 패키지를 사용하면서 data.frame을 핸들링할 때보다 코드를 크게 줄였던 경험을 갖고 있다.

사실 data.frame 데이터형은 RDBMS의 table과 성격이 비슷하다. 따라서 이를 SQL로 인터페이싱 하려는 노력이 있었는데, sqldf가 바로 그 역할을 한다.

plyr 패키지는 필자가 앞의 세 패키지 중에서 가장 먼저 사용한 것이다. 이를 만든 Hadley 교수가 다양한 유명 패키지 가운데서도 이를 사용하면서 더욱 유명해졌다.

얼마 전에 어느 블로거가 가장 많이 사용되는 50개의 R 패키지를 소개한 워드클라우드가 있어서 소개한다.

clip_image002

그림 1. 가장 많이 사용되는 R 패키지 50 워드클라우드

(출처: http://r-de-jeu.blogspot.kr/2012/04/50-most-used-r-packages.html)

다음에 소개할 ggplot2이 가장 인기 있는 패키지이고, 그 뒤를 따르는 것들이 data.table과 plyr 패키지라는 것을 잘 알 수 있을 것이다. sqldf는 성능에 비해 나온 지 얼마 되지 않아 사용자가 많지 않은 것으로 생각된다.

R 기반 집계 함수

SQL의 select 류의 함수를 R의 기본 데이터 연산과 비교해 간단하게 알아보자. 아마도 통계학과나 컴퓨터공학과 혹은 개발자 경력을 가진 독자라면 SQL이라는 게 어떤 것을 의미하고 실제 사용해 봤을 것이다. 실제로 통계학과나 컴퓨터공학과의 경우 해당 과목이 커리큘럼에 있다. 안드로이드 앱 개발의 경우도 경량 DB가 포함돼 있기 때문에 대부분 경우 SQL을 사용해 봤을 거라고 생각한다. 그리고 앞선 연재에서 data.frame이 RDBMS의 table과 비슷하다고 소개했던 사실도 기억하고 있을 것이다. 따라서 SQL을 기반으로 data.frame의 기본 연산에 대해 좀더 살펴보자. 결론적으로 말하면 data.frame을 핸들링할 수 있는 방법은 정말 많다. 같은 결과를 도출할 수 있는 다양한 방법이 있기 때문에 여기서 언급하지 않은 부분이 존재함을 이해해 주기를 부탁 드린다.

기본적으로 iris 데이터는 R 쉘을 띄운 후 바로 사용이 가능한 데이터 세트이므로 이 데이터를 기반으로 설명한다. 참고로 R 코드에서는 주석 앞에 #을 붙인다.

summary(iris)

# 첫 번째 행

iris[1,]

#1~10 row까지 출력

iris[1:10,]

iris[c(1:10),]

#꽃잎 길이가 2 이상인 행 출력

iris[iris$Petal.Width > 2,]

#Petal.Width가 2보다 큰 행에서 Sepal.Length와 Sepal.Width 열만 출력

iris[iris$Petal.Width > 2,c("Sepal.Length", "Sepal.Width")] —— ①

# Sepal.Length와 Sepal.Width 열은 첫 번째,

# 두 번째 열이므로 위 명령어와 같은 결과를 내준다.

iris[1:10,1:2]

#종종 사용하는 테크닉

iris[which(iris$Species == "setosa"),"Species"]

data.frame의 인덱스 연산은 대부분 아래와 같은 사용방식을 따른다.

DF[where, select]

따라서 ①코드는 SQL로 변환한다면 아래와 같다. 이때 “.”문자는 SQL에서는 R과 다른 의미로 쓰이므로 주의해야 한다.

select Sepal.Length, Sepal.Width from iris wherePetal.Width> 2;

더 추가하면 DF[value] 형식의 연산을 할 수도 있다. 이 부분은 좀 헷갈릴 수 있으니 주의가 필요하다.

a <- iris[1]

b <- iris[,1]

identical(a,b) #FALSE —-

b <- iris[,1,drop=F] —-

identical(a,b) #TRUE

a <- iris[1:2]

b <- iris[,1:2]

identical(a,b) #TRUE

a <- iris["Species"]

b <- iris[,"Species",drop=F]

identical(a,b) #TRUE

identical() 함수는 입력된 두 객체가 정확하게 같은지 비교하는 연산이다.

①에서 FALSE가 나오는 이유는 두 연산에서 나오는 데이터 내용은 같지만 하나는 data.frame을 리턴하고 나머지 하나는 벡터를 리턴하기 때문이다. 이 부분은 초보자들이 쉽게 틀릴 수 있는 부분이므로 염두에 둬야 한다. 따라서 data.frame 형식으로 무조건 리턴하게 만들기 위해 drop=F(ALSE) 옵션을 할당했더니 비로서 같은 데이터를 리턴했다는 메시지를 볼 수 있다. 이 옵션의 경우는 어떤 열이 선택될지 모르는 상황에서 연산으로 리턴되는 데이터타입을 보장하기 위해서 주로 사용된다.

SQL의 많은 연산은 특정 기준값에 대해 집계를 하는 류의 질의어가 상당히 많이 사용되고 있다. group by 같은 연산이 그 예이다. 하지만 아쉽게도 이와 같은 고수준의 데이터 연산을 data.frame에서는 지원하지 않는다. 이를 기본 base 패키지만 갖고 수행해보고자 한다면 tapply, aggregate, by 류의 집계함수를 사용해야 한다.

tapply, aggregate, by 함수

먼저 iris 데이터는 너무 재미 없고 심심하므로 우리 생활과 밀접한 정보를 담고 있는 생필품 가격 데이터를 사용해 보자. 이 데이터의 출처는 http://data.seoul.go.kr/metaview.jsp?id=OA-1109이다. 이곳에 가입해 사용해도 좋지만 실습 편의성을 위해 데이터를 ‘드롭박스’에 올려두고 바로 읽어 들이는 방식으로 코드를 소개한다. 물론 위 링크에 가서 CSV 파일로 저장한 뒤 그 데이터를 사용해도 무방하다. 데이터는 아래 순서와 같은 필드(열)로 구성돼 있다.

일련번호,

시장/마트번호

시장/마트이름

품목번호

품목이름

실판매규격

가격

년도

비고

시장유형 구분코드

시장유형 구분 이름

자치구 코드

자치구 이름

market_price <- read.csv("https://dl.dropbox.com/u/8686172/requisites.csv",

fileEncoding="euc-kr")

head(market_price)

nrow(market_price)

summary(market_price)

read.csv 명령어는 웹에 공개된 파일이든 로컬 파일이든 읽어와 텍스트 인코딩에 맞게 읽어 data.frame 형식으로 리턴한다. fileEncoding 옵션은 외부 파일을 읽어 들일 때 항상 명시해 주는 습관을 들이는 게 좋다. 왜냐면 한글 표현에 있어서 문제가 생길 수 있기 때문이다. 그러나 인코딩을 잘 모르겠다는 독자는 윈도우나 맥의 에디터에서 문서 정보를 볼 수 있는데 그 부분을 확인하면 된다.

대부분의 국내 R 사용자는 윈도우 환경을 이용하기 때문에 윈도우를 기준으로 소개한다. 프리웨어인 Notepad++(http://www.notepad-plus-plus.org/) 에디터를 열고 다음과 같은 트레이에 인코딩 정보를 확인한다. 한글이 에디터에서 잘 보이고 트레이에 인코딩이 ANSI, EUC-KR, Windows 949라고 표현되어 있으면, 코드와 같이 “EUC-KR”로 입력하면 된다. UTF-8로 표시돼 있으면 fileEncoding=”UTF-8”로 하면 된다.

clip_image005

그림 2. Notepad++에서 인코딩 정보 확인

R에서 한글 데이터를 분석하기 위해서는 여러 기반 지식이 필요하다. 그중에 하나가 인코딩에 대한 이해다. 이 부분에 대한 좀더 상세한 설명을 보고 싶으면 필자가 올려둔 동영상(http://www.youtube.com/watch?v=araCWRa5Nxg)을 확인하기 바란다.

인코딩 설명에 서두가 길어졌는데, market_price변수에 data.frame이 저장된다. 그리고 일상적으로 head, nrow, summary 같은 명령어를 입력해 데이터가 어떻게 생겼는지 확인하는 과정을 통상적으로 하게 된다. 이때 확인하는 부분은 data.frame 데이터의 레코드가 자신이 알고 있는 레코드 개수와 맞는지, 본인이 예상한 필드명과 그에 해당하는 데이터들이 적합한지 개략적으로 확인한다. summary 명령어를 보면 대략적인 데이터 분포를 필드별로 확인이 가능해 굉장히 유용하다.

그럼 이제 데이터로 돌아가자!

이 데이터는 전통시장과 대형 마트에서 생필품 가격이 어떻게 형성되는지 직접 공무원들이 조사를 나가 수집한 데이터다. 물론 지역에 따라 모두 구분돼 있고, 시장과 마트 구분은 물론, 마트 이름과 시장 이름도 확인 가능하다.

여기서 여러분들이 가장 먼저 보고 싶은 정보은 무엇인가? 필자는 직접 장을 자주 보기 때문에 전통시장과 대형 마트 간 가격차이가 있는지 확인해 보고 싶었다. 물론 품목별로 평균을 뽑아 적절한 데이터 포맷으로 보여주면 될 것이다.

R은 특정 데이터 집단에 함수를 적용하는 tapply, by, aggregate 같은 함수를 제공한다. 이들의 동작원리는 대략 다음과 같다.

clip_image007

그림 3. R 집계함수의 동작원리

split, apply, combine라는 전략을 사용하는 방식으로 특정 기준에 의해 데이터를 그룹화 하고 개별적으로 처리해 마지막에 결합하는 효율적인 전략을 사용한다. 이 개념은 Hadoop의 map/reduce와 닮았다. 참고로 R에도 Map(), Reduce()라는 함수형 언어에서 가져온 함수가 포함돼 있다. tapply, by, aggregate 함수들은 모두 위와 같은 전략을 따르며, 입출력 포맷에 따라 다른 이름을 가지고 있을 뿐이다.

tapply를 가지고 대형 마트, 전통시장의 평균 물품 가격을 뽑아보는 명령어는 다음과 같다.

tapply(market_price$"A_PRICE", market_price[,c("M_TYPE_NAME","A_NAME")],mean)

함수의 원형은 “tapply(X, INDEX, FUN = NULL, …, simplify = TRUE)” 형태와 같다. X는 우리가 확인하려는 가격이 되고, INDEX는 factor형으로 구성된 리스트를 입력한다. 물론 의문을 가질 수 있다. 왜냐면 위의 예제는 사실 data.frame이 인자로 들어갔기 때문이다. 이제야 언급하지만 data.frame은 리스트이기도 하다. 물론 리스트에 대해서는 자세한 설명을 하지 않았지만 data.frame의 연산방식에 리스트의 특징도 들어가 있는 것을 알 수 있을 것이다. 그리고 마지막 인자 FUN에는 함수명을 입력한다. 이렇게 나오는 코드는 행이 M_TYPE_NAME이고, 열이 A_NAME으로 구성된 matrix 데이터를 리턴한다. 물론 그 내부 값들은 모두 평균값이 계산된 결과이다.

tapply는 키가 하나이고 FUN으로 입력된 함수의 리턴값이 하나라면, 벡터형의 데이터를 반환하고, 이것보다 복잡한 데이터가 리턴될 경우 matrix나 리스트 형태로 반환된다.

by(market_price$A_PRICE, market_price[,c("M_TYPE_NAME","A_NAME")], mean)

위의 명령어도 있다. 역시 비슷한 작업을 하나 작업 방식이 서브 data.frame을 키를 기준으로 분리한 뒤 각 data.frame에 mean함수를 적용해서 이들을 리스트로 엮어서 리턴한다.

aggregate(market_price$A_PRICE, market_price[,c("M_TYPE_NAME","A_NAME")] ,

       mean)aggregate( A_PRICE ~ M_GU_NAME + A_NAME, market_price, mean)

위의 aggregate 함수는 이전 함수들과 같은 연산을 하지만 리턴되는 데이터가 data.frame이다. 사실 대부분의 경우 aggregate 함수로 집계한 결과가 보기에 편하고 직관적이다. 아니면 data.frame이 데이터를 핸들링하고 보는 데 익숙해서 그럴 수도 있다(엑셀의 시트, RDBMS의 테이블이 이와 비슷한 성격인 것도 한몫 하는 거 같다).

M_TYPE_NAME A_NAME x

1 대형마트 고등어 1000.0000

2 대형마트 고등어(30cm,국산) 5335.0000

3 전통시장 고등어(30cm,국산) 2666.6667

4 대형마트 고등어(냉동,국산) 2825.0000

5 전통시장 고등어(냉동,국산) 3524.8462

6 대형마트 고등어(냉동,수입산) 1500.0000

7 전통시장 고등어(냉동,수입산) 1750.0000

8 대형마트 고등어(생물,국산) 4550.4545

위는 aggregate 명령어의 결과로 나온 data.frame의 첫 부분이다. 그리고 ggplot2에서는 이런 작업을 사용자 모르게 내부적으로 수행해 집계된 결과를 플로팅해 준다. 그리고 이를 처리해 주는 패키지가 plyr이고, 역시 ggplot2의 개발자인 Hadley 교수가 필요에 의해 만든 것이다.

그럼 이제 plyr 패키지를 활용한 방법에 대해 설명할 때가 된 듯 하다.

여담이지만 이 패키지의 개발자인 Hadley 교수는 R커뮤니티 내에서도 굉장한 인기가 높다. 필자는 UseR! 2012 행사에 참석해 직접 이 분이 발표하는 세션을 들었던 경험이 있다. 강의실 제일 뒤에서 서서 들어야 했으며 강의실 사이에 난 통로까지 사람들이 빼곡히 들어차 그 엄청난 인기를 실감할 수 있었다. 대단한 내용이 아닌 발표임에도 수많은 사람으로 인산인해를 이뤘다. 그러나 정작 중요한 것은 Hadley 교수의 R 패키지 개발 동기였다. 그분이 밝힌 개발 동기는 다른 사람이 자신이 겪은 시행착오를 겪지 않고 편하게 분석할 수 있게 만들고 싶어서였다고 한다. 패키지들의 완성도만큼이나 훌륭한 동기를 갖고 계셨고, 그런 열정과 대중을 위한 재능 기부가 그 인기를 만들었으리라 생각해 본다.

plyr 패키지 이외에도 앞서 첨부한 워드 클라우드에서 ‘reshape’라는 패키지를 볼 수 있을 것이다. 이 역시 데이터를 핸들링하고 변형하는 패키지이며, Hadley 교수가 만들었다. 물론 이 패키지를 이용해서도 유사한 데이터 먼징 작업을 수행할 수 있지만 성격은 약간 다르다. plyr이 split, apply, combine의 전략으로 데이터를 그룹하여 집계 처리하는 것과는 다르게, 집계하기 보다는 데이터 정보의 손실 없이 데이터의 형태를 바꾸는 작업이 주된 작업이다. 물론 집계도 가능하지만 패키지의 목적은 집계가 아니다. 그래서 이름도 reshape이다. reshape에 대한 설명은 여기서 다루지 않겠지만 관심 있는 독자라면 패키지의 목적 정도는 직접 파악해 보기를 권한다.

거꾸로 알아보는 하둡

split-apply-combine 전략은 많은 데이터 처리 업무에 대한 프레임워크를 제공해 줬다. 쪼개고 적용하는 부분을 map이라 하고 이를 다시 결합하는 부분을 reduce라고 불러 이를 프레임워크로 만든 게 하둡(Hadoop)이라 생각하면 된다. 하둡은 대용량 데이터를 처리하는 데 효과적이고, 내부의 데이터가 어떤 것이든지 프레임워크 차원에서 간단하게 설정 가능하게 한 유연성을 제공해 왔다. 그러나 한계가 없는 것은 아니다. split된 이전 데이터 조각의 정보를 가져와 현재 조각에서 뭔가를 하고자 하는 연산은 이 프레임워크에 적합하지 않다. 물론 이 부분은 map/reduce도 마찬가지다.

사실 이 패키지의 목적은 SQL의 group by와 같은 집계를 편하게 하기 위함이지, 하둡과 같이 분산 처리가 주된 목적은 아니다. 하지만 여러분의 시스템 환경이 멀티코어라면 조각난 데이터에서 함수를 적용하는 부분을 멀티코어 분산처리가 가능하게 하는 옵션을 제공한다.

plyr은 입출력 데이터 포맷에 따라 다음과 같이 쓰일 수 있는 함수를 제공한다.

array

data.frame

List

Array

aaply

adply

Alply

data.frame

daply

ddply

Dlply

List

laply

ldply

Llply

<표> 입출력 데이터 포맷에 따른 plyr의 함수

이 중에서 가장 많이 쓰이는 ddply를 사용해 이전에 했던 집계 작업을 해보자.

ddply(.data, .variables, .fun = NULL, …, .progress = "none",

.drop = TRUE, .parallel = FALSE)

data는 입력 data.frame이며, .variables는 쪼개는 기준이 되는 값이다. 다음과 같은 형태 중에 하나가 입력된다.

– 문자벡터: c(“M_GU_NAME”, “A_NAME”)

– 숫자벡터: c(5,13)

– 수식(Formula) : ~ M_GU_NAME + A_NAME

– 특별식: .( M_GU_NAME , A_NAME)

네 번째 특별식은 plyr 패키지에서만 쓰이는 방식이다.

fun은 함수명이 들어가며, …은 입력된 함수명에 추가되는 인자들이다. progress는 수행시간이 오래 걸릴 경우를 대비해 진행상황을 표시해 주도록 하는 옵션이다. drop=TRUE는 현재 쪼개는 조건이 데이터에 있는 경우들만 계산해 출력하도록 하는 옵션이다. 만일 이게 FALSE라면 모든 가능 조건에 대해 출력을 하게 된다. parallel은 머신의 멀티코어를 사용할 수 있게 해주는 옵션이다. 멀티코어를 효과적으로 사용하기 위해서는 데이터 크기와 처리 시간을 고려해 사용해야 한다. 이 부분은 연재의 범위를 넘어서는 주제이므로 생략한다.

library(plyr)

ddply(market_price, .( M_TYPE_NAME, A_NAME), summarise, avg=mean(A_PRICE))

앞의 코드는 우리가 목적하는 결과를 뽑기 위한 것이다. 사실 위에서 fun에 해당하는 코드들이 바로 summarise 이하 부분이다. summarise 함수는 그룹화된 data.frame을 요약하는 데 쓰이는 함수다. 따라서 summarise의 첫 번째 인수에는 쪼개는 기준으로 쪼개진 data.frame 조각들이 들어가게 된다. avg라는 새로운 열로 쪼개진 각 데이터에서 A_PRICE 열의 평균값들을 넣게 된다. 대부분 summarise를 넣어 사용하지만 간혹 transform 함수를 넣어 사용하기도 한다. transform 함수는 기본 base 패키지에서 제공하는 함수로, 기존의 data.frame에 새로운 열을 추가한다든지 혹은 기존의 열을 다른 데이터로 채워 넣을 때 사용한다. summarise 함수가 avg에 대한 하나의 열만 반환하지만, transform은 열을 추가한 data.frame 조각 전체를 반환하게 된다. 사실 설명만으로 두 함수의 구별점을 찾기는 힘들다. R을 학습하면서 가장 좋은 습관 중에 하나는 특정 데이터를 갖고 함수가 실제 어떻게 동작하는지 확인하는 것이다. 앞의 두 함수도 어떻게 동작하는지 독자들이 꼭 한번 직접 실습해 보기 바란다.

마지막으로 연산 처리 속도가 가장 빠르다고 자타가 공인하는 data.table의 코드를 살펴보자. 사실 data.table이 빠른 이유는 RDBMS에서 테이블에 인덱스를 거는 것과 같은 작업을 명시적으로 해주기 때문이다. 특정 키에 대해 sequential search를 하는 data.frame보다 binary search를 하는 data.table이 빠른 건 어찌 보면 당연한 이야기일 수 있다.

data.table의 경우 data.frame을 상속해 대부분 유사한 사용방법을 공유하고 있다. 하지만 이를 굳이 data.frame을 통해 구현하지 않은 이유는 사용 편의성과 유연성을 제공하기 위해 기존의 data.frame과 혼용 시 오류를 불러올 수 있는 부분들이 있기 때문이다. 따라서 data.table을 data.frame에 기반해 접근하는 것은 가장 쉬운 접근 방식이지만, 반드시 자신이 의도한 바에 일치하는지 메뉴얼을 기반으로 확인 후에 사용해야 한다. 왜냐면 미묘하게 다르게 동작하는 부분들이 있기 때문이다.

data.table의 개략적인 문법은 아래와 같다.

dt[i, j, mult={‘first’, ‘last’, ‘all’},

        nomatch={0, NA},

        roll={FALSE, TRUE},

        by=’colname’]

i,j에 들어가는 값은 data.frame의 그것과 크게 다르지 않다. i는 행을 선택하는 문법이며, j에는 열을 선택하는 문법이 들어가게 된다. 그리고 나머지 옵션에서 mult는 JOIN 연산 시 키 매칭의 처음과 마지막, 그리고 모두를 리턴하는 옵션을 의미한다. nomatch의 경우 OUTER JOIN을 사용할때 적용되며, roll은 시계열 데이터에 대한 rolling join을 할 때 가장 최근의 시간 데이터를 활용하게 하는 옵션이다. by의 경우 GROUP BY 연산을 할 때 사용하는 옵션으로 우리가 필요로 하는 옵션이다.

library(data.table)

market_price.dt <- data.table(market_price) —

market_price.dt[2,list(M_NAME)] —

여기서 지금까지 사용한 data.frame 객체인 market_price을 data.table() 함수를 이용해 data.table로 변환해주는 코드 ①과 변환된 객체에서 두 번째 행의 M_NAME 열의 데이터를 가져오는 코드 ②를 보여준다. 사실 ②번 코드는 기존의 data.frame을 이용할 경우 아래의 코드로 가능하다.

market_price[2,"M_NAME",drop=F]

다르게 동작하는 이들 코드에는 상당히 많은 내용이 함축돼 있다. 일단 drop=F를 사용한 이유에 대해 설명하면 다음과 같다.

앞서 간단히 설명했지만, data.frame의 경우 한 열을 리턴할 경우 vector로 리턴하고, 두 개 이상의 열을 리턴할 경우 data.frame으로 리턴하는 상당히 특이한 방식을 사용한다. 이 때문에 함수를 작성하는 데 어려움을 겪을 때가 종종 있다. 물론 data.frame으로 해결할 수 있는 방법도 있는데 바로 drop=F가 그런 역할을 한다.

이런 어려운 점을 해결하기 위해 data.table의 경우 리스트로 열 이름을 입력 받게 하고 모든 경우에 data.table로 리턴하게 했다. 이는 data.frame을 오용하는 것을 원천에 방지한다.

그럼 우리가 구하고자 하는 코드를 보여주도록 하겠다.

market_price.dt[,list(avg = mean(A_PRICE)), by=list(M_TYPE_NAME, A_NAME)]

M_TYPE과 A_NAME으로 GROUP BY된 각각의 data.table에 avg라는 열을 입력해 A_PRICE의 평균값을 넣는 연산을 의미한다. 물론 by에 들어가는 변수는 여러 개가 될 수 있으며, 연산을 통해 만들어지는 avg 같은 변수들도 여러 개가 될 수 있다. 이는 기본 base 패키지에서 제공하는 집계함수에서는 보기 힘든 연산 방식이며, plyr에서도 이와 같은 연산을 지원한다.

마지막으로 sqldf를 활용한 방법을 간단하게 살펴보겠다.

library(sqldf)

sqldf("select M_TYPE_NAME, A_NAME, avg(A_PRICE) as avg

   from market_price group by M_TYPE_NAME,A_NAME;")

사실 이 패키지의 대부분을 차지하는 함수는 바로 sqldf()이다. 아마도 독자가 SQL에 익숙하다면 위 select 문이 어떤 것을 의미하는지 바로 파악할 수 있을 것이다. 더불어 앞서 설명한 여러 함수들과 동일한 결과를 의도한다는 것을 바로 눈치챘을 것이다. 바로 위의 함수 사용 예처럼 sqldf는 data.frame을 흡사 데이터베이스의 테이블처럼 여기며 sql문으로 조작할 수 있는 인터페이스를 제공한다. 이는 SAS의 SQL procedure와 흡사하며, sqldf() 안에서 대부분의 SQL 쿼리를 수행할 수 있고 avg 같은 SQL 내장 함수도 호출할 수 있다.

plyr이나 data.table, sqldf를 설명하면서 반드시 나오게 되는 내용은 퍼포먼스 차이이다. 이미 언급한 것처럼 알고리즘 관점에서 data.table이 빠를 거라는 예상 할 수 있다.

> system.time(ddply(market_price, .(M_TYPE_NAME, A_NAME),

         summarise, avg=mean(A_PRICE)))

사용자 시스템 소요된

0.11 0.00 0.11

> system.time(market_price.dt[,list(avg = mean(A_PRICE)),

         by=list(M_TYPE_NAME, A_NAME)])

사용자 시스템 소요된

0.02 0.00 0.02

> system.time(sqldf("select M_TYPE_NAME, A_NAME, avg(A_PRICE) as avg

         from market_price group by M_TYPE_NAME,A_NAME;"))

사용자 시스템 소요된

0.03 0.00 0.03

system.time은 인자로 주어진 표현식(expression)에 대해 수행 소요 시간을 리턴하는 함수다. 앞의 결과를 보면 data.table, sqldf, plyr 순으로 속도가 빠른 것을 알 수 있다. 물론 이 소요시간은 각자 코드를 돌리는 환경에 따라 다를 것이다.

먼징을 해야 하나?

사실 왜 데이터를 이리저리 조작하는 게 시각화 작업에 필요한지 충분한 설명을 하지 못한 게 사실이다. 실제 이 변환된 데이터를 기반으로 확인할 수 있는 건, 전통시장, 대형마트, 그리고 품목에 따른 가격 차이이다. 원 데이터로는 이 결과를 확인할 수 없다. 따라서 변환을 시도한 것이며, 이 변환된 결과가 아래와 같은 플로팅 결과로 나오게 된다.

clip_image009

그림 한 장으로 품목간의 대략적인 가격 차이와 마켓 형태에 따른 가격 차이의 경향을 볼 수 있다. 따라서 위와 같은 플로팅 결과를 보기 위해 그에 따른 적절한 변환이 필요한 것이다. 바로 이런 이유 때문에 우리가 이번 연재에서 데이터 먼징을 배우는 것이다(물론 먼징의 목적이 시각화를 위한 것만은 아니다). 우리가 추후 배울 ggplot2도 그룹 연산을 통해 데이터를 시각화 해주는 옵션이 내부적으로 존재한다. 하지만 그 옵션 자체가 유연성이 많지 않아 직접 이런 식으로 데이터를 변환해 줘야 하는 것이다.

CC BY-NC 4.0 [DBGUIDE 연재] Data Munging With R by from __future__ import dream is licensed under a Creative Commons Attribution-NonCommercial 4.0 International License.