18  결측값(Missing values)

18.1 소개

이 책의 앞부분에서 결측값의 기본 사항을 이미 배웠습니다. Chapter 1 에서 플롯을 만들 때 경고를 유발하는 것으로 처음 보았고, Section 3.5.2 에서 요약 통계 계산을 방해하는 것으로 보았으며, Section 12.2.2 에서 전염성이 있는 특성과 존재 여부를 확인하는 방법에 대해 배웠습니다. 이제 세부 사항을 더 알아보기 위해 결측값에 대해 더 깊이 다룰 것입니다.

NA로 기록된 결측값으로 작업하기 위한 몇 가지 일반적인 도구에 대해 논의하는 것으로 시작하겠습니다. 그런 다음 데이터에 단순히 없는 값인 암시적(implicitly) 결측값의 아이디어를 탐구하고 이를 명시적(explicit)으로 만드는 데 사용할 수 있는 몇 가지 도구를 보여줄 것입니다. 데이터에 나타나지 않는 팩터 수준으로 인해 발생하는 빈 그룹에 대한 관련 논의로 마무리하겠습니다.

18.1.1 선수 지식

결측 데이터 작업을 위한 함수는 대부분 tidyverse의 핵심 멤버인 dplyr 및 tidyr에서 제공됩니다.

library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 4.5.2
#> Warning: package 'readr' was built under R version 4.5.2

18.2 명시적 결측값

먼저 NA가 보이는 셀인 명시적 결측값을 생성하거나 제거하는 몇 가지 편리한 도구를 살펴보겠습니다.

18.2.1 마지막 관측값 이월(Last observation carried forward)

결측값의 일반적인 용도 중 하나는 데이터 입력 편의입니다. 데이터를 손으로 입력할 때 결측값은 때때로 이전 행의 값이 반복됨(또는 이월됨)을 나타냅니다:

treatment <- tribble(
  ~person,           ~treatment, ~response,
  "Derrick Whitmore", 1,         7,
  NA,                 2,         10,
  NA,                 3,         NA,
  "Katherine Burke",  1,         4
)

tidyr::fill()을 사용하여 이러한 결측값을 채울 수 있습니다. 이것은 select()처럼 작동하며 일련의 열을 취합니다:

treatment |>
  fill(everything())
#> # A tibble: 4 × 3
#>   person           treatment response
#>   <chr>                <dbl>    <dbl>
#> 1 Derrick Whitmore         1        7
#> 2 Derrick Whitmore         2       10
#> 3 Derrick Whitmore         3       10
#> 4 Katherine Burke          1        4

이 처리를 때때로 “마지막 관측값 이월(last observation carried forward)” 또는 줄여서 locf라고 합니다. .direction 인수를 사용하여 더 이국적인 방식으로 생성된 결측값을 채울 수 있습니다.

18.2.2 고정 값

때때로 결측값은 0과 같이 알려진 고정 값을 나타냅니다. dplyr::coalesce()를 사용하여 대체할 수 있습니다:

x <- c(1, 4, 5, 7, NA)
coalesce(x, 0)
#> [1] 1 4 5 7 0

때로는 구체적인 값이 실제로 결측값을 나타내는 정반대의 문제에 부딪힐 수 있습니다. 이는 일반적으로 결측값을 나타내는 적절한 방법이 없는 오래된 소프트웨어에서 생성된 데이터에서 발생하므로 99 또는 -999와 같은 특수한 값을 대신 사용해야 합니다.

가능하면 데이터를 읽을 때, 예를 들어 readr::read_csv()na 인수를 사용하여(예: read_csv(path, na = "99")) 이 문제를 처리하세요. 나중에 문제를 발견하거나 데이터 소스가 읽을 때 처리할 방법을 제공하지 않는 경우 dplyr::na_if()를 사용할 수 있습니다:

x <- c(1, 4, 5, 7, -99)
na_if(x, -99)
#> [1]  1  4  5  7 NA

18.2.3 NaN

계속하기 전에 때때로 마주칠 수 있는 특별한 유형의 결측값이 하나 있습니다. NaN(“난”이라고 발음) 또는 자가 님(not a number)입니다. 일반적으로 NA와 똑같이 작동하기 때문에 아는 것이 그리 중요하지는 않습니다:

