16.1 들어가기

  1. 티블의 기초 객체인 벡터에 대해 알아야 한다.

  2. 함수의 대부분은 벡터로 인하여 작동하기 때문 매우 중요.

16.1.1 준비하기

purrr 패키지의 함수를 사용할 것

library(tidyverse)

16.2 벡터의 기초

  • 원자벡터: 6가지 유형

     1. 논리형(logical)
     2. 정수형(integer)
     3. 더블형(dou-ble)
     4. 문자형(character)
     5. 복소수형(character)
     6. 원시형(raw)

  • 리스트 = 재귀 벡터라고도 불리며 한 리스트가 다른 리스트를 포함할 수 있다.

    <원자 벡터와 리스트의 차이점>

    1. 원자 벡터 = 동질적, 리스트 = 이질적
    2. NULL은 벡터가 없는 것을 나타내기 위해 종종 사용된다.

    모든 벡터는 두가지 주요 속성이 있다.

    • 유형 : typeof()로 확인할 수 있음.
    typeof(letters)
    ## [1] "character"
    typeof(1:10)
    ## [1] "integer"
    • 길이: length()로 확인할 수 있음.
    x <- list("a", "b", 1:10)
    length(x)
    ## [1] 3


    임의의 추가 메타 데이터를 속성(attribute)형식으로 벡터에 포함시켜 확장 벡터를 만드는데 사용한다. 확장 벡터에는 네 가지 중요한 유형이 있다.

    • 팩터형은 정수형 벡터를 기반으로 만들어졌다.
    • 데이트형과 데이트-타임형은 수치형 벡터를 기반으로 만들어졌다.
    • 데이터프레임과 티블은 리스트를 기반으로 만들어졌다.

16.3 원자 벡터의 주요 유형

16.3.1 논리형

  • FALSE, TRUE, NA 세 가지 값만 사용 할 수 있기에 가장 단순한 원자 벡터이다.
  • 비교 연산자로 생성
  • c()를 사용하여 직접 생성할 수도 있다.
1:10 %% 3 == 0
##  [1] FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE FALSE  TRUE FALSE
c(TRUE, TRUE, FALSE, NA)
## [1]  TRUE  TRUE FALSE    NA

16.3.2 수치형

  • 정수형 + 더블형 = 수치형 벡터
  • R에서 숫자는 기본값으로 더블형이다.
  • 정수형으로 만들려면 숫자 뒤에 L을 붙이면 된다.
typeof(1)
## [1] "double"
typeof(1L)
## [1] "integer"
1.5L
## [1] 1.5


정수형과 더블형을 구분하는 게 일반적으로 중요하지 않지만 두 가지 중요한 차이점을 알고 있어야 한다.

  • 더블형은 근사값이다. 더블형은 부동 소수점 수를 나타내는데 고정된 크기의 메모리로 이를 매번 정확히 표현할 수는 없다. 즉, 더블형 값은 모두 근사치로 간주해야 한다.
예를 들어 2의 제곱근의 제곱은 무엇일까?
x <- sqrt(2) ^ 2
x
## [1] 2
x - 2
## [1] 4.440892e-16

즉, 대부분의 계산에는 근사 오차가 포함된다. ==를 사용하여 부동 소수점 수를 비교하는 대신 dplyr::near()를 사용해야 한다. 그러면 수치상 오차가 허용된다.

  • 정수형에는 특수한 값이 NA 한 개가 있음
  • 더블형에는 NA, NaN, Inf, 및 -Inf가 있다.
  • 이 모든 수치들은 나눗셈에서 발생할 수 있다.
c(-1, 0, 1) / 0
## [1] -Inf  NaN  Inf

이러한 특수한 값을 확인할 목적으로 ==을 사용하면 안 된다.

대신 도우미 함수

16.3.3 문자형

