트랙
/
R
R
/
학습 과정
/
데이터프레임
데이

데이터프레임 에서 R

1개의 연습 문제

데이터프레임 소개

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

  • 원자 벡터는 1차원이고 타입이 균일해요.
  • 리스트는 1차원이고 원소가 서로 다른 타입일 수 있어요.
  • 행렬과 배열은 다차원이고 타입이 균일해요.

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

데이터프레임의 종류

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

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

data.table

tibble(아래에서 설명해요)과 data.table은 모두 Base R의 전통적인 data.frame을 개선하려는 높이 평가받는 시도예요. 어느 쪽이 "더 나은지"에 대해서는 당연히도 많은 논쟁이 있어요. 아마 다음 몇 가지 점에 대해서는 어느 정도 합의가 있을 거예요(단순하다는 비판을 받겠지만요).

  • tibble은 주로 사용 편의성과 Tidyverse 생태계와의 통합에 최적화되어 있어요.
  • data.table은 주로 순수한 성능과 확장성, 특히 아주 큰 데이터셋을 다룰 때에 최적화되어 있어요.

어쨌든 data.table은 Exercism에서는 사용할 수 없어서, 여기서는 완전성을 위해 언급만 해 둘게요.

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

# 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

문자형 문자열 열 하나, 숫자 열 하나, 불리언 열 하나가 있어요. 규모가 커지면, 이는 많은 실무 데이터 모음을 표현하는 직관적인 방법이에요.

tibble

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

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

문서도 Tidyverse 스타일로 꽤 자세하게 마련되어 있어요:

tibble 만들기

가장 간단하게는, 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을 내놓기 때문이에요. 이 내용은 이후 개념에서 다시 다룰 거예요.

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    

행 단위 연산

Note

분명히 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()를 사용해요.

tibble 수정하기

먼저 주의할 점: 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의 기술적 제약 안에서요).

GitHub에서 편집 링크가 새 창이나 탭에서 열려요

데이터프레임 배우기