13  숫자

13.1 소개

수치형 벡터는 데이터 과학의 중추이며, 책의 앞부분에서 이미 여러 번 사용했습니다. 이제 R에서 수치형 벡터로 수행할 수 있는 작업을 체계적으로 조사하여 수치형 벡터와 관련된 향후 문제를 해결할 수 있는 준비를 확실히 할 때입니다.

문자열이 있는 경우 숫자를 만드는 몇 가지 도구를 제공하고 count()에 대해 조금 더 자세히 설명하는 것으로 시작하겠습니다. 그런 다음 mutate()와 잘 어울리는 다양한 수치 변환을 살펴볼 것입니다. 여기에는 다른 유형의 벡터에도 적용할 수 있지만 수치형 벡터에 자주 사용되는 보다 일반적인 변환이 포함됩니다. 마지막으로 summarize()와 잘 어울리는 요약 함수를 다루고 mutate()와 함께 사용하는 방법도 보여줄 것입니다.

13.1.1 선수 지식

이 장에서는 대부분 기본(base) R의 함수를 사용하는데, 이는 패키지를 로드하지 않고도 사용할 수 있습니다. 하지만 mutate()filter()와 같은 tidyverse 함수 내에서 이러한 기본 R 함수를 사용할 것이므로 여전히 tidyverse가 필요합니다. 지난 장과 마찬가지로 nycflights13의 실제 예제와 c()tribble()로 만든 장난감 예제를 사용할 것입니다.

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

13.2 숫자 만들기

대부분의 경우 R의 수치 유형인 정수(integer) 또는 배정밀도 실수(double) 중 하나로 이미 기록된 숫자를 얻게 됩니다. 그러나 어떤 경우에는 열 헤더에서 피벗하여 생성했거나 데이터 가져오기 프로세스에서 문제가 발생했기 때문에 문자열로 된 숫자를 만날 수도 있습니다.

readr은 문자열을 숫자로 파싱하기 위한 두 가지 유용한 함수인 parse_double()parse_number()를 제공합니다. 숫자가 문자열로 작성되었을 때 parse_double()을 사용하세요:

x <- c("1.2", "5.6", "1e3")
parse_double(x)
#> [1]    1.2    5.6 1000.0

문자열에 무시하고 싶은 숫자가 아닌 텍스트가 포함되어 있는 경우 parse_number()를 사용하세요. 이것은 통화 데이터와 백분율에 특히 유용합니다:

x <- c("$1,234", "USD 3,513", "59%")
parse_number(x)
#> [1] 1234 3513   59

13.3 개수(Counts)

개수와 약간의 기본 산술만으로 얼마나 많은 데이터 과학을 수행할 수 있는지 놀랍기 때문에 dplyr은 count()를 사용하여 가능한 한 쉽게 개수를 셀 수 있도록 노력합니다. 이 함수는 분석 중 빠른 탐색 및 확인에 좋습니다:

flights |> count(dest)
#> # A tibble: 105 × 2
#>   dest      n
#>   <chr> <int>
#> 1 ABQ     254
#> 2 ACK     265
#> 3 ALB     439
#> 4 ANC       8
#> 5 ATL   17215
#> 6 AUS    2439
#> # ℹ 99 more rows

(Chapter 4 의 조언에도 불구하고 계산이 예상대로 작동하는지 빠르게 확인하기 위해 콘솔에서 주로 사용되므로 count()는 보통 한 줄에 씁니다.)

가장 흔한 값을 보려면 sort = TRUE를 추가하세요:

flights |> count(dest, sort = TRUE)
#> # A tibble: 105 × 2
#>   dest      n
#>   <chr> <int>
#> 1 ORD   17283
#> 2 ATL   17215
#> 3 LAX   16174
#> 4 BOS   15508
#> 5 MCO   14082
#> 6 CLT   14064
#> # ℹ 99 more rows

그리고 모든 값을 보려면 |> View() 또는 |> print(n = Inf)를 사용할 수 있다는 것을 기억하세요.

group_by(), summarize(), n()을 사용하여 “수동으로” 동일한 계산을 수행할 수 있습니다. 이것은 동시에 다른 요약을 계산할 수 있기 때문에 유용합니다:

flights |> 
  group_by(dest) |> 
  summarize(
    n = n(),
    delay = mean(arr_delay, na.rm = TRUE)
  )
#> # A tibble: 105 × 3
#>   dest      n delay
#>   <chr> <int> <dbl>
#> 1 ABQ     254  4.38
#> 2 ACK     265  4.85
#> 3 ALB     439 14.4 
#> 4 ANC       8 -2.5 
#> 5 ATL   17215 11.3 
#> 6 AUS    2439  6.02
#> # ℹ 99 more rows

n()은 인수를 받지 않고 대신 “현재” 그룹에 대한 정보에 액세스하는 특수 요약 함수입니다. 즉, dplyr 동사 내부에서만 작동합니다:

n()
#> Error in `n()`:
#> ! Must only be used inside data-masking verbs like `mutate()`,
#>   `filter()`, and `group_by()`.