문자형 벡터는 각 요소가 문자열이기 때문에 가장 복잡한 유형의 원자 벡터이다.

  • 문자열 구현에 있어서 중요한 특징 중 하나
  • R은 전역 문자열 풀을 사용한다. -> 고유 문자열은 메모리에 한 번만 저장되며 문자열을 사용할때 마다 해당 표현을 포인트한다.
pryr::object_size()를 사용하여 이 동작을 직접 볼 수 있다.
x <- "적당히 긴 문자열입니다"
pryr::object_size(x)
## 136 B
y <- rep(x, 1000)
pryr::object_size(y)
## 8.13 kB

16.3.4 결측값

각 유형의 원자 벡터에는 고유한 결측값이 있다.

NA            # 논리형
## [1] NA
NA_integer_   # 정수형
## [1] NA
NA_real_      # 더블형
## [1] NA
NA_character_ # 문자형
## [1] NA

16.3 원자 벡터 이용하기

여러 유형의 원자 벡터를 이해했으므로 이제 함께 사용할 수 있는 도구들을 검토하는 것이 좋다.

  • 한 유형에서 다른 유형으로 변환시키는 법, 자동으로 변환되는 조건.
  • 한 객체가 특정 유형 벡터인지 알아보는 법.
  • 다른 길이의 벡터들로 작업할 때 발생하는 일.
  • 벡터의 요소를 이름 짓는 법.
  • 관심 있는 요소를 추출하는 법.

16.4.1 강제 변환

한 유형 벡터에서 다른 유형으로 강제 변환하는 방법은 두 가지이다.

  • 명시적 강제 변환은 logical(), as.integer(), as.double(), as.character()와 같은 함수를 호출할 때 발생
명시적 강제 변환이 필요한 경우
 -readr의 col_types 명세를 조정해야 할 수도 있다.
  • 암묵적 강제 변환은 특정 유형의 벡터가 필요한 상황에서 어떤 다른 벡터를 사용하는 경우 발생한다.
수치형 요약 함수에 논리형 벡터를 사용하거나 정수형 벡터가 예상되는 곳에 더블형 벡터를 사용 하는 경우이다.  


이미 암묵적 강제 변환을 보았다. -> 수치형 문제에서 논리형 벡터를 사용 했던 것이다.

x <- sample(20, 100, replace = TRUE)
y <- x > 10
sum(y) # 10보다 큰 것의 개수?
## [1] 42
mean(y) # 10보다 큰 비율?
## [1] 0.42


반대 방향, 즉 정수형에서 논리형으로의 암시적 강제 변환에 의존하는 코드도 볼 수 있다.

if (length(x)) {
  # 어떤 작업
}
## NULL

이렇게 하면 코드를 이해하기가 더 어렵게 된다고 생각하기 때문에 이를 권장하지 않는다.

length(x) > 0

또한 c()로 여러 유형을 포함하는 벡터를 만들려고 할 때 다음과 같은 일이 일어난다는 것을 이해하는 것도 중요하며 가장 복잡한 유형으로 변환된다.

typeof(c(TRUE, 1L))
## [1] "integer"
typeof(c(1L, 1.5))
## [1] "double"
typeof(c(1.5, "a"))
## [1] "character"

벡터의 유형은 개별요소가 아닌 전체 벡터의 특성, 한 원자 벡터의 유형이 여러 개일 수는 없다.
동일한 벡터에서 여러 유형을 혼합해야 하는 경우에는 리스트를 사용하면 된다.

16.4.2 테스트 함수

  1. typeof()를 사용하는 것
  2. TRUE 또는 FALSE를 반환하는 테스트 함수를 사용하는 것 purrr이 제공하는 is_* 함수를 사용하는 것이 더 안전하다 위의 각 함수마다 is_scalar atomic()과 같은 '스칼라' 버전이 있는데 이는 길이가 1인지 확인한다.

