Tidy data
많은 패키지들, 특히 ggplot2는 특정한 형태로 정리된 데이터를 요구한다. 처음에 익숙하지 않을 때는 함수가 요구하는 데이터의 형태를 맞추기도 버거웠지만 차츰 익숙해지면서 깔끔하게 정리된 데이터의 중요성을 깨닫게 된다. 깔끔한 데이터를 만드는 과정 중에서 wide format과 long format 사이의 변환은 방법을 모르면 헤매거나 크게 돌아갈 수도 있기 때문에 여기서 가장 대표적인 패키지, tidyr
과 reshape2
의 기본적인 사용법을 정리해보려고 한다.
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 패키지를 사용하게 된다
기본적으로는 melt
와 cast
라는 두 가지 함수로 구성된다. melt
는 messy data(또는 wide format)를 tidy한 형태(또는 long format)로 변형시켜준다
cast
는 tidy한 형태의 데이터를 contingency table이나 엑셀의 피벗테이블처럼 행과 열에 특정 변수를 지정하여 데이터를 요약해준다. 결과물의 형태에 따라 acast
와 dcast
로 구분된다.
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로 구분할 열의 이름들을 지정하면 된다. dplyr
의 select
항목에서 사용할 수 있는 열 선택 방법과 동일하게 사용할 수 있다. 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_na
와 fill
이라는 함수를 제공한다
http://lumiamitie.github.io/r/replace_na_with_tidyr/ 에 정리한 내용을 참고하면 된다