유용하게 사용할 수 있는 n()count()의 몇 가지 변형이 있습니다:

  • n_distinct(x)는 하나 이상의 변수의 고유한(unique) 값의 수를 셉니다. 예를 들어 어떤 목적지가 가장 많은 항공사에 의해 운항되는지 파악할 수 있습니다:

    flights |> 
      group_by(dest) |> 
      summarize(carriers = n_distinct(carrier)) |> 
      arrange(desc(carriers))
    #> # A tibble: 105 × 2
    #>   dest  carriers
    #>   <chr>    <int>
    #> 1 ATL          7
    #> 2 BOS          7
    #> 3 CLT          7
    #> 4 ORD          7
    #> 5 TPA          7
    #> 6 AUS          6
    #> # ℹ 99 more rows
  • 가중 개수는 합계입니다. 예를 들어 각 비행기가 비행한 마일 수를 “셀” 수 있습니다:

    flights |> 
      group_by(tailnum) |> 
      summarize(miles = sum(distance))
    #> # A tibble: 4,044 × 2
    #>   tailnum  miles
    #>   <chr>    <dbl>
    #> 1 D942DN    3418
    #> 2 N0EGMQ  250866
    #> 3 N10156  115966
    #> 4 N102UW   25722
    #> 5 N103US   24619
    #> 6 N104UW   25157
    #> # ℹ 4,038 more rows

    가중 개수는 흔한 문제이므로 count()에는 동일한 작업을 수행하는 wt 인수가 있습니다:

    flights |> count(tailnum, wt = distance)
  • sum()is.na()를 결합하여 결측값을 셀 수 있습니다. flights 데이터셋에서 이것은 취소된 항공편을 나타냅니다:

    flights |> 
      group_by(dest) |> 
      summarize(n_cancelled = sum(is.na(dep_time))) 
    #> # A tibble: 105 × 2
    #>   dest  n_cancelled
    #>   <chr>       <int>
    #> 1 ABQ             0
    #> 2 ACK             0
    #> 3 ALB            20
    #> 4 ANC             0
    #> 5 ATL           317
    #> 6 AUS            21
    #> # ℹ 99 more rows

13.3.1 연습문제

  1. count()를 사용하여 주어진 변수에 대해 결측값이 있는 행의 수를 어떻게 셀 수 있습니까?
  2. count()에 대한 다음 호출을 확장하여 대신 group_by(), summarize(), arrange()를 사용하세요:
    1. flights |> count(dest, sort = TRUE)

    2. flights |> count(tailnum, wt = distance)

13.4 수치 변환

변환 함수는 출력이 입력과 길이가 같기 때문에 mutate()와 잘 작동합니다. 대부분의 변환 함수는 이미 기본 R에 내장되어 있습니다. 모두 나열하는 것은 비현실적이므로 이 섹션에서는 가장 유용한 것들을 보여줄 것입니다. 예를 들어 R은 꿈꿔왔던 모든 삼각 함수를 제공하지만 데이터 과학에는 거의 필요하지 않으므로 여기에는 나열하지 않습니다.

13.4.1 산술 및 재활용 규칙

Chapter 2 에서 산술(+, -, *, /, ^)의 기초를 소개했고 그 이후로 많이 사용했습니다. 이 함수들은 초등학교에서 배운 것을 수행하기 때문에 엄청난 설명이 필요하지 않습니다. 하지만 왼쪽과 오른쪽의 길이가 다를 때 어떤 일이 일어나는지 결정하는 재활용 규칙(recycling rules) 에 대해 잠시 이야기해야 합니다. 이것은 flights |> mutate(air_time = air_time / 60)과 같은 작업에 중요합니다. / 왼쪽에는 336,776개의 숫자가 있지만 오른쪽에는 하나만 있기 때문입니다.

R은 짧은 벡터를 재활용(recycling), 즉 반복하여 길이가 일치하지 않는 것을 처리합니다. 데이터 프레임 외부에서 몇 가지 벡터를 생성하면 이 작동 방식을 더 쉽게 볼 수 있습니다:

x <- c(1, 2, 10, 20)
x / 5
#> [1] 0.2 0.4 2.0 4.0
# 다음의 단축 표현입니다
x / c(5, 5, 5, 5)
#> [1] 0.2 0.4 2.0 4.0

일반적으로 단일 숫자(즉, 길이 1의 벡터)만 재활용하고 싶지만 R은 더 짧은 길이의 벡터를 재활용합니다. 보통(항상은 아니지만) 긴 벡터가 짧은 벡터의 배수가 아닌 경우 경고를 제공합니다:

x * c(1, 2)
#> [1]  1  4 10 40
x * c(1, 2, 3)
#> Warning in x * c(1, 2, 3): longer object length is not a multiple of shorter
#> object length
#> [1]  1  4 30 20

이러한 재활용 규칙은 논리 비교(==, <, <=, >, >=, !=)에도 적용되며, 실수로 %in% 대신 ==를 사용하고 데이터 프레임의 행 수가 불행한 경우 놀라운 결과로 이어질 수 있습니다. 예를 들어 1월과 2월의 모든 항공편을 찾으려는 이 코드를 살펴보세요:

flights |> 
  filter(month == c(1, 2))
#> # A tibble: 25,977 × 19
#>    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#>   <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
#> 1  2013     1     1      517            515         2      830            819
#> 2  2013     1     1      542            540         2      923            850
#> 3  2013     1     1      554            600        -6      812            837
#> 4  2013     1     1      555            600        -5      913            854
#> 5  2013     1     1      557            600        -3      838            846
#> 6  2013     1     1      558            600        -2      849            851
#> # ℹ 25,971 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

코드는 오류 없이 실행되지만 원하는 것을 반환하지 않습니다. 재활용 규칙 때문에 홀수 행에서 1월에 출발한 항공편과 짝수 행에서 2월에 출발한 항공편을 찾습니다. 그리고 불행히도 flights의 행 수가 짝수이므로 경고가 없습니다.

이러한 유형의 침묵의 실패로부터 보호하기 위해 대부분의 tidyverse 함수는 단일 값만 재활용하는 더 엄격한 형태의 재활용을 사용합니다. 불행히도 핵심 계산이 filter()가 아니라 기본 R 함수 ==에 의해 수행되므로 여기에서는 도움이 되지 않습니다.

