교육 과정의 다른 부분에서는 서로 다른 특징을 지닌 여러 데이터 타입을 살펴봤어요.
이번 개념에서는 다차원이면서 서로 다른 타입인 데이터를 저장하는 방법을 살펴봐요. 실제로 대부분의 실무 데이터가 이렇기 때문에, 이제 R이 (주로) 실무에서 어떻게 쓰이는지 그 핵심으로 들어가는 셈이에요.
수십 년에 걸쳐 R은 표 형식 데이터를 다루기 위한 여러 데이터 타입을 추가해 왔어요.
이 교육 과정에서는 주로 tibble에 초점을 맞추지만, 몇 가지 대안을 알아 두면 유용해요.
data.tabletibble(아래에서 설명해요)과 data.table은 모두 Base R의 전통적인 data.frame을 개선하려는 높이 평가받는 시도예요.
어느 쪽이 "더 나은지"에 대해서는 당연히도 많은 논쟁이 있어요.
아마 다음 몇 가지 점에 대해서는 어느 정도 합의가 있을 거예요(단순하다는 비판을 받겠지만요).
tibble은 주로 사용 편의성과 Tidyverse 생태계와의 통합에 최적화되어 있어요.data.table은 주로 순수한 성능과 확장성, 특히 아주 큰 데이터셋을 다룰 때에 최적화되어 있어요.어쨌든 data.table은 Exercism에서는 사용할 수 없어서, 여기서는 완전성을 위해 언급만 해 둘게요.
data.frameBase R에서 data.frame은 길이가 같은 vectors들로 이루어진 list예요.
이것은 데이터를 담은 직사각형 표라고 생각할 수 있는데, 각 열은 타입이 균일하지만 각 행은 서로 다른 타입의 데이터를 담을 수 있고 보통 그렇게 해요.
이를 보여 주는 예시를 볼까요:
# create the column vectors
languages <- c("Fortran", "R", "Python", "Julia")
created <- c(1957, 1993, 1991, 2012)
has.syllabus <- c(FALSE, TRUE, TRUE, TRUE)
# join columns to create the dataframe
df <- data.frame(languages, created, has.syllabus)
df
#> languages created has.syllabus
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
# look at the structure
str(df)
#> 'data.frame': 4 obs. of 3 variables:
#> $ languages : chr "Fortran" "R" "Python" "Julia"
#> $ created : num 1957 1993 1991 2012
#> $ has.syllabus: logi FALSE TRUE TRUE TRUE
문자형 문자열 열 하나, 숫자 열 하나, 불리언 열 하나가 있어요. 규모가 커지면, 이는 많은 실무 데이터 모음을 표현하는 직관적인 방법이에요.
tibbledata.frame의 설계는 오래됐어요.
수십 년에 걸친 경험과 R이 사용되는 방식의 변화가 재설계로 이어져, Tidyverse에 현대화된 대안인 tibble이 탄생했어요.
Base R과 비교했을 때 tibble에는 이런 특징이 있어요:
요컨대 tibble은 "더 적게 하고 더 많이 불평하기"를 목표로 하는데, "게으르고 퉁명스러운"이라고도 표현돼요.
두 타입은 보통 서로 바꿔 쓸 수 있어요: data.frame을 받는 함수는 tibble도 받아들이고, 반대의 경우도 마찬가지예요.
as_tibble(df)와 as.data.frame(tbl)를 쓰면 두 타입 사이의 변환은 쉬워요.
새로운 작업에는 tibble을 쓰는 편이 더 견고한 code를 만드는 데 도움이 될 거예요.
하지만 R 세계에는 레거시 code와 레거시 데이터가 아주 많아서, data.frame은 앞으로도 오랫동안 흔하게 쓰일 가능성이 커요.
# column vectors are same as for data.frame
library(tibble)
tbl <- tibble(languages, created, has.syllabus)
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
str(tbl)
#> tibble [4 × 3] (S3: tbl_df/tbl/data.frame)
#> $ languages : chr [1:4] "Fortran" "R" "Python" "Julia"
#> $ created : num [1:4] 1957 1993 1991 2012
#> $ has.syllabus: logi [1:4] FALSE TRUE TRUE TRUE
기본 출력 형식을 눈여겨보세요: 차원을 나타내는 주석 줄이 자동으로 출력되고, 열 타입도 함께 표시돼요.
tibble은 Tidyverse의 핵심 요소라서, library(tibble)이나 library(tidyverse)로 불러와요.
문서도 Tidyverse 스타일로 꽤 자세하게 마련되어 있어요:
가장 간단하게는, data.frame()에서처럼 tibble() 함수로 열 벡터를 결합할 수 있어요.
이에 대한 예시는 이전 섹션에서 보여 드렸어요.
값을 행 단위로 입력하는 편이 더 편하다면, 그에 해당하는 함수는 tribble()이에요.
tbl_r <- tribble(
# column names marked with tilde prefix
~languages, ~created, ~has.syllabus,
"Fortran", 1957, FALSE,
"R", 1993, TRUE,
"Python", 1991, TRUE,
"Julia", 2012, TRUE
)
tbl_r
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
실제로 tibble을 만드는 방법은 수십 가지가 있는데, 다양한 Tidyverse 함수들이 기본 출력 형식으로 tibble을 내놓기 때문이에요. 이 내용은 이후 개념에서 다시 다룰 거예요.
함수형 프로그래밍 개념에서는 벡터와 리스트(1차원 데이터 구조)를 조작하는 purrr 라이브러리를 다뤘어요.
데이터프레임(전통적인 것이든 tibble이든)에는 그에 해당하는 dplyr 라이브러리를 사용해요.
dplyr는 앞서 Switch 개념에서 소개했어요.
그때는 몇 가지 유틸리티 함수만 썼지만, 이제 이 큰 라이브러리의 나머지 부분을 살펴볼 수 있어요.
tibble을 포함한 데이터프레임은 열 벡터의 리스트로 취급할 수 있어서, 리스트 인덱싱으로 특정 열을 꺼낼 수 있어요.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl$created
#> [1] 1957 1993 1991 2012
데이터프레임은 행렬 방식의 인덱싱으로도 접근할 수 있어요.
tbl[c(2, 4), 1:2]
# A tibble: 2 × 2
#> languages created
#> <chr> <dbl>
#> 1 R 1993
#> 2 Julia 2012
Tidyverse 생태계와 함께하는 현대적인 R에서는 dplyr 함수가 대체로 더 유연하고 편리하며, 이 개념의 나머지 부분에서도 여기에 초점을 맞출 거예요.
pull()로 열 이름이나 순번(오른쪽에서 왼쪽으로 세려면 음수)을 지정해 열 하나를 가져와요.
tbl |> pull(created)
#> [1] 1957 1993 1991 2012
tbl$created와 같은 결과지만, 파이프라인에 잘 어울리는 함수를 사용한 거예요.
여러 열을 가져오려면 select() 함수가 적절한데, 아주 다재다능해요.
이름이나 타입의 특성에 따라 열을 가져오거나 제외할 수 있어요.
# Range with position and/or name
tbl |> select(1:created)
# A tibble: 4 × 2
#> languages created
#> <chr> <dbl>
#> 1 Fortran 1957
#> 2 R 1993
#> 3 Python 1991
#> 4 Julia 2012
# Exclude a column
tbl |> select(!created)
# A tibble: 4 × 2
#> languages has.syllabus
#> <chr> <lgl>
#> 1 Fortran FALSE
#> 2 R TRUE
#> 3 Python TRUE
#> 4 Julia TRUE
# Use type of column
tbl |> select(where(is.numeric))
# A tibble: 4 × 1
#> created
#> <dbl>
#> 1 1957
#> 2 1993
#> 3 1991
#> 4 2012
불리언 연산자 &, |, !(and, or, not)를 사용하면 여러 조건을 지정할 수 있어요.
_유효한 R 식별자_인 열 이름은 select() 안에서 따옴표가 필요 없어요.
유효하지 않은 이름(예를 들어 공백이 포함된 이름)은 백틱으로 감쌀 수 있지만, 이름을 바꾸는 편이 더 나을 수도 있어요.
select() 함수는 열 이름을 고르는 여러 도우미 함수와 함께 쓸 수 있어요: starts_with, contains, num_range 등등이요.
matches는 완전한 정규 표현식 매칭을 지원해요.
자세한 내용은 문서에서 확인해 보세요.
이런 강력함도 언어만 담긴 장난감 데이터프레임을 다룰 때는 좀 우스워 보여요.
다행히 dplyr에는 starwars tibble이 들어 있어서, 연습해 볼 더 큰 데이터가 있어요.
# limit display to top 3 rows of non-list columns
starwars |>
select(!where(is.list)) |>
head(3)
# A tibble: 3 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
# pick a subset of columns
starwars |>
select(name | ends_with("color")) |>
head(5)
# A tibble: 5 × 4
#> name hair_color skin_color eye_color
#> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker blond fair blue
#> 2 C-3PO NA gold yellow
#> 3 R2-D2 NA white, blue red
#> 4 Darth Vader none white yellow
#> 5 Leia Organa brown light brown
분명히 dplyr는 이름으로 열을 선택하는 강력한 방법을 제공해요.
행 이름으로도 비슷한 일을 할 수 있을까요?
아니요!
전통적인 R 데이터프레임은 행 이름을 가질 수 있지만, (bug와 성능 문제의 역사를 겪은 뒤) tibbles에서는 행 이름이 허용되지 않아요.
이름을 쓰고 싶다면 <chr> 열(보통 첫 번째 열)에 넣고, 다른 열과 똑같이 사용하세요.
as_tibble() 같은 가져오기 함수는 이름 있는 행이 있는 데이터를 가져올 때 이 열을 자동으로 만들어 줘요.
이 행 이름 제한이 이상하게 제한적으로 느껴진다면, 대부분의 대형 데이터베이스 시스템도 테이블을 같은 방식으로 다룬다는 점을 기억하세요: Oracle, SQL Server, PostgreSQL, MySQL...
filter()로 어떤 조건에 맞는 행을 가져오거나, filter_out()으로 그런 행을 제외해요.
starwars |>
select(name:mass) |>
filter(between(height, 150, 165) & !is.na(mass))
# A tibble: 4 × 3
#> name height mass
#> <chr> <int> <dbl>
#> 1 Leia Organa 150 49
#> 2 Beru Whitesun Lars 165 75
#> 3 Nien Nunb 160 68
#> 4 Ben Quadinaros 163 65
필터 조건은 얼마든지 복잡해질 수 있지만, 언제나 행의 _내용_을 기준으로 해요.
행 _번호_를 알고 있다면, 다양한 slice() 함수를 사용할 수 있어요.
starwars |>
select(name | homeworld) |>
slice(20:25)
# A tibble: 6 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Palpatine Naboo
#> 2 Boba Fett Kamino
#> 3 IG-88 NA
#> 4 Bossk Trandosha
#> 5 Lando Calrissian Socorro
#> 6 Lobot Bespin
# random sample of rows
starwars |>
select(name | homeworld) |>
slice_sample(n = 4)
# A tibble: 4 × 2
#> name homeworld
#> <chr> <chr>
#> 1 Shaak Ti Shili
#> 2 Luminara Unduli Mirial
#> 3 Grievous Kalee
#> 4 Palpatine Naboo
중복된 행을 제거하려면 distinct()를 사용해요.
먼저 주의할 점: copy-on-modify 기본 동작 덕분에 원본 tibble은 보통 그대로 유지돼요.
대부분의 수정은 열 단위로 적용돼요.
열 이름은 rename(newname = oldname)로 바꿀 수 있고, 함수를 적용하려면 rename_with()를 써요.
전형적인 용례는 가져온 이름에서 공백을 없애고 관련된 이름들을 일관된 형식으로 맞춰, R에서 다루기 쉽게 정리하는 거예요.
rename() 안의 문법을 눈여겨보세요.
oldname 열의 _내용_이 newname이라는 이름에 묶이기 때문에 이런 순서가 돼요.
열 순서는 relocate()로 바꿀 수 있어요.
지정한 열은 기본적으로 가장 왼쪽 위치로 이동하지만, 더 세밀하게 위치를 잡으려면 .before나 .after 인자를 사용할 수 있어요.
sw <- starwars |> select(name:species) |> slice(1:4)
sw
# A tibble: 4 × 11
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr>
#> 1 Luke Skywalker 172 77 blond fair blue 19 male masculine Tatooine Human
#> 2 C-3PO 167 75 NA gold yellow 112 none masculine Tatooine Droid
#> 3 R2-D2 96 32 NA white, blue red 33 none masculine Naboo Droid
#> 4 Darth Vader 202 136 none white yellow 41.9 male masculine Tatooine Human
sw |> relocate(c(species, homeworld), .after = name)
# A tibble: 4 × 11
#> name species homeworld height mass hair_color skin_color eye_color birth_year sex gender
#> <chr> <chr> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr>
#> 1 Luke Skywalker Human Tatooine 172 77 blond fair blue 19 male masculine
#> 2 C-3PO Droid Tatooine 167 75 NA gold yellow 112 none masculine
#> 3 R2-D2 Droid Naboo 96 32 NA white, blue red 33 none masculine
#> 4 Darth Vader Human Tatooine 202 136 none white yellow 41.9 male masculine
더 큰 변경을 위해서는 mutate()로 이런 일을 할 수 있어요:
NULL로 설정해 열 삭제하기분명히 mutate()는 강력하고, 헷갈릴 수도 있으며, copy-on-modify에 아주 감사하게 만드는 기능이에요.
스타워즈 등장인물의 체질량 지수를 굳이 신경 쓸 이유는 딱히 없지만, 혹시나 해서요:
starwars |>
select(c(name, species, height, mass)) |>
mutate(BMI = mass / (height / 100)^2) |>
head(4)
# A tibble: 4 × 5
#> name species height mass BMI
#> <chr> <chr> <int> <dbl> <dbl>
#> 1 Luke Skywalker Human 172 77 26.0
#> 2 C-3PO Droid 167 75 26.9
#> 3 R2-D2 Droid 96 32 34.7
#> 4 Darth Vader Human 202 136 33.3
mutate() 같은 함수로 열의 일부에만 작업하고 싶을 때, 위 예시의 select() |> mutate() 순서가 한 가지 방법이에요.
결과에는 선택한 열만 남아요.
또는, mutate() 호출 안에서 pick()을 쓰는 것이 편리할 수도 있어요:
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
When you want to operate on a subset of the columns with functions such as `mutate()`, the `select() |> mutate()` sequence in the above example is one option.
Only the selected columns will be in the result.
Alternatively, it can be convenient to use `pick()` _within_ the `mutate()` call:
```R
starwars |> mutate(pick(c(name, species, height, mass)), BMI = mass / (height / 100)^2) |> head(4)
# A tibble: 4 × 15
#> name height mass hair_color skin_color eye_color birth_year sex gender homeworld species films vehicles
#> <chr> <int> <dbl> <chr> <chr> <chr> <dbl> <chr> <chr> <chr> <chr> <lis> <list>
#> 1 Luke Skywal… 172 77 blond fair blue 19 male mascu… Tatooine Human <chr> <chr>
#> 2 C-3PO 167 75 NA gold yellow 112 none mascu… Tatooine Droid <chr> <chr>
#> 3 R2-D2 96 32 NA white, bl… red 33 none mascu… Naboo Droid <chr> <chr>
#> 4 Darth Vader 202 136 none white yellow 41.9 male mascu… Tatooine Human <chr> <chr>
# ℹ 2 more variables: starships <list>, BMI <dbl>
변형에는 pick한 열만 사용되지만, 모든 열이 반환돼요.
행 단위 연산은 tibble 하나를 수정할 때는 덜 흔해요(여러 tibble을 병합하는 방법은 이후 개념에서 다룰 거예요).
한 가지 예외는 arrange()로, 하나 이상의 열에 담긴 값에 따라 행을 정렬해요.
tbl
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 R 1993 TRUE
#> 3 Python 1991 TRUE
#> 4 Julia 2012 TRUE
tbl |> arrange(languages)
# A tibble: 4 × 3
#> languages created has.syllabus
#> <chr> <dbl> <lgl>
#> 1 Fortran 1957 FALSE
#> 2 Julia 2012 TRUE
#> 3 Python 1991 TRUE
#> 4 R 1993 TRUE
전통적인 것이든 tibble이든, 데이터프레임은 현대 R이 일반적으로 사용되는 방식의 중심에 있어요.
대부분의 Tidyverse 함수(dplyr만이 아니라)는 tibble을 입력으로 받거나 출력으로 만들어요.
이 개념은 짧게 소개만 했을 뿐이라, 가능한 것들의 표면만 살짝 긁은 셈이에요.
이후 개념에서는 데이터프레임의 여러 다른 측면을 다룰 거예요(Exercism의 기술적 제약 안에서요).