Tidy data

by Minho Lee — on

cover-image

Tidy data

많은 패키지들, 특히 ggplot2는 특정한 형태로 정리된 데이터를 요구한다. 처음에 익숙하지 않을 때는 함수가 요구하는 데이터의 형태를 맞추기도 버거웠지만 차츰 익숙해지면서 깔끔하게 정리된 데이터의 중요성을 깨닫게 된다. 깔끔한 데이터를 만드는 과정 중에서 wide format과 long format 사이의 변환은 방법을 모르면 헤매거나 크게 돌아갈 수도 있기 때문에 여기서 가장 대표적인 패키지, tidyrreshape2의 기본적인 사용법을 정리해보려고 한다.

tidy data에 대한 내용은 https://cran.r-project.org/web/packages/tidyr/vignettes/tidy-data.html를 참고하거나 R에서 tidyr을 설치하고 vignette('tidy-data')명령어로 문서를 볼 수 있다.



library(dplyr)
library(tidyr)
library(reshape2)


데이터를 살펴보면 table의 형태를 가진 자료가 많다. 열과 행에 각각 변수가 들어가며, 찾고자 하는 열과 행에 기록된 값을 찾는다.

이런 형태의 데이터는 contingency table, wide-format data 등 여러 가지 이름을 가진다

엑셀을 사용한다면 데이터블 pivot table로 정리했을 때 이러한 형태로 데이터를 정리할 수 있다

WorldPhones
##      N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1951  45939  21574 2876   1815    1646     89      555
## 1956  60423  29990 4708   2568    2366   1411      733
## 1957  64721  32510 5230   2695    2526   1546      773
## 1958  68484  35218 6662   2845    2691   1663      836
## 1959  71799  37598 6856   3000    2868   1769      911
## 1960  76036  40341 8220   3145    3054   1905     1008
## 1961  79831  43173 9053   3338    3224   2005     1076

위 데이터에서 1958년도 Asia의 값을 확인하려면 4번째 행, 3번째 열의 값인 6662를 보면 된다

WorldPhones[4,3] #로 바로 확인할 수 있다
## [1] 6662

WorldPhones 데이터는 7개의 행과 7개의 열을 가지고 있다.

다시 말하자면 행에 해당하는 변수(연도)가 7가지, 열에 해당하는 변수(지역)가 7가지인 셈이다.

또, 같은 데이터를 이러한 형태로도 표현할 수 있다

data.frame(area = dimnames(WorldPhones)[[2]], 
			as.data.frame(t(WorldPhones), row.names=''))
##       area X1951 X1956 X1957 X1958 X1959 X1960 X1961
## 1   N.Amer 45939 60423 64721 68484 71799 76036 79831
## 2   Europe 21574 29990 32510 35218 37598 40341 43173
## 3     Asia  2876  4708  5230  6662  6856  8220  9053
## 4   S.Amer  1815  2568  2695  2845  3000  3145  3338
## 5  Oceania  1646  2366  2526  2691  2868  3054  3224
## 6   Africa    89  1411  1546  1663  1769  1905  2005
## 7 Mid.Amer   555   733   773   836   911  1008  1076


위 자료들은 모두 동일한 데이터를 표현하고 있지만 그 형태는 각기 다르다.

그냥 단순하게 행과 열을 구분하는 것 만으로는 우리가 표현하고자 하는 데이터를 명확하게 표현하기 힘들다

따라서 데이터가 생긴 형태뿐만 아니라 그 안에 담긴 의미를 적절하게 표현해줄 수 있는 방식을 정의할 필요가 있다



Tidy data

우리가 어떤 Dataset을 가지고 있다면 이 데이터가 가지고 있는 기본적인 속성은 다음과 같다

  • 기본적으로 Dataset은 숫자 또는 문자열 등의 값(value)이 모여서 구성된다.

  • 모든 값은 하나의 변수(variable)와 하나의 관측치(observation)에 속한다.

  • 변수는 같은 속성의 값을 측정한 결과를 말하고, 관측치는 관측대상 하나가 가지는 모든 속성값을 의미한다

WorldPhones_df = data.frame(year = row.names(WorldPhones), 
                            as.data.frame(WorldPhones, row.names = ''))

head(WorldPhones_df)
##   year N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1 1951  45939  21574 2876   1815    1646     89      555
## 2 1956  60423  29990 4708   2568    2366   1411      733
## 3 1957  64721  32510 5230   2695    2526   1546      773
## 4 1958  68484  35218 6662   2845    2691   1663      836
## 5 1959  71799  37598 6856   3000    2868   1769      911
## 6 1960  76036  40341 8220   3145    3054   1905     1008