13.4.2 최소 및 최대

산술 함수는 한 쌍의 변수와 함께 작동합니다. 밀접하게 관련된 두 함수는 pmin()pmax()이며, 두 개 이상의 변수가 주어지면 각 행에서 가장 작은 값이나 가장 큰 값을 반환합니다:

df <- tribble(
  ~x, ~y,
  1,  3,
  5,  2,
  7, NA,
)

df |> 
  mutate(
    min = pmin(x, y, na.rm = TRUE),
    max = pmax(x, y, na.rm = TRUE)
  )
#> # A tibble: 3 × 4
#>       x     y   min   max
#>   <dbl> <dbl> <dbl> <dbl>
#> 1     1     3     1     3
#> 2     5     2     2     5
#> 3     7    NA     7     7

이것들은 여러 관측값을 받아 단일 값을 반환하는 요약 함수 min()max()와는 다릅니다. 모든 최소값과 모든 최대값이 동일한 값을 가질 때 잘못된 형식을 사용했음을 알 수 있습니다:

df |> 
  mutate(
    min = min(x, y, na.rm = TRUE),
    max = max(x, y, na.rm = TRUE)
  )
#> # A tibble: 3 × 4
#>       x     y   min   max
#>   <dbl> <dbl> <dbl> <dbl>
#> 1     1     3     1     7
#> 2     5     2     1     7
#> 3     7    NA     1     7

13.4.3 모듈러 연산

모듈러 연산은 소수점에 대해 배우기 전에 수행했던 수학 유형, 즉 정수와 나머지를 산출하는 나눗셈의 기술 이름입니다. R에서 %/%는 정수 나눗셈을 수행하고 %%는 나머지를 계산합니다:

1:10 %/% 3
#>  [1] 0 0 1 1 1 2 2 2 3 3
1:10 %% 3
#>  [1] 1 2 0 1 2 0 1 2 0 1

모듈러 연산은 flights 데이터셋에 편리합니다. sched_dep_time 변수를 hourminute로 푸는 데 사용할 수 있기 때문입니다:

flights |> 
  mutate(
    hour = sched_dep_time %/% 100,
    minute = sched_dep_time %% 100,
    .keep = "used"
  )
#> # A tibble: 336,776 × 3
#>   sched_dep_time  hour minute
#>            <int> <dbl>  <dbl>
#> 1            515     5     15
#> 2            529     5     29
#> 3            540     5     40
#> 4            545     5     45
#> 5            600     6      0
#> 6            558     5     58
#> # ℹ 336,770 more rows

이것을 Section 12.4mean(is.na(x)) 트릭과 결합하여 취소된 항공편의 비율이 하루 동안 어떻게 변하는지 확인할 수 있습니다. 결과는 Figure 13.1 에 나와 있습니다.

flights |> 
  group_by(hour = sched_dep_time %/% 100) |> 
  summarize(prop_cancelled = mean(is.na(dep_time)), n = n()) |> 
  filter(hour > 1) |> 
  ggplot(aes(x = hour, y = prop_cancelled)) +
  geom_line(color = "grey50") + 
  geom_point(aes(size = n))
하루 동안 취소된 항공편의 비율이 어떻게 변하는지 보여주는 선 플롯.  비율은 오전 5시에 약 0.5%로 낮게 시작한 다음 하루 종일 꾸준히 증가하여  오후 7시에 4%로 정점을 찍습니다. 그런 다음 취소된 항공편의 비율은 급격히  떨어져 자정 무렵에는 약 1%로 떨어집니다.
Figure 13.1: x축에 예정된 출발 시간, y축에 취소된 항공편의 비율이 있는 선 플롯. 취소는 오후 8시까지 하루 종일 누적되는 것 같으며, 매우 늦은 항공편은 취소될 가능성이 훨씬 적습니다.

13.4.4 로그

로그는 여러 자릿수(orders of magnitude)에 걸친 데이터를 처리하고 지수 성장을 선형 성장으로 변환하는 데 매우 유용한 변환입니다. R에서는 세 가지 로그를 선택할 수 있습니다: log()(자연 로그, 밑 e), log2()(밑 2), log10()(밑 10). log2() 또는 log10()을 사용하는 것이 좋습니다. log2()는 로그 척도에서 1의 차이가 원래 척도에서 두 배에 해당하고 -1의 차이가 절반에 해당하기 때문에 해석하기 쉽습니다. 반면 log10()은 역변환하기 쉽습니다(예: 3은 10^3 = 1000). log()의 역함수는 exp()입니다. log2() 또는 log10()의 역함수를 계산하려면 2^ 또는 10^를 사용해야 합니다.

13.4.5 반올림

숫자를 가장 가까운 정수로 반올림하려면 round(x)를 사용하세요:

round(123.456)
#> [1] 123

두 번째 인수 digits로 반올림의 정밀도를 제어할 수 있습니다. round(x, digits)는 가장 가까운 10^-n으로 반올림하므로 digits = 2는 가장 가까운 0.01로 반올림합니다. 이 정의는 round(x, -3)이 가장 가까운 천 단위로 반올림한다는 것을 의미하므로 유용합니다:

round(123.456, 2)  # 두 자리
#> [1] 123.46
round(123.456, 1)  # 한 자리
#> [1] 123.5
round(123.456, -1) # 가장 가까운 십 단위로 반올림
#> [1] 120
round(123.456, -2) # 가장 가까운 백 단위로 반올림
#> [1] 100

