교육 과정의 다른 부분에서는 서로 다른 특징을 가진 여러 가지 데이터 타입을 살펴봤어요.
이번 개념에서는 다차원이면서 타입이 섞인 데이터를 저장하는 방법을 살펴봐요. 실제로 _대부분_의 현실 데이터가 이렇기 때문에, 이제 R이 (주로) 실무에서 어떻게 쓰이는지 그 핵심으로 들어가 봐요.
수십 년에 걸쳐 R은 표 형식 데이터를 다루기 위한 여러 데이터 타입을 추가해 왔어요.
이 교육 과정에서는 주로 tibble에 초점을 맞추지만, 다른 대안들도 알아 두면 유용해요.
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
문자열 열 하나, 숫자 열 하나, 불리언 열 하나가 있어요. 규모를 키우면, 이런 방식이 현실 세계의 다양한 데이터 모음을 표현하는 직관적인 방법이 돼요.
tibbledata.frame의 설계는 오래되었어요.
수십 년간 쌓인 경험과 R이 쓰이는 방식의 변화가 재설계로 이어졌고, Tidyverse에는 tibble이라는 현대적인 대안이 탄생했어요.
Base R과 비교하면 tibble은 다음과 같아요.
요약하면 tibble은 "덜 하고 더 투덜거리기"를 목표로 해요. "게으르고 심술궂다"고도 표현해요.
두 타입은 보통 서로 바꿔 쓸 수 있어요. data.frame을 받는 함수는 tibble도 받아들이고, _그 반대도 마찬가지_예요.
타입 사이의 변환은 쉬워요. as_tibble(df)와 as.data.frame(tbl)을 쓰면 돼요.
새로 작업할 때는 tibble을 쓰면 더 견고한 코드를 만드는 데 도움이 될 거예요.
하지만 R 생태계에는 예전 코드와 예전 데이터가 아주 많기 때문에, 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
기본 출력 형식을 눈여겨봐요. 차원을 나타내는 주석 줄이 자동으로 출력되고, 열 타입도 함께 표시돼요.
tibble은 Tidyverse의 핵심 요소라서, library(tibble)이나 library(tidyverse)로 불러와요.
가장 간단하게는 data.frame()에서처럼 tibble() 함수로 열 벡터들을 이어 붙일 수 있어요.
이에 대한 예시는 앞부분에서 살펴봤어요.
값을 행 단위로 입력하는 편이 더 편하다면, 그에 해당하는 함수는 tribble()이에요.
실제로 tibble을 만드는 방법은 아주 많아요. 다양한 Tidyverse 함수들이 tibble을 기본 출력 형식으로 내놓기 때문이에요.
함수형 프로그래밍 개념에서는 벡터와 리스트(1차원 데이터 구조)를 다루는 purrr 라이브러리를 이야기했어요.
데이터프레임(전통적인 data.frame이든 tibble이든)을 다룰 때는 dplyr 라이브러리를 사용해요.
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
# 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)를 써서 여러 조건을 함께 지정할 수 있어요.
이런 강력함도 언어만 담긴 장난감 데이터프레임에서는 좀 우스워 보여요.
다행히 dplyr에는 starwars tibble이 들어 있어서, 연습해 볼 더 큰 데이터가 있어요.
# 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
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
먼저 주의할 점이 있어요. _수정 시 복사_가 기본이라 원래 tibble은 보통 그대로 남아 있어요.
대부분의 수정은 열 단위로 이루어져요.
열 이름은 rename(newname = oldname)으로 바꾸거나, rename_with()로 함수를 적용해서 바꿀 수 있어요.
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()는 강력하지만 헷갈릴 수도 있고, 수정 시 복사가 얼마나 고마운지 느끼게 해 주는 기능이에요.
스타워즈 등장인물의 체질량지수까지 신경 쓸 이유는 없지만, 혹시 몰라서 한번 해봐요.
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
행 단위 연산은 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
규모가 큰 체리 과수원에는 보통 목재로 내주고 싶은 여분의 나무가 조금 있어요. 이렇게 하면 과수원을 건강하게 유지하는 동시에 추가 수익도 얻을 수 있어요. 과수원은 잠재 고객이 전화했을 때 나무를 고르는 과정에 도움이 될 기능을 코딩해 달라고 부탁했어요.
내장 datasets 라이브러리에는 여러 실습용 데이터셋이 들어 있고, 이 연습 문제에서는 trees 데이터셋을 사용해요.
데이터셋을 불러오려면 그냥 trees라는 이름을 사용하면 돼요.
library(datasets) # loading is optional
trees |> head(3)
#> A data.frame: 3 × 3
#> Girth Height Volume
#> <dbl> <dbl> <dbl>
#> 1 8.3 70 10.3
#> 2 8.6 65 10.3
#> 3 8.8 63 10.2
Girth, Height, Volume 세 개의 열이 있다는 점에 주목해 주세요.
데이터셋은 처음에는 data.frame 형태이지만, tibble로 다루기로 선택하고 싶을 수도 있어요.
안타깝게도 한 번도 고쳐지지 않은 사무상의 착오가 있어서, 데이터셋의 한 열이 Girth라는 이름을 달고 있지만 사실 이 열은 Diameter예요.
trees의 Girth 열 이름을 Diameter로 바로잡은 tree_data라는 data.frame/tibble을 정의해요.
head(tree_data, 3)
#> A tibble: 3 × 3
#> Diameter Height Volume
#> <dbl> <dbl> <dbl>
#> 8.3 70 10.3
#> 8.6 65 10.3
#> 8.8 63 10.2
둘레(즉, 원주)는 여전히 유용한 측정값이고, 목재의 무게도 마찬가지예요.
둘레는 πD로 간단히 계산할 수 있는데, 여기서 D는 지름이에요.
목재 전체의 무게는 ρV로 계산할 수 있는데, 여기서 ρ = 35는 체리 나무의 밀도이고 V는 목재의 부피예요.
데이터프레임과 숫자 인자를 받는 girth_n_weight(data, rnd_digits) 함수를 정의해요.
이 함수는 Girth와 Weight 두 열을 추가한 새 데이터프레임을 반환해야 하고, 값은 rnd_digits로 주어진 자릿수에 맞게 반올림해야 해요.
girth_n_weight(tree_data, 1) |> head(3)
#> A tibble: 3 × 5
#> Diameter Height Volume Girth Weight
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 8.3 70 10.3 26.1 360.5
#> 8.6 65 10.3 27.0 360.5
#> 8.8 63 10.2 27.6 357.0
과수원은 잠재 고객마다 판매에 도움이 되도록 데이터셋의 특별한 사본을 만들어 둬요.
이 데이터셋은 가장 관련 있는 열인 Weight와 Height를 맨 앞으로 옮기고, Weight를 기준으로 정렬한 형태예요.
데이터프레임을 입력으로 받는 orchard_copy(data, important_cols) 함수를 정의해요.
이 함수는 Weight와 Height를 맨 앞으로 옮기고 행을 Weight 기준으로 정렬한 새 데이터프레임을 반환해야 해요.
tree_data |> girth_n_weight() |> orchard_copy() |> head(3)
#> A tibble: 3 × 5
#> Weight Height Diameter Volume Girth
#> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 357 63 9 10 28
#> 360 70 8 10 26
#> 360 65 9 10 27
잠재 고객은 각자의 선호에 따라 맞춤형 데이터셋을 받게 돼요. 이를 위해 고객은 나무 한 그루의 최소 높이, 최대 높이, 최대 무게를 지정할 수 있어요.
customer_copy(data, min_height, max_height, max_weight) 함수를 정의해요.
Height, Weight, Diameter, Girth 열을 가지면서 조건을 벗어나지 않는 행만 담은 고객용 데이터프레임 사본을 반환해요.
tree_data |>
girth_n_weight(1) |>
orchard_copy() |>
customer_copy(65, 75, 1500) |>
head(3)
#> A tibble: 3 × 4
#> Height Weight Diameter Girth
#> <dbl> <dbl> <dbl> <dbl>
#> 70 360.5 8.3 26.1
#> 65 360.5 8.6 27.0
#> 66 546.0 11.0 34.6
Exercism에 가입하고 R 트랙을 개념 21개연습 문제 111개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.