x <- c(NA, NaN)
x * 10
#> [1]  NA NaN
x == 1
#> [1] NA NA
is.na(x)
#> [1] TRUE TRUE

NANaN을 구별해야 하는 드문 경우에는 is.nan(x)를 사용할 수 있습니다.

일반적으로 결과가 불확정적인 수학적 연산을 수행할 때 NaN을 만나게 됩니다:

0 / 0 
#> [1] NaN
0 * Inf
#> [1] NaN
Inf - Inf
#> [1] NaN
sqrt(-1)
#> Warning in sqrt(-1): NaNs produced
#> [1] NaN

18.3 암시적 결측값

지금까지 우리는 명시적으로(explicitly) 누락된, 즉 데이터에서 NA를 볼 수 있는 결측값에 대해 이야기했습니다. 하지만 전체 데이터 행이 데이터에 단순히 없는 경우 결측값이 암시적으로(implicitly) 누락될 수도 있습니다. 분기별 주식 가격을 기록하는 간단한 데이터셋으로 차이점을 설명해 보겠습니다:

stocks <- tibble(
  year  = c(2020, 2020, 2020, 2020, 2021, 2021, 2021),
  qtr   = c(   1,    2,    3,    4,    2,    3,    4),
  price = c(1.88, 0.59, 0.35,   NA, 0.92, 0.17, 2.66)
)

이 데이터셋에는 두 개의 결측 관측값이 있습니다:

  • 2020년 4분기의 price는 값이 NA이기 때문에 명시적으로 누락되었습니다.

  • 2021년 1분기의 price는 데이터셋에 단순히 나타나지 않기 때문에 암시적으로 누락되었습니다.

차이점에 대해 생각하는 한 가지 방법은 다음과 같은 선(Zen)적인 화두입니다:

명시적 결측값은 부재의 존재(presence of an absence)입니다.

암시적 결측값은 존재의 부재(absence of a presence)입니다.

때로는 물리적으로 작업할 무언가를 갖기 위해 암시적 결측을 명시적으로 만들고 싶을 때가 있습니다. 다른 경우에는 명시적 결측이 데이터 구조에 의해 강제되고 그것들을 제거하고 싶을 때가 있습니다. 다음 섹션에서는 암시적 결측과 명시적 결측 사이를 이동하는 몇 가지 도구에 대해 설명합니다.

18.3.1 피벗(Pivoting)

암시적 결측을 명시적으로 만들고 그 반대로 만들 수 있는 도구 하나를 이미 보았습니다: 피벗입니다. 데이터를 더 넓게 만들면 행과 새 열의 모든 조합에 값이 있어야 하기 때문에 암시적 결측값이 명시적으로 될 수 있습니다. 예를 들어 stocks를 피벗하여 quarter를 열에 넣으면 두 결측값이 모두 명시적으로 됩니다:

stocks |>
  pivot_wider(
    names_from = qtr, 
    values_from = price
  )
#> # A tibble: 2 × 5
#>    year   `1`   `2`   `3`   `4`
#>   <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1  2020  1.88  0.59  0.35 NA   
#> 2  2021 NA     0.92  0.17  2.66

기본적으로 데이터를 길게 만들면 명시적 결측값이 보존되지만, 데이터가 깔끔하지 않기 때문에 존재하는 구조적 결측값인 경우 values_drop_na = TRUE를 설정하여 삭제(암시적으로 만듦)할 수 있습니다. 자세한 내용은 Section 5.2 의 예제를 참조하세요.

18.3.2 완료(Complete)

tidyr::complete()를 사용하면 존재해야 하는 행의 조합을 정의하는 변수 집합을 제공하여 명시적 결측값을 생성할 수 있습니다. 예를 들어 stocks 데이터에 yearqtr의 모든 조합이 존재해야 한다는 것을 알고 있습니다:

stocks |>
  complete(year, qtr)
#> # A tibble: 8 × 3
#>    year   qtr price
#>   <dbl> <dbl> <dbl>
#> 1  2020     1  1.88
#> 2  2020     2  0.59
#> 3  2020     3  0.35
#> 4  2020     4 NA   
#> 5  2021     1 NA   
#> 6  2021     2  0.92
#> # ℹ 2 more rows