round()에는 언뜻 보기에 놀라운 이상한 점이 하나 있습니다:

round(c(1.5, 2.5))
#> [1] 2 2

round()는 “반을 짝수로 반올림(round half to even)” 또는 은행가 반올림(Banker’s rounding)이라고 알려진 방법을 사용합니다: 숫자가 두 정수 사이의 중간에 있으면 짝수 정수로 반올림됩니다. 이것은 반올림을 편향되지 않게 유지하기 때문에 좋은 전략입니다. 0.5의 절반은 올림되고 절반은 내림됩니다.

round()는 항상 내림하는 floor()와 항상 올림하는 ceiling()과 짝을 이룹니다:

x <- 123.456

floor(x)
#> [1] 123
ceiling(x)
#> [1] 124

이 함수들에는 digits 인수가 없으므로 대신 축소하고 반올림한 다음 다시 확대할 수 있습니다:

# 가장 가까운 두 자리로 내림
floor(x / 0.01) * 0.01
#> [1] 123.45
# 가장 가까운 두 자리로 올림
ceiling(x / 0.01) * 0.01
#> [1] 123.46

round()를 다른 숫자의 배수로 반올림하려는 경우 동일한 기술을 사용할 수 있습니다:

# 가장 가까운 4의 배수로 반올림
round(x / 4) * 4
#> [1] 124

# 가장 가까운 0.25로 반올림
round(x / 0.25) * 0.25
#> [1] 123.5

13.4.6 숫자를 범위로 자르기

수치형 벡터를 이산 버킷으로 나누려면(일명 비닝) cut()1을 사용하세요:

x <- c(1, 2, 5, 10, 15, 20)
cut(x, breaks = c(0, 5, 10, 15, 20))
#> [1] (0,5]   (0,5]   (0,5]   (5,10]  (10,15] (15,20]
#> Levels: (0,5] (5,10] (10,15] (15,20]

나누기(breaks)는 균등하게 간격을 둘 필요가 없습니다:

cut(x, breaks = c(0, 5, 10, 100))
#> [1] (0,5]    (0,5]    (0,5]    (5,10]   (10,100] (10,100]
#> Levels: (0,5] (5,10] (10,100]

선택적으로 자신만의 labels를 제공할 수 있습니다. labelsbreaks보다 하나 적어야 한다는 점에 유의하세요.

cut(x, 
  breaks = c(0, 5, 10, 15, 20), 
  labels = c("sm", "md", "lg", "xl")
)
#> [1] sm sm sm md lg xl
#> Levels: sm md lg xl

나누기 범위 밖의 값은 NA가 됩니다:

y <- c(NA, -10, 5, 10, 30)
cut(y, breaks = c(0, 5, 10, 15, 20))
#> [1] <NA>   <NA>   (0,5]  (5,10] <NA>  
#> Levels: (0,5] (5,10] (10,15] (15,20]

구간이 [a, b)인지 (a, b]인지, 가장 낮은 구간이 [a, b]여야 하는지 제어하는 rightinclude.lowest와 같은 다른 유용한 인수에 대해서는 설명서를 참조하세요.

13.4.7 누적 및 롤링 집계

기본 R은 누적 합, 곱, 최소 및 최대를 위해 cumsum(), cumprod(), cummin(), cummax()를 제공합니다. dplyr은 누적 평균을 위한 cummean()을 제공합니다. 누적 합이 실제로 가장 많이 나오는 경향이 있습니다:

x <- 1:10
cumsum(x)
#>  [1]  1  3  6 10 15 21 28 36 45 55

더 복잡한 롤링 또는 슬라이딩 집계가 필요한 경우 slider 패키지를 사용해 보세요.

13.4.8 연습문제

  1. Figure 13.1 를 생성하는 데 사용된 코드의 각 줄이 무엇을 하는지 말로 설명하세요.

  2. R은 어떤 삼각 함수를 제공합니까? 이름을 추측하고 설명서를 찾아보세요. 도(degrees)를 사용합니까, 아니면 라디안(radians)을 사용합니까?

  3. 현재 dep_timesched_dep_time은 보기에 편리하지만 실제로는 연속적인 숫자가 아니기 때문에 계산하기 어렵습니다. 아래 코드를 실행하여 기본적인 문제를 볼 수 있습니다. 각 시간 사이에 간격이 있습니다.

    flights |> 
      filter(month == 1, day == 1) |> 
      ggplot(aes(x = sched_dep_time, y = dep_delay)) +
      geom_point()

    더 진실한 시간 표현(소수 시간 또는 자정 이후 분)으로 변환하세요.

  4. dep_timearr_time을 가장 가까운 5분 단위로 반올림하세요.

13.5 일반적인 변환

다음 섹션에서는 수치형 벡터에 자주 사용되지만 다른 모든 열 유형에도 적용할 수 있는 몇 가지 일반적인 변환을 설명합니다.

13.5.1 순위(Ranks)

dplyr은 SQL에서 영감을 얻은 여러 순위 함수를 제공하지만 항상 dplyr::min_rank()로 시작해야 합니다. 동점을 처리하는 일반적인 방법(예: 1등, 2등, 2등, 4등)을 사용합니다.

x <- c(1, 5, 5, 17, 22, NA)
min_rank(x)
#> [1]  1  2  2  4  5 NA

가장 작은 값이 가장 낮은 순위를 얻습니다. 가장 큰 값에 가장 작은 순위를 부여하려면 desc(x)를 사용하세요:

min_rank(desc(x))
#> [1]  5  3  3  2  1 NA

