트랙
/
R
R
/
연습 문제
/
나무와 tibble
나무와 tibble

나무와 tibble

학습 연습 문제

소개

교육 과정의 다른 부분에서는 서로 다른 특징을 가진 여러 가지 데이터 타입을 살펴봤어요.

  • 원자 벡터는 1차원이고 타입이 모두 같아요.
  • 리스트는 1차원이고, 원소의 타입은 서로 달라도 돼요.
  • 행렬과 배열은 다차원이고 타입이 모두 같아요.

이번 개념에서는 다차원이면서 타입이 섞인 데이터를 저장하는 방법을 살펴봐요. 실제로 _대부분_의 현실 데이터가 이렇기 때문에, 이제 R이 (주로) 실무에서 어떻게 쓰이는지 그 핵심으로 들어가 봐요.

데이터프레임의 종류

수십 년에 걸쳐 R은 표 형식 데이터를 다루기 위한 여러 데이터 타입을 추가해 왔어요.

이 교육 과정에서는 주로 tibble에 초점을 맞추지만, 다른 대안들도 알아 두면 유용해요.

data.frame

Base 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

문자열 열 하나, 숫자 열 하나, 불리언 열 하나가 있어요. 규모를 키우면, 이런 방식이 현실 세계의 다양한 데이터 모음을 표현하는 직관적인 방법이 돼요.

tibble

data.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 다루기

tibble은 Tidyverse의 핵심 요소라서, library(tibble)이나 library(tidyverse)로 불러와요.

tibble 만들기

가장 간단하게는 data.frame()에서처럼 tibble() 함수로 열 벡터들을 이어 붙일 수 있어요. 이에 대한 예시는 앞부분에서 살펴봤어요.

값을 행 단위로 입력하는 편이 더 편하다면, 그에 해당하는 함수는 tribble()이에요.

실제로 tibble을 만드는 방법은 아주 많아요. 다양한 Tidyverse 함수들이 tibble을 기본 출력 형식으로 내놓기 때문이에요.

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 수정하기

먼저 주의할 점이 있어요. _수정 시 복사_가 기본이라 원래 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     

지침

규모가 큰 체리 과수원에는 보통 목재로 내주고 싶은 여분의 나무가 조금 있어요. 이렇게 하면 과수원을 건강하게 유지하는 동시에 추가 수익도 얻을 수 있어요. 과수원은 잠재 고객이 전화했을 때 나무를 고르는 과정에 도움이 될 기능을 코딩해 달라고 부탁했어요.

Note

내장 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로 다루기로 선택하고 싶을 수도 있어요.

1. 열 이름 바꾸기

안타깝게도 한 번도 고쳐지지 않은 사무상의 착오가 있어서, 데이터셋의 한 열이 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

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

3. 과수원용 데이터셋 사본

과수원은 잠재 고객마다 판매에 도움이 되도록 데이터셋의 특별한 사본을 만들어 둬요. 이 데이터셋은 가장 관련 있는 열인 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

4. 고객용 데이터셋 사본

잠재 고객은 각자의 선호에 따라 맞춤형 데이터셋을 받게 돼요. 이를 위해 고객은 나무 한 그루의 최소 높이, 최대 높이, 최대 무게를 지정할 수 있어요.

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
GitHub에서 편집 링크가 새 창이나 탭에서 열려요
R Exercism

나무와 tibble 문제를 시작해 볼 준비가 됐나요?

Exercism에 가입하고 R 트랙을 개념 21개연습 문제 111개, 그리고 실제 사람의 멘토링과 함께 배우고 익혀 보세요. 모두 무료예요.