위에서 설명한 데이터를 좀 더 깔끔하게 구성하기 위해 다음과 같은 원칙을 정했다

  • 각 변수는 하나의 열을 구성하고, 각 관측치는 하나의 행을 구성한다

  • 하나의 테이블은 동일한 속성의 관측대상들로 구성된다

이러한 원칙 하에 정리된 데이터를 tidy data라고 한다

WorldPhones 데이터를 깔끔하게 정리해본다면 다음과 같이 할 수 있을 것이다

일단 함수보다는 결과물에 형태를 살펴보자

melt(WorldPhones)
##    Var1     Var2 value
## 1  1951   N.Amer 45939
## 2  1956   N.Amer 60423
## 3  1957   N.Amer 64721
## 4  1958   N.Amer 68484
## 5  1959   N.Amer 71799
## 6  1960   N.Amer 76036
## 7  1961   N.Amer 79831
## 8  1951   Europe 21574
## 9  1956   Europe 29990
## 10 1957   Europe 32510
## 11 1958   Europe 35218
## 12 1959   Europe 37598
## 13 1960   Europe 40341
## 14 1961   Europe 43173
## 15 1951     Asia  2876
## 16 1956     Asia  4708
## 17 1957     Asia  5230
## 18 1958     Asia  6662
## 19 1959     Asia  6856
## 20 1960     Asia  8220
## 21 1961     Asia  9053
## 22 1951   S.Amer  1815
## 23 1956   S.Amer  2568
## 24 1957   S.Amer  2695
## 25 1958   S.Amer  2845
## 26 1959   S.Amer  3000
## 27 1960   S.Amer  3145
## 28 1961   S.Amer  3338
## 29 1951  Oceania  1646
## 30 1956  Oceania  2366
## 31 1957  Oceania  2526
## 32 1958  Oceania  2691
## 33 1959  Oceania  2868
## 34 1960  Oceania  3054
## 35 1961  Oceania  3224
## 36 1951   Africa    89
## 37 1956   Africa  1411
## 38 1957   Africa  1546
## 39 1958   Africa  1663
## 40 1959   Africa  1769
## 41 1960   Africa  1905
## 42 1961   Africa  2005
## 43 1951 Mid.Amer   555
## 44 1956 Mid.Amer   733
## 45 1957 Mid.Amer   773
## 46 1958 Mid.Amer   836
## 47 1959 Mid.Amer   911
## 48 1960 Mid.Amer  1008
## 49 1961 Mid.Amer  1076

위와 같이 정리하면 변수 Var1에는 연도가, Var2에는 지역이 들어가고 value 열에는 값이 들어간다

또, 첫 번째 행에는 1951년도 북아메리카의 전화기 수, 10 번째 행에는 1957년도 유럽의 전화기 수가 정리되어 있다.

원래의 형태에 비해서는 분명 양이 많아졌지만 이와 같은 형태로 여러 가지 장점이 있다.

우선 변수를 자유롭게 추가할 수 있다. 기존의 WorldPhone과 같은 Wide format의 데이터는 행과 열에 변수가 하나씩 들어간다. 때문에 두 개 변수에 대해서는 더 간단하고 깔끔하게 정리할 수 있지만 추가적인 변수를 표현하기에는 힘들다. 반면에 long format으로 정리한 경우에는 그냥 열을 하나 더 만들면 변수를 추가할 수 있다

두 번째로는 변수를 열 하나로 관리하기 때문에 데이터를 더 깔끔하게 처리할 수 있다. 하나의 변수는 같은 속성을 공유하게 되는데 같은 변수라면 같은 열에 배치되는 이러한 방식은 동일한 속성의 값을 같은 방식으로 처리할 수 있게 해준다. 이렇게 데이터가 정리된 경우 dplyr의 mutate함수를 이용하면 열 단위로 데이터를 처리할 수 있게된다. 또한 ggplot2같은 패키지에서는 변수와 그래프의 에스테틱을 맵핑시키는 과정에서 이러한 tidy data의 강점을 최대한 활용한다. 또, formula를 사용하게 되는 경우라면 하나의 변수가 하나의 열을 구성하는 것이 필수적이다