min_rank()가 필요한 작업을 수행하지 않으면 변형인 dplyr::row_number(), dplyr::dense_rank(), dplyr::percent_rank(), dplyr::cume_dist()를 살펴보세요. 자세한 내용은 설명서를 참조하세요.

df <- tibble(x = x)
df |> 
  mutate(
    row_number = row_number(x),
    dense_rank = dense_rank(x),
    percent_rank = percent_rank(x),
    cume_dist = cume_dist(x)
  )
#> # A tibble: 6 × 5
#>       x row_number dense_rank percent_rank cume_dist
#>   <dbl>      <int>      <int>        <dbl>     <dbl>
#> 1     1          1          1         0          0.2
#> 2     5          2          2         0.25       0.6
#> 3     5          3          2         0.25       0.6
#> 4    17          4          3         0.75       0.8
#> 5    22          5          4         1          1  
#> 6    NA         NA         NA        NA         NA

기본 R의 rank()에 적절한 ties.method 인수를 선택하여 동일한 결과를 많이 얻을 수 있습니다. 아마도 NANA로 유지하려면 na.last = "keep"을 설정하고 싶을 것입니다.

row_number()는 dplyr 동사 내부에서 인수 없이 사용할 수도 있습니다. 이 경우 “현재” 행의 번호를 제공합니다. %% 또는 %/%와 결합하면 데이터를 비슷한 크기의 그룹으로 나누는 유용한 도구가 될 수 있습니다:

df <- tibble(id = 1:10)

df |> 
  mutate(
    row0 = row_number() - 1,
    three_groups = row0 %% 3,
    three_in_each_group = row0 %/% 3
  )
#> # A tibble: 10 × 4
#>      id  row0 three_groups three_in_each_group
#>   <int> <dbl>        <dbl>               <dbl>
#> 1     1     0            0                   0
#> 2     2     1            1                   0
#> 3     3     2            2                   0
#> 4     4     3            0                   1
#> 5     5     4            1                   1
#> 6     6     5            2                   1
#> # ℹ 4 more rows

13.5.2 오프셋(Offsets)

dplyr::lead()dplyr::lag()를 사용하면 “현재” 값 바로 앞이나 뒤의 값을 참조할 수 있습니다. 입력과 길이가 같은 벡터를 반환하며 시작이나 끝에 NA가 채워집니다:

x <- c(2, 5, 11, 11, 19, 35)
lag(x)
#> [1] NA  2  5 11 11 19
lead(x)
#> [1]  5 11 11 19 35 NA
  • x - lag(x)는 현재 값과 이전 값의 차이를 제공합니다.

    x - lag(x)
    #> [1] NA  3  6  0  8 16
  • x == lag(x)는 현재 값이 언제 변경되는지 알려줍니다.

    x == lag(x)
    #> [1]    NA FALSE FALSE  TRUE FALSE FALSE

두 번째 인수 n을 사용하여 한 위치 이상 리드하거나 래그할 수 있습니다.

13.5.3 연속 식별자

때로는 이벤트가 발생할 때마다 새 그룹을 시작하고 싶을 때가 있습니다. 예를 들어 웹사이트 데이터를 볼 때 이벤트를 세션으로 나누고 싶은데, 마지막 활동 이후 x분 이상의 간격이 있은 후 새 세션을 시작하는 것이 일반적입니다. 예를 들어 누군가가 웹사이트를 방문한 시간이 있다고 상상해 보세요:

events <- tibble(
  time = c(0, 1, 2, 3, 5, 10, 12, 15, 17, 19, 20, 27, 28, 30)
)

그리고 각 이벤트 사이의 시간을 계산하고 자격을 갖출 만큼 충분히 큰 간격이 있는지 파악했습니다:

events <- events |> 
  mutate(
    diff = time - lag(time, default = first(time)),
    has_gap = diff >= 5
  )
events
#> # A tibble: 14 × 3
#>    time  diff has_gap
#>   <dbl> <dbl> <lgl>  
#> 1     0     0 FALSE  
#> 2     1     1 FALSE  
#> 3     2     1 FALSE  
#> 4     3     1 FALSE  
#> 5     5     2 FALSE  
#> 6    10     5 TRUE   
#> # ℹ 8 more rows

하지만 그 논리형 벡터에서 어떻게 group_by()할 수 있는 것으로 갈 수 있을까요? Section 13.4.7cumsum()이 구해주러 옵니다. 갭, 즉 has_gapTRUE이면 group이 1씩 증가하기 때문입니다(Section 12.4.2):

events |> mutate(
  group = cumsum(has_gap)
)
#> # A tibble: 14 × 4
#>    time  diff has_gap group
#>   <dbl> <dbl> <lgl>   <int>
#> 1     0     0 FALSE       0
#> 2     1     1 FALSE       0
#> 3     2     1 FALSE       0
#> 4     3     1 FALSE       0
#> 5     5     2 FALSE       0
#> 6    10     5 TRUE        1
#> # ℹ 8 more rows

그룹화 변수를 만드는 또 다른 접근 방식은 consecutive_id()로, 인수 중 하나가 변경될 때마다 새 그룹을 시작합니다. 예를 들어 이 stackoverflow 질문에서 영감을 받아 반복되는 값이 많은 데이터 프레임이 있다고 상상해 보세요:

df <- tibble(
  x = c("a", "a", "a", "b", "c", "c", "d", "e", "a", "a", "b", "b"),
  y = c(1, 2, 3, 2, 4, 1, 3, 9, 4, 8, 10, 199)
)

반복되는 각 x의 첫 번째 행을 유지하려면 group_by(), consecutive_id(), slice_head()를 사용할 수 있습니다:

df |> 
  group_by(id = consecutive_id(x)) |> 
  slice_head(n = 1)