일반적으로 기존 변수의 이름으로 complete()를 호출하여 누락된 조합을 채웁니다. 그러나 때로는 개별 변수 자체가 불완전하므로 대신 자신의 데이터를 제공할 수 있습니다. 예를 들어 stocks 데이터셋이 2019년부터 2021년까지 실행되어야 한다는 것을 알 수 있으므로 year에 대해 해당 값을 명시적으로 제공할 수 있습니다:

stocks |>
  complete(year = 2019:2021, qtr)
#> # A tibble: 12 × 3
#>    year   qtr price
#>   <dbl> <dbl> <dbl>
#> 1  2019     1 NA   
#> 2  2019     2 NA   
#> 3  2019     3 NA   
#> 4  2019     4 NA   
#> 5  2020     1  1.88
#> 6  2020     2  0.59
#> # ℹ 6 more rows

변수의 범위는 정확하지만 모든 값이 있는 것은 아닌 경우 full_seq(x, 1)을 사용하여 min(x)에서 max(x)까지 1씩 간격을 둔 모든 값을 생성할 수 있습니다.

경우에 따라 관측값의 전체 집합이 변수의 단순한 조합으로 생성될 수 없습니다. 이 경우 complete()가 수행하는 작업을 수동으로 수행할 수 있습니다. 존재해야 하는 모든 행을 포함하는 데이터 프레임을 만든 다음(필요한 기술 조합을 사용하여) dplyr::full_join()을 사용하여 원본 데이터셋과 결합합니다.

18.3.3 조인(Joins)

이것은 암시적으로 누락된 관측값을 드러내는 또 다른 중요한 방법인 조인으로 우리를 이끕니다. Chapter 19 에서 조인에 대해 자세히 배우게 되겠지만, 한 데이터셋의 값이 누락되었다는 것을 다른 데이터셋과 비교할 때만 알 수 있는 경우가 많기 때문에 여기서 간단히 언급하고 싶었습니다.

dplyr::anti_join(x, y)y에 일치하는 항목이 없는 x의 행만 선택하므로 여기에서 특히 유용한 도구입니다. 예를 들어 두 개의 anti_join()을 사용하여 flights에 언급된 4개의 공항과 722대의 비행기에 대한 정보가 누락되었음을 드러낼 수 있습니다:

library(nycflights13)

flights |> 
  distinct(faa = dest) |> 
  anti_join(airports)
#> Joining with `by = join_by(faa)`
#> # A tibble: 4 × 1
#>   faa  
#>   <chr>
#> 1 BQN  
#> 2 SJU  
#> 3 STT  
#> 4 PSE

flights |> 
  distinct(tailnum) |> 
  anti_join(planes)
#> Joining with `by = join_by(tailnum)`
#> # A tibble: 722 × 1
#>   tailnum
#>   <chr>  
#> 1 N3ALAA 
#> 2 N3DUAA 
#> 3 N542MQ 
#> 4 N730MQ 
#> 5 N9EAMQ 
#> 6 N532UA 
#> # ℹ 716 more rows

18.3.4 연습문제

  1. 항공사와 planes에서 누락된 것으로 보이는 행 사이에 어떤 관계를 찾을 수 있습니까?

18.4 팩터와 빈 그룹

마지막 유형의 결측은 팩터로 작업할 때 발생할 수 있는 관측값이 포함되지 않은 그룹인 빈 그룹입니다. 예를 들어 사람들에 대한 건강 정보가 포함된 데이터셋이 있다고 상상해 보세요:

health <- tibble(
  name   = c("Ikaia", "Oletta", "Leriah", "Dashay", "Tresaun"),
  smoker = factor(c("no", "no", "no", "no", "no"), levels = c("yes", "no")),
  age    = c(34, 88, 75, 47, 56),
)

그리고 dplyr::count()로 흡연자 수를 세고 싶습니다:

health |> count(smoker)
#> # A tibble: 1 × 2
#>   smoker     n
#>   <fct>  <int>
#> 1 no         5

이 데이터셋에는 비흡연자만 포함되어 있지만 흡연자가 존재한다는 것은 알고 있습니다. 흡연자 그룹은 비어 있습니다. .drop = FALSE를 사용하여 데이터에 보이지 않는 그룹을 포함하여 모든 그룹을 유지하도록 count()에 요청할 수 있습니다:

health |> count(smoker, .drop = FALSE)
#> # A tibble: 2 × 2
#>   smoker     n
#>   <fct>  <int>
#> 1 yes        0
#> 2 no         5

동일한 원칙이 ggplot2의 이산 축에도 적용되며, 이산 축도 값이 없는 수준을 삭제합니다. 적절한 이산 축에 drop = FALSE를 제공하여 강제로 표시할 수 있습니다:

ggplot(health, aes(x = smoker)) +
  geom_bar() +
  scale_x_discrete()

ggplot(health, aes(x = smoker)) +
  geom_bar() +
  scale_x_discrete(drop = FALSE)

x축에 단일 값 "no"가 있는 막대 차트.

마지막 플롯과 동일한 막대 차트이지만 이제 x축에 "yes"와 "no" 두 값이 있습니다. "yes" 범주에 대한 막대는 없습니다.

동일한 문제가 dplyr::group_by()에서 더 일반적으로 발생합니다. 그리고 다시 .drop = FALSE를 사용하여 모든 팩터 수준을 보존할 수 있습니다:

health |> 
  group_by(smoker, .drop = FALSE) |> 
  summarize(
    n = n(),
    mean_age = mean(age),
    min_age = min(age),
    max_age = max(age),
    sd_age = sd(age)
  )
#> # A tibble: 2 × 6
#>   smoker     n mean_age min_age max_age sd_age
#>   <fct>  <int>    <dbl>   <dbl>   <dbl>  <dbl>
#> 1 yes        0      NaN     Inf    -Inf   NA  
#> 2 no         5       60      34      88   21.6

여기서 흥미로운 결과를 얻을 수 있는데, 빈 그룹을 요약할 때 요약 함수가 길이가 0인 벡터에 적용되기 때문입니다. 길이 0인 빈 벡터와 각각 길이 1인 결측값 사이에는 중요한 차이가 있습니다.

# 두 개의 결측값을 포함하는 벡터
x1 <- c(NA, NA)
length(x1)
#> [1] 2

# 아무것도 포함하지 않는 벡터
x2 <- numeric()
length(x2)
#> [1] 0

모든 요약 함수는 길이가 0인 벡터와 함께 작동하지만 언뜻 보기에 놀라운 결과를 반환할 수 있습니다. 여기서 mean(age)NaN을 반환하는 것을 볼 수 있습니다. mean(age) = sum(age)/length(age)인데 여기서는 0/0이기 때문입니다. max()min()은 빈 벡터에 대해 -Inf와 Inf를 반환하므로 결과를 새로운 데이터의 비어 있지 않은 벡터와 결합하고 다시 계산하면 새로운 데이터의 최소값 또는 최대값을 얻게 됩니다1.

때로는 더 간단한 접근 방식은 요약을 수행한 다음 complete()를 사용하여 암시적 결측을 명시적으로 만드는 것입니다.

health |> 
  group_by(smoker) |> 
  summarize(
    n = n(),
    mean_age = mean(age),
    min_age = min(age),
    max_age = max(age),
    sd_age = sd(age)
  ) |> 
  complete(smoker)
#> # A tibble: 2 × 6
#>   smoker     n mean_age min_age max_age sd_age
#>   <fct>  <int>    <dbl>   <dbl>   <dbl>  <dbl>
#> 1 yes       NA       NA      NA      NA   NA  
#> 2 no         5       60      34      88   21.6

이 접근 방식의 주요 단점은 개수가 0이어야 한다는 것을 알고 있음에도 불구하고 개수에 대해 NA를 얻는다는 것입니다.

18.5 요약

결측값은 이상합니다! 때로는 명시적인 NA로 기록되지만 다른 때에는 부재를 통해서만 알 수 있습니다. 이 장에서는 명시적 결측값으로 작업하는 도구, 암시적 결측값을 밝혀내는 도구를 제공하고 암시적이 명시적이 되거나 그 반대가 될 수 있는 몇 가지 방법에 대해 논의했습니다.

다음 장에서는 이 파트의 마지막 장인 조인을 다룹니다. 데이터 프레임 안에 넣는 것이 아니라 데이터 프레임 전체와 작동하는 도구에 대해 논의할 것이기 때문에 지금까지의 장과는 약간 다릅니다.


  1. 즉, min(c(x, y))는 항상 min(min(x), min(y))와 같습니다.↩︎