세 번째로는 데이터 구조가 표준화된다는 점이 있다. 어떤 함수가 tidy data를 요구한다는 것을 알면 우리는 해당 데이터를 data.frame으로 구성해서 각 변수가 각각의 열을 구성하도록 하면 된다. 그리고 이렇게 tidy data를 한 번 구성하고 나면 tidy data를 사용하는 다른 패키지 및 함수에 바로 적용할 수도 있게 된다. ggplot2에서는 이러한 성질을 이용해서 다양한 시각화를 통일된 문법으로 시도할 수 있도록 기능을 지원한다


<br / >

Tidying messy datasets

tidy-data 문서를 보면 지저분한 데이터의 대표적인 사례로 다섯 가지 경우를 들고 있다

  • 열 이름에 변수 이름이 아니라 값이 들어가있는 경우

  • 하나의 열에 여러 개의 변수가 들어가있는 경우

  • 변수가 행, 열 모두에 들어간 경우

  • 하나의 테이블에 여러 가지 종류의 관측대상이 포함된 경우

  • 하나의 관측 대상이 여러 개의 테이블에 포함된 경우


각각의 경우에 대한 설명은 vignette에서 확인할 수 있으니 여기에서는 넘어가려고 한다. 결국 우리가 원하는 것은 지저분한 데이터를 단정하게 만드는 것이다. 글의 초반에 나와있는 문구처럼 지저분한 데이터는 그 종류를 여러가지로 나눠볼 수 있겠지만 단정하게 만들면 데이터의 형태가 비슷한 형태로 종합된다


reshape2

tidyr보다 이전에 등장하여 많이 쓰였던 패키지이다. tidyr패키지가 data.frame에 특화되어있기 때문에 data.frame에 대한 작업이 아니라면 여전히 reshape2 패키지를 사용하게 된다

기본적으로는 meltcast라는 두 가지 함수로 구성된다. melt는 messy data(또는 wide format)를 tidy한 형태(또는 long format)로 변형시켜준다

cast는 tidy한 형태의 데이터를 contingency table이나 엑셀의 피벗테이블처럼 행과 열에 특정 변수를 지정하여 데이터를 요약해준다. 결과물의 형태에 따라 acastdcast로 구분된다.

dcast는 data.frame의 형태로 결과물을 내보내고, acast는 vector / matrix / array의 형태로 결과물을 만들어낸다

phone_melt = melt(WorldPhones, value.name = 'number')
head(phone_melt)
##   Var1   Var2 number
## 1 1951 N.Amer  45939
## 2 1956 N.Amer  60423
## 3 1957 N.Amer  64721
## 4 1958 N.Amer  68484
## 5 1959 N.Amer  71799
## 6 1960 N.Amer  76036


dcast는 formula 형태로 row와 col에 들어갈 변수를 지정할 수 있다

y~x의 형태로 사용하기 때문에 row에 들어갈 변수는 x에, col에 들어갈 변수는 y에 지정한다

value.var에는 value값으로 들어갈 변수를 지정할 수 있다.

지정하지 않으면 적당한 변수를 넣지만 경고메세지가 발생한다

dcast(phone_melt, Var1~Var2, value.var = 'number')
##   Var1 N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1 1951  45939  21574 2876   1815    1646     89      555
## 2 1956  60423  29990 4708   2568    2366   1411      733
## 3 1957  64721  32510 5230   2695    2526   1546      773
## 4 1958  68484  35218 6662   2845    2691   1663      836
## 5 1959  71799  37598 6856   3000    2868   1769      911
## 6 1960  76036  40341 8220   3145    3054   1905     1008
## 7 1961  79831  43173 9053   3338    3224   2005     1076

같은 내용을 acast로 실행하면 matrix로 데이터가 출력된다

acast(phone_melt, Var1~Var2, value.var = 'number')
##      N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1951  45939  21574 2876   1815    1646     89      555
## 1956  60423  29990 4708   2568    2366   1411      733
## 1957  64721  32510 5230   2695    2526   1546      773
## 1958  68484  35218 6662   2845    2691   1663      836
## 1959  71799  37598 6856   3000    2868   1769      911
## 1960  76036  40341 8220   3145    3054   1905     1008
## 1961  79831  43173 9053   3338    3224   2005     1076
# matrix
class(acast(phone_melt, Var1~Var2, value.var = 'number'))
## [1] "matrix"


reshape2가 tidyr과 다른 점은 matrix에 대해서도 바로 적용할 수 있다는 것이다

matrix형태의 데이터를 이용해 ggplot2로 그래프를 그린다고 하면 reshape2를 사용해 data.frame으로 변경하면 된다