#> # A tibble: 7 × 3
#> # Groups:   id [7]
#>   x         y    id
#>   <chr> <dbl> <int>
#> 1 a         1     1
#> 2 b         2     2
#> 3 c         4     3
#> 4 d         3     4
#> 5 e         9     5
#> 6 a         4     6
#> # ℹ 1 more row

13.5.4 연습문제

  1. 순위 함수를 사용하여 가장 많이 지연된 10개의 항공편을 찾으세요. 동점은 어떻게 처리하고 싶습니까? min_rank()에 대한 설명을 주의 깊게 읽으세요.

  2. 어떤 비행기(tailnum)가 정시 기록이 가장 나쁩니까?

  3. 지연을 최대한 피하고 싶다면 하루 중 언제 비행해야 합니까?

  4. flights |> group_by(dest) |> filter(row_number() < 4)는 무엇을 합니까? flights |> group_by(dest) |> filter(row_number(dep_delay) < 4)는 무엇을 합니까?

  5. 각 목적지에 대해 총 지연 분을 계산하세요. 각 항공편에 대해 목적지에 대한 총 지연의 비율을 계산하세요.

  6. 지연은 일반적으로 시간적으로 상관관계가 있습니다: 초기 지연을 유발한 문제가 해결된 후에도 이전 항공편이 떠날 수 있도록 나중 항공편이 지연됩니다. lag()를 사용하여 한 시간 동안의 평균 항공편 지연이 이전 시간의 평균 지연과 어떻게 관련되어 있는지 탐색하세요.

    flights |> 
      mutate(hour = dep_time %/% 100) |> 
      group_by(year, month, day, hour) |> 
      summarize(
        dep_delay = mean(dep_delay, na.rm = TRUE),
        n = n(),
        .groups = "drop"
      ) |> 
      filter(n > 5)
  7. 각 목적지를 살펴보세요. 의심스럽게 빠른 항공편(즉, 잠재적인 데이터 입력 오류를 나타내는 항공편)을 찾을 수 있습니까? 해당 목적지로 가는 가장 짧은 항공편에 대한 항공편의 비행 시간을 계산하세요. 공중에서 가장 많이 지연된 항공편은 무엇입니까?

  8. 적어도 두 항공사가 운항하는 모든 목적지를 찾으세요. 그 목적지들을 사용하여 동일한 목적지에 대한 성과를 기반으로 항공사의 상대적 순위를 매기세요.

13.6 숫자 요약

이미 소개한 개수, 평균, 합계만 사용해도 많은 것을 얻을 수 있지만 R은 다른 많은 유용한 요약 함수를 제공합니다. 유용할 만한 선택 항목은 다음과 같습니다.

13.6.1 중심

지금까지 우리는 주로 값 벡터의 중심을 요약하기 위해 mean()을 사용했습니다. Section 3.6 에서 보았듯이 평균은 합계를 개수로 나눈 것이기 때문에 소수의 비정상적으로 높거나 낮은 값에도 민감합니다. 대안은 median()을 사용하는 것입니다. 이는 벡터의 “중간”에 있는 값, 즉 값의 50%는 그 위에 있고 50%는 그 아래에 있는 값을 찾습니다. 관심 있는 변수의 분포 모양에 따라 평균 또는 중앙값이 중심의 더 나은 척도가 될 수 있습니다. 예를 들어 대칭 분포의 경우 일반적으로 평균을 보고하는 반면 치우친 분포의 경우 일반적으로 중앙값을 보고합니다.

Figure 13.2 은 각 목적지에 대한 평균 대 중앙값 출발 지연(분)을 비교합니다. 중앙값 지연은 항상 평균 지연보다 작습니다. 항공편은 때때로 몇 시간 늦게 출발하지만 몇 시간 일찍 출발하는 경우는 없기 때문입니다.

flights |>
  group_by(year, month, day) |>
  summarize(
    mean = mean(dep_delay, na.rm = TRUE),
    median = median(dep_delay, na.rm = TRUE),
    n = n(),
    .groups = "drop"
  ) |> 
  ggplot(aes(x = mean, y = median)) + 
  geom_abline(slope = 1, intercept = 0, color = "white", linewidth = 2) +
  geom_point()
모든 점은 45도 선 아래에 위치하며, 이는 중앙값 지연이 항상 평균  지연보다 작음을 의미합니다. 대부분의 점은 평균 [0, 20] 및 중앙값 [-5, 5]의  밀집된 영역에 모여 있습니다. 평균 지연이 증가함에 따라 중앙값의 확산도  증가합니다. 평균 ~60, 중앙값 ~30, 평균 ~85, 중앙값 ~55인 두 개의  이상치 점이 있습니다.
Figure 13.2: 일일 출발 지연을 평균 대신 중앙값으로 요약할 때의 차이를 보여주는 산점도.

최빈값(mode), 즉 가장 흔한 값에 대해서도 궁금할 수 있습니다. 이것은 매우 간단한 경우에만 잘 작동하는 요약(그래서 고등학교에서 배웠을 수도 있음)이지만 많은 실제 데이터셋에서는 잘 작동하지 않습니다. 데이터가 이산형인 경우 가장 흔한 값이 여러 개 있을 수 있고, 데이터가 연속형인 경우 모든 값이 아주 조금씩 다르기 때문에 가장 흔한 값이 없을 수 있습니다. 이러한 이유로 통계학자들은 최빈값을 잘 사용하지 않는 경향이 있으며 기본 R에는 포함된 최빈값 함수가 없습니다2.

13.6.2 최소, 최대 및 분위수