16.4.3 스칼라와 재활용 규칙

  • 짧은 벡터가 긴 벡터의 길이로 반복되거나 재활용되므로 이를 벡터 재활용이라고 한다.
  • 벡터 재활용은 벡터와 ’스칼라’를 혼합할 때 매우 유용하다.
  • 사실 R에서는 스칼라가 존재하지 않는다.
  • 스칼라가 없으므로 대부분의 내장 함수는 벡터화(즉, 수치 벡터에서 작동)된다.
예로 든 코드이다
sample(10) + 100
##  [1] 103 106 107 108 109 101 104 102 110 105
runif(10) > 0.5
##  [1] FALSE FALSE  TRUE FALSE  TRUE  TRUE FALSE FALSE FALSE FALSE


같은 길이의 두 벡터 또는 벡터와 ’스칼라’를 더할 경우 일어나야 하는 일은 직관적이다.

but 길이가 다른 두 개의 벡터를 더하면 어떻게 되는가?
1:10 + 1:2
##  [1]  2  4  4  6  6  8  8 10 10 12


여기에서 R은 가장 짧은 벡터를 가장 긴 벡터 길이로 확장함 -> 이것을 재활용이라고 부른다.

1:10 + 1:3
##  [1]  2  4  6  5  7  9  8 10 12 11
#> Warning in  1:10 + 1:3: longer object length is not a multiple of shorter
#> object length


벡터 재활용을 사용하면 매우 간결하고 영리한 코드를 작성할 수 있지만 조용히 문제가 숨겨질 수도 있다. 재활용을 하고싶다면 rep()으로 직접 처리해야 한다.

tibble(x = 1:4, y = 1:2)
#> Error: colum 'y' must be length 1 or 4, not 2
tibble(x = 1:4, y = rep(1:2, 2))
## # A tibble: 4 x 2
##       x     y
##   <int> <int>
## 1     1     1
## 2     2     2
## 3     3     1
## 4     4     2
tibble(x = 1:4, y = rep(1:2, each = 2))
## # A tibble: 4 x 2
##       x     y
##   <int> <int>
## 1     1     1
## 2     2     1
## 3     3     2
## 4     4     2

16.4.4 벡터 이름 짓기

모든 유형의 벡터는 이름을 지정할 수 있다. c()를 사용하여 생성 시 이름을 지정할 수 있다.

c(x = 1, y = 2, z = 3)
## x y z 
## 1 2 3

또는 생성 이후에 purrr::set_names()으로 이름을 지정할 수도 있다.

setNames(1:3, c("a", "b", "c"))
## a b c 
## 1 2 3

명명된 벡터는 서브셋할 때 매우 유용하며 다음에 설명된다.

16.4.5 서브셋 하기