str(volcano)
##  num [1:87, 1:61] 100 101 102 103 104 105 105 106 107 108 ...
volcano_melt = melt(volcano)
head(volcano_melt)
##   Var1 Var2 value
## 1    1    1   100
## 2    2    1   101
## 3    3    1   102
## 4    4    1   103
## 5    5    1   104
## 6    6    1   105


tidyr

tidyr 패키지는 data.frame에만 적용할 수 있는 패키지이다. pipe 연산자(%>%)를 이용한 작업을 쉽게 할 수 있도록 구성되어있다. pipe에 대한 자세한 내용은 이 곳 에서 확인할 수 있다

위에서 만들었던 WorldPhones_df 를 tidy한 형태로 변형시키려고 한다

WorldPhones_df
##   year N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1 1951  45939  21574 2876   1815    1646     89      555
## 2 1956  60423  29990 4708   2568    2366   1411      733
## 3 1957  64721  32510 5230   2695    2526   1546      773
## 4 1958  68484  35218 6662   2845    2691   1663      836
## 5 1959  71799  37598 6856   3000    2868   1769      911
## 6 1960  76036  40341 8220   3145    3054   1905     1008
## 7 1961  79831  43173 9053   3338    3224   2005     1076


gather는 reshape2의 melt와 비슷한 기능을 한다.

gather함수는 기본적으로 gather(data, key, value, column_list)로 구성된다.

data는 말 그대로 대상이 되는 data.frame을 의미하고 key는 key column의 이름, value는 value column의 이름을 입력한다. column_list에는 key와 value로 구분할 열의 이름들을 지정하면 된다. dplyrselect 항목에서 사용할 수 있는 열 선택 방법과 동일하게 사용할 수 있다. N.Amer 부터 Mid.Amer까지의 열을 선택할 것이니 N.Amer:Mid.Amer로 지정하면 된다

이 중에서 첫 번째 열은 year는 놔두고, N.Amer부터 Mid.Amer까지를 key와 value로 분리시킨다

key column의 이름은 area, value column의 이름은 number로 지정한다

phone_gather = gather(WorldPhones_df, area, number, N.Amer:Mid.Amer)
head(phone_gather)
##   year   area number
## 1 1951 N.Amer  45939
## 2 1956 N.Amer  60423
## 3 1957 N.Amer  64721
## 4 1958 N.Amer  68484
## 5 1959 N.Amer  71799
## 6 1960 N.Amer  76036

spread 함수는 reshape2의 cast계열 함수와 비슷한 기능을 한다

spread(data, key, value)의 형태로 사용되는데 열별로 정리하려고하는 변수를 key에 지정하고, 값으로 정리해야하는 변수는 value에 넣는다.

spread함수를 이용해서 gather를 사용하기 전의 원래 형태로 돌리려면 다음과 같이 할 수 있다

spread(phone_gather, area, number)
##   year N.Amer Europe Asia S.Amer Oceania Africa Mid.Amer
## 1 1951  45939  21574 2876   1815    1646     89      555
## 2 1956  60423  29990 4708   2568    2366   1411      733
## 3 1957  64721  32510 5230   2695    2526   1546      773
## 4 1958  68484  35218 6662   2845    2691   1663      836
## 5 1959  71799  37598 6856   3000    2868   1769      911
## 6 1960  76036  40341 8220   3145    3054   1905     1008
## 7 1961  79831  43173 9053   3338    3224   2005     1076


만약 NA값이 발생할 경우 fill 옵션을 통해서 NA값을 임의의 값으로 채워넣을 수 있다

na_sample = phone_gather %>% sample_n(10)

아래와 같이 NA가 잔뜩 발생하는 경우

spread(na_sample, area, number)
##   year N.Amer Europe Asia S.Amer Oceania
## 1 1951  45939     NA 2876   1815      NA
## 2 1957     NA  32510   NA     NA      NA
## 3 1958  68484  35218   NA     NA      NA
## 4 1959     NA  37598 6856     NA      NA
## 5 1961  79831     NA   NA     NA    3224

fill 옵션에 0을 지정하면 NA값 대신에 0으로 빈칸을 채운다

spread(na_sample, area, number, fill = 0)
##   year N.Amer Europe Asia S.Amer Oceania
## 1 1951  45939      0 2876   1815       0
## 2 1957      0  32510    0      0       0
## 3 1958  68484  35218    0      0       0
## 4 1959      0  37598 6856      0       0
## 5 1961  79831      0    0      0    3224

좀 더 상세한 NA처리를 위해서는 replace_nafill이라는 함수를 제공한다

http://lumiamitie.github.io/r/replace_na_with_tidyr/ 에 정리한 내용을 참고하면 된다

Comments