중심 이외의 위치에 관심이 있다면 어떻게 해야 할까요? min()max()는 가장 큰 값과 가장 작은 값을 제공합니다. 또 다른 강력한 도구는 중앙값의 일반화인 quantile()입니다. quantile(x, 0.25)는 값의 25%보다 큰 x 값을 찾고, quantile(x, 0.5)는 중앙값과 동일하며, quantile(x, 0.95)는 값의 95%보다 큰 값을 찾습니다.

flights 데이터의 경우 가장 많이 지연된 항공편의 5%는 매우 극단적일 수 있으므로 최대값보다는 지연의 95% 분위수를 보고 싶을 수 있습니다.

flights |>
  group_by(year, month, day) |>
  summarize(
    max = max(dep_delay, na.rm = TRUE),
    q95 = quantile(dep_delay, 0.95, na.rm = TRUE),
    .groups = "drop"
  )
#> # A tibble: 365 × 5
#>    year month   day   max   q95
#>   <int> <int> <int> <dbl> <dbl>
#> 1  2013     1     1   853  70.1
#> 2  2013     1     2   379  85  
#> 3  2013     1     3   291  68  
#> 4  2013     1     4   288  60  
#> 5  2013     1     5   327  41  
#> 6  2013     1     6   202  51  
#> # ℹ 359 more rows

13.6.3 산포(Spread)

때로는 데이터의 대부분이 어디에 있는지보다는 어떻게 퍼져 있는지에 관심이 있을 수 있습니다. 일반적으로 사용되는 두 가지 요약은 표준 편차 sd(x)와 사분위수 범위 IQR()입니다. sd()는 아마 이미 익숙할 것이므로 여기서 설명하지 않겠지만 IQR()은 생소할 수 있습니다. 이것은 quantile(x, 0.75) - quantile(x, 0.25)이며 데이터의 중간 50%를 포함하는 범위를 제공합니다.

이것을 사용하여 flights 데이터의 작은 이상함을 드러낼 수 있습니다. 공항은 항상 같은 장소에 있기 때문에 출발지와 목적지 사이의 거리 산포가 0일 것이라고 예상할 수 있습니다. 그러나 아래 코드는 EGE 공항에 대한 데이터 이상함을 보여줍니다:

flights |> 
  group_by(origin, dest) |> 
  summarize(
    distance_iqr = IQR(distance), 
    n = n(),
    .groups = "drop"
  ) |> 
  filter(distance_iqr > 0)
#> # A tibble: 2 × 4
#>   origin dest  distance_iqr     n
#>   <chr>  <chr>        <dbl> <int>
#> 1 EWR    EGE              1   110
#> 2 JFK    EGE              1   103

13.6.4 분포

위에서 설명한 모든 요약 통계는 분포를 단일 숫자로 줄이는 방법이라는 점을 기억할 가치가 있습니다. 이는 근본적으로 축소적이며 잘못된 요약을 선택하면 그룹 간의 중요한 차이를 쉽게 놓칠 수 있음을 의미합니다. 그렇기 때문에 요약 통계를 확정하기 전에 항상 분포를 시각화하는 것이 좋습니다.

Figure 13.3 는 출발 지연의 전체 분포를 보여줍니다. 분포가 너무 치우쳐 있어서 데이터의 대부분을 보려면 확대해야 합니다. 이는 평균이 좋은 요약이 아닐 수 있으며 대신 중앙값을 선호할 수 있음을 시사합니다.

`dep_delay`의 두 히스토그램. 왼쪽에서는 0 주변에 매우 큰 스파이크가  있고 막대 높이가 급격히 감소하며 플롯 대부분에서 막대가 너무 짧아 볼 수  없다는 것 외에는 패턴을 보기가 매우 어렵습니다. 2시간 이상의 지연을  버린 오른쪽에서는 스파이크가 0보다 약간 아래에서 발생한다는 것을 볼 수  있지만(즉, 대부분의 항공편은 몇 분 일찍 출발함), 그 후에도 여전히 매우  가파른 감소가 있습니다.
Figure 13.3: (왼쪽) 전체 데이터의 히스토그램은 매우 치우쳐 있어 세부 정보를 얻기 어렵습니다. (오른쪽) 2시간 미만의 지연으로 확대하면 대부분의 관측값에서 무슨 일이 일어나고 있는지 볼 수 있습니다.

하위 그룹의 분포가 전체와 유사한지 확인하는 것도 좋습니다. 다음 플롯에서는 dep_delay의 365개 빈도 다각형(매일 하나씩)이 겹쳐져 있습니다. 분포는 일반적인 패턴을 따르는 것으로 보이며 매일 동일한 요약을 사용해도 괜찮음을 시사합니다.

flights |>
  filter(dep_delay < 120) |> 
  ggplot(aes(x = dep_delay, group = interaction(day, month))) + 
  geom_freqpoly(binwidth = 5, alpha = 1/5)

`dep_delay`의 분포는 오른쪽으로 매우 치우쳐 있으며 0보다 약간 작은  강한 봉우리가 있습니다. 365개의 빈도 다각형이 대부분 겹쳐져 두꺼운  검은 띠를 형성합니다.

작업 중인 데이터에 특별히 맞춤화된 사용자 정의 요약을 탐색하는 것을 두려워하지 마세요. 이 경우 일찍 출발한 항공편과 늦게 출발한 항공편을 별도로 요약하거나 값이 너무 심하게 치우쳐 있으므로 로그 변환을 시도해 볼 수 있습니다. 마지막으로 Section 3.6 에서 배운 내용을 잊지 마세요: 수치 요약을 생성할 때마다 각 그룹의 관측값 수를 포함하는 것이 좋습니다.