지금까지 dplyr::filter()를 사용하여 티블의 행을 필터링했다.
filter()는 티블에서만 작동하기 때문에 벡터용 도구가 새로 필요하다 -> [ 이다
[ 은 서브셋 하는 함수이며 x[a]와 같이 호출된다.

  • 정수형만 포함하는 수치형 벡터
x <- c("one", "two", "three", "four", "five")
x[c(3, 2, 5)]
## [1] "three" "two"   "five"

위치를 반복하면 실제로 입력보다 긴 출력을 만들 수 있다.

x[c(1, 1, 5, 5, 5, 2)]
## [1] "one"  "one"  "five" "five" "five" "two"

음수값은 해당 위치의 요소를 누락시킨다.

x[c(-1, -3, -5)]
## [1] "two"  "four"

양수값과 음수값을 혼합하면 오류이다.

x[c(1, -1)]
#> Error in x[c(1, -1)]
#> only 0's may be mixed with negative subscripts

다음의 오류 메시지는 0으로 서브셋했기 때문에 아무 값도 반환하지 않았음을 나타낸다.

x[0]
## character(0)
  • 논리형 벡터로 서브셋하면 TRUE 값에 해당하는 모든 값이 유지된다.
x <- c(10, 3, NA, 5, 8, 1, NA)

# x 중 결측값이 아닌 모든 값
x[!is.na(x)]
## [1] 10  3  5  8  1
# x 중 모든 짝수(혹은 결측값!)
x[x %% 2 == 0]
#> [1] 10 NA 8 NA
  • 명명된 벡터가 있다면 이를 문자형 벡터로 서브셋할 수 있다.
x <- c(abc = 1, def = 2, xyz = 5)
x[c("xyz", "def")]
## xyz def 
##   5   2
  • 가장 간단한 서브셋 동작은 x[ ]이며, 전체 x를 반환한다.

  • [ 의 변형 [[ 이다. [[ 은 오직 하나의 요소만 추출하고 항상 이름을 누락시킨다.

16.5 재귀 벡터(리스트)

  • 다양한 종류의 데이터를 가지는 데이터 묶음

  • 리스트 자료형 자체도 데이터로 가질 수 있음

  • 그래서 재귀형(recursive)라고도 표현함

  • 원자 벡터 생성 : c() 함수 사용 (c = combin의 약자)

  • 리스트 생성 : list() 함수 사용

  • 이중 대괄호와 대괄호가 있는 형태

x <- list(1, 2, 3)
x
## [[1]]
## [1] 1
## 
## [[2]]
## [1] 2
## 
## [[3]]
## [1] 3
  • str() 함수를 사용하여 어떤 구조인지 확인
str(x)
## List of 3
##  $ : num 1
##  $ : num 2
##  $ : num 3
x_named <- list(a = 1,b = 2,c = 3)
str(x_named)
## List of 3
##  $ a: num 1
##  $ b: num 2
##  $ c: num 3
  • 원자 벡터와 다르게 list()는 객체들을 혼합하여 포함할 수 있다.
y <- list("a", 1L, 1.5, TRUE)
str(y)
## List of 4
##  $ : chr "a"
##  $ : int 1
##  $ : num 1.5
##  $ : logi TRUE
  • 리스트는 다른 리스트를 포함할 수도 있다.
z <- list(list(1, 2), list(3, 4))
str(z)
## List of 2
##  $ :List of 2
##   ..$ : num 1
##   ..$ : num 2
##  $ :List of 2
##   ..$ : num 3
##   ..$ : num 4

16.5.1 리스트 시각화

복잡한 리스트는 시각적으로 표현하는 것이 편리하다.

x1 <- list(c(1, 2), c(3, 4))
x2 <- list(list(1, 2), list(3, 4))
x3 <- list(1, list(2, list(3)))

3가지 원칙이 있다.

  1. 리스트는 모서리가 둥글다. 원자 벡터는 모서리가 사각이다.
  2. 자식은 부모 내부에 그려지며 계층을 쉽게 볼 수 있게 배경이 약간 더 어둡다.
  3. 자식의 방향(즉, 행 또는 열)은 중요하지 않으므로 공간을 절약하거나 예제에서 중요한 속성을 잘 설명할 수 있는 행 또는 열 방향을 선택한다.

16.5.2 서브셋하기

리스트 서브셋하는 방법은 세 가지이다. 예시로 어떤 리스트가 a라 하자.

a <- list(a = 1:3, b = "a string", c = pi, d = list(-1, -5))
  • [ 는 부분 리스트를 추출한다. 결과는 항상 리스트이다.
str(a[1:2])
## List of 2
##  $ a: int [1:3] 1 2 3
##  $ b: chr "a string"
str(a[4])
## List of 1
##  $ d:List of 2
##   ..$ : num -1
##   ..$ : num -5

벡터에서와 같이 논리형, 정수형, 문자형 벡터로 서브셋할 수 있다.

  • [[ 는 리스트의 단일 구성요소를 추출한다. 리스트의 계층구조에서 한 레벨을 제거한다.
str(a[[1]])
##  int [1:3] 1 2 3
str(a[[4]])
## List of 2
##  $ : num -1
##  $ : num -5
  • $는 리스트의 명명된 요소를 추출하는 단축문자이다. 이는 따옴표가 필요없다는 것을 제외하고는 [[ 와 유사하게 동작한다.
a$a
## [1] 1 2 3
a[["a"]]
## [1] 1 2 3


[ 와 [[ 의 차이는 리스트에 있어서 정말 중요하다. [[ 는 리스트 안으로 내려가는 반면 [ 는 더 작은 새 리스트를 반환하기 때문이다.

16.5.3 조미료 리스트

[ 와 [[ 의 차이점은 매우 중요. 헷길리기 쉬움으로 후추통으로 비유해보자

이 후추통이 리스트 x라고 한다면 x[1]은 후추팩 하나가 들어있는 후추통이다.

x[2]는 똑같지만, 두 번째 팩을 포함한다. x[1:2]는 후추팩 두 개가 들어있는 후추통이다.
x[[1]]은 후추팩 하나이다.

후추팩의 내용물은 x[[1]][[1]]으로 얻을 수 있다.

16.6 속성

  • 확장 벡터는 속성을 가짐
  • 속성(attribute): 메타 데이터(“데이터의 데이터”)
  • attr() 함수를 사용하여 가져오거나 attributes()로 한번에 모두 볼 수 있다.
x <- 1:10
attr(x, "greeting")
## NULL
attr(x, "greeting") <- "Hi!"
attr(x, "farewell") <- "Bye"
attributes(x)
## $greeting
## [1] "Hi!"
## 
## $farewell
## [1] "Bye"

R의 기본 부분을 구현하는데 세 가지 매우 중요한 속성이 사용되었다.

  • Names는 벡터 요소의 이름을 지정하는 데 사용된다.
  • Dimensions(줄여서 dims)는 벡터를 행렬이나 어레이 같이 동작하도록 만든다.
  • Class는 S3 객체지향 시스템을 구현하는데 사용된다.

Class는 제네릭 함수의 작동 방식을 제어한다.
   일반적인 제너릭 함수는 다음과 같다.

as.Date
## function (x, ...) 
## UseMethod("as.Date")
## <bytecode: 0x0000000014f674b8>
## <environment: namespace:base>

‘UseMethod’ 호출은 이 함수가 제네릭 함수임을 의미하여 첫 번째 인수의 클래스를 기반으로 특정함수(메소드)를 호출한다. (모든 메소드는 함수이지만 모든 함수가 메서드인 것은 아니다.) methods()를 사용하여 제네릭의 모든 메서드를 나열할 수 있다.

methods("as.Date")
## [1] as.Date.character   as.Date.default     as.Date.factor     
## [4] as.Date.numeric     as.Date.POSIXct     as.Date.POSIXlt    
## [7] as.Date.vctrs_sclr* as.Date.vctrs_vctr*
## see '?methods' for accessing help and source code

예를 들어 x가 문자형 벡터이면 as.Date()는 as.Date.character()를 호출하고 팩터형이면 as.Date.factor()를 호출한다.
   getS3method()로 메서드의 구현을 구체적으로 볼 수 있다.

getS3method("as.Date", "default")
## function (x, ...) 
## {
##     if (inherits(x, "Date")) 
##         x
##     else if (is.null(x)) 
##         .Date(numeric())
##     else if (is.logical(x) && all(is.na(x))) 
##         .Date(as.numeric(x))
##     else stop(gettextf("do not know how to convert '%s' to class %s", 
##         deparse1(substitute(x)), dQuote("Date")), domain = NA)
## }
## <bytecode: 0x00000000197b6100>
## <environment: namespace:base>
getS3method("as.Date", "numeric")
## function (x, origin, ...) 
## {
##     if (missing(origin)) {
##         if (!length(x)) 
##             return(.Date(numeric()))
##         if (!any(is.finite(x))) 
##             return(.Date(x))
##         stop("'origin' must be supplied")
##     }
##     as.Date(origin, ...) + x
## }
## <bytecode: 0x00000000187ae7d8>
## <environment: namespace:base>

가장 중요한 S3 제네릭은 print()인데 이는 콘솔에서 이름이 타이핑되었을때 객체가 출력되는 방식을 조정한다. 다른 중요한 제네릭은 서브넷 동작 함수들인 [,[[, $이다.

16.7 확장 벡터

확장벡터는 클래스를 가지므로 원자 벡터와 다르게 동작한다.

  • 팩터형
  • 데이트형
  • 데이트-타임형
  • 티블

16.7.1 팩터형

  • 팩터형은 범주형 데이터를 표현하기 위해 설계되었다.
  • 팩터형은 정수형을 기반으로 만들어졌고 레벨 속성을 갖는다.
x <- factor(c("ab", "cd", "ab"), levels = c("ab", "cd", "ef"))
typeof(x)
## [1] "integer"
attributes(x)
## $levels
## [1] "ab" "cd" "ef"
## 
## $class
## [1] "factor"

16.7.2 데이트형과 데이트-타임형

R의 데이트형은 1970년 1월 1일부터 지난 일 수를 나타내는 수치형 벡터이다.

x <- as.Date("1971-01-01")
unclass(x)
## [1] 365
typeof(x)
## [1] "double"
attributes(x)
## $class
## [1] "Date"

데이트-타임형은 1970년 1월 1일부터 지난 초 수를 나타내는 POSIXct 클래스를 가진 수치형 벡터이다.

'POSIXct'는 Portable Operating System Interface calendar time을 의미한다.
x <- lubridate::ymd_hm("1970-01-01 01:00")
unclass(x)
## [1] 3600
## attr(,"tzone")
## [1] "UTC"
typeof(x)
## [1] "double"
attributes(x)
## $class
## [1] "POSIXct" "POSIXt" 
## 
## $tzone
## [1] "UTC"

tzone 속성은 선택사항이다. 이것은 시간이 출력되는 방식을 제어하는 것이지 절대 시간을 제어하는 것이 아니다.

attr(x, "tzone") <- "US/Pacific"
x
## [1] "1969-12-31 17:00:00 PST"
attr(x, "tzone") <- "US/Eastern"
x
## [1] "1969-12-31 20:00:00 EST"

데이트-타임형의 또 다른 유형은 POSIXlt라고 부르는 것이다. 이는 명명된 리스트를 기반으로 만들어졌다.

y <- as.POSIXlt(x)
typeof(y)
## [1] "list"
attributes(y)
## $names
##  [1] "sec"    "min"    "hour"   "mday"   "mon"    "year"   "wday"   "yday"  
##  [9] "isdst"  "zone"   "gmtoff"
## 
## $class
## [1] "POSIXlt" "POSIXt" 
## 
## $tzone
## [1] "US/Eastern" "EST"        "EDT"

POSIXlt는 tidyverse 내부에서 거의 쓰지 않는다.

16.7.3 티블

  • 티블은 확장형 리스트이다.
  • 세 가지 클래스인 tbl_df, tbl, data.frame을 갖는다.
  • 두 가지 속성인 names(열 이름)와 row.names(행이름)를 갖는다.
tb <- tibble::tibble(x = 1:5, y = 5:1)
typeof(tb)
## [1] "list"
attributes(tb)
## $names
## [1] "x" "y"
## 
## $row.names
## [1] 1 2 3 4 5
## 
## $class
## [1] "tbl_df"     "tbl"        "data.frame"

전통적인 데이터프레임은 매우 유사한 구조를 가지고 있다.

df <- data.frame(x = 1:5, y = 5:1)
typeof(df)
## [1] "list"
attributes(df)
## $names
## [1] "x" "y"
## 
## $class
## [1] "data.frame"
## 
## $row.names
## [1] 1 2 3 4 5
  • 여기서의 차이점은 클래스이다. 티블의 클래스는 ’data.frame’을 포함하는데 이는 티블이 기본값으로 일반적인 데이터프라임 동작을 물려받음을 의미한다.

  • 티블 데이터프라임과 리스트의 차이점은 모든 요소는 길이가 같은 벡터이어야 한다는 점이다.