13.6.5 위치

수치형 벡터에 유용하지만 다른 모든 유형의 값과도 작동하는 요약 유형이 하나 더 있습니다. 특정 위치의 값을 추출하는 것입니다: first(x), last(x), nth(x, n).

예를 들어 매일 첫 번째, 다섯 번째, 마지막 출발을 찾을 수 있습니다:

flights |> 
  group_by(year, month, day) |> 
  summarize(
    first_dep = first(dep_time, na_rm = TRUE), 
    fifth_dep = nth(dep_time, 5, na_rm = TRUE),
    last_dep = last(dep_time, na_rm = TRUE)
  )
#> `summarise()` has grouped output by 'year', 'month'. You can override using
#> the `.groups` argument.
#> # A tibble: 365 × 6
#> # Groups:   year, month [12]
#>    year month   day first_dep fifth_dep last_dep
#>   <int> <int> <int>     <int>     <int>    <int>
#> 1  2013     1     1       517       554     2356
#> 2  2013     1     2        42       535     2354
#> 3  2013     1     3        32       520     2349
#> 4  2013     1     4        25       531     2358
#> 5  2013     1     5        14       534     2357
#> 6  2013     1     6        16       555     2355
#> # ℹ 359 more rows

(참고: dplyr 함수는 _를 사용하여 함수 및 인수 이름의 구성 요소를 구분하므로 이러한 함수는 na.rm 대신 na_rm을 사용합니다.)

Section 27.2 에서 다시 다룰 [에 익숙하다면 이러한 함수가 필요한지 궁금할 수 있습니다. 세 가지 이유가 있습니다: default 인수를 사용하면 지정된 위치가 존재하지 않는 경우 기본값을 제공할 수 있고, order_by 인수를 사용하면 행의 순서를 로컬에서 재정의할 수 있으며, na_rm 인수를 사용하면 결측값을 삭제할 수 있습니다.

위치에서 값을 추출하는 것은 순위에 대한 필터링을 보완합니다. 필터링은 각 관측값이 별도의 행에 있는 모든 변수를 제공합니다:

flights |> 
  group_by(year, month, day) |> 
  mutate(r = min_rank(sched_dep_time)) |> 
  filter(r %in% c(1, max(r)))
#> # A tibble: 1,195 × 20
#> # Groups:   year, month, day [365]
#>    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#>   <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
#> 1  2013     1     1      517            515         2      830            819
#> 2  2013     1     1     2353           2359        -6      425            445
#> 3  2013     1     1     2353           2359        -6      418            442
#> 4  2013     1     1     2356           2359        -3      425            437
#> 5  2013     1     2       42           2359        43      518            442
#> 6  2013     1     2      458            500        -2      703            650
#> # ℹ 1,189 more rows
#> # ℹ 12 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

13.6.6 mutate()와 함께

이름에서 알 수 있듯이 요약 함수는 일반적으로 summarize()와 짝을 이룹니다. 그러나 Section 13.4.1 에서 논의한 재활용 규칙 때문에 mutate()와 유용하게 짝을 이룰 수도 있으며, 특히 어떤 종류의 그룹 표준화를 수행하려는 경우에 그렇습니다. 예를 들어:

  • x / sum(x)는 전체에 대한 비율을 계산합니다.
  • (x - mean(x)) / sd(x)는 Z-점수(평균 0 및 표준 편차 1로 표준화)를 계산합니다.
  • (x - min(x)) / (max(x) - min(x))는 범위 [0, 1]로 표준화합니다.
  • x / first(x)는 첫 번째 관측값을 기반으로 지수를 계산합니다.

13.6.7 연습문제

  1. 항공편 그룹의 전형적인 지연 특성을 평가하는 적어도 5가지 다른 방법을 브레인스토밍하세요. mean()은 언제 유용합니까? median()은 언제 유용합니까? 다른 것을 사용하고 싶을 때는 언제입니까? 도착 지연을 사용해야 할까요, 출발 지연을 사용해야 할까요? planes의 데이터를 사용하고 싶은 이유는 무엇일까요?

  2. 어떤 목적지가 공중 속도에서 가장 큰 변동을 보입니까?

  3. EGE의 모험을 더 탐구하기 위해 플롯을 만드세요. 공항이 위치를 옮겼다는 증거를 찾을 수 있습니까? 차이를 설명할 수 있는 다른 변수를 찾을 수 있습니까?

13.7 요약

여러분은 이미 숫자를 다루는 많은 도구에 익숙하며 이 장을 읽고 나면 R에서 그것들을 사용하는 방법을 알게 되었습니다. 또한 순위 및 오프셋과 같이 일반적으로 수치형 벡터에 적용되지만 배타적이지는 않은 유용한 일반 변환 몇 가지를 배웠습니다. 마지막으로 여러 수치 요약을 살펴보고 고려해야 할 몇 가지 통계적 과제에 대해 논의했습니다.

다음 두 장에 걸쳐 stringr 패키지로 문자열을 다루는 방법에 대해 자세히 알아볼 것입니다. 문자열은 큰 주제이므로 문자열의 기초에 대한 장과 정규 표현식에 대한 장 두 개를 얻습니다.


  1. ggplot2는 cut_interval(), cut_number(), cut_width()에서 일반적인 경우에 대한 몇 가지 도우미를 제공합니다. ggplot2는 인정하건대 이러한 함수가 살기에는 이상한 곳이지만 히스토그램 계산의 일부로 유용하며 tidyverse의 다른 부분이 존재하기 전에 작성되었습니다.↩︎

  2. mode() 함수는 완전히 다른 일을 합니다!↩︎