3  데이터 변형

3.1 소개

시각화는 통찰력을 생성하는 데 중요한 도구이지만, 원하는 그래프를 만드는 데 필요한 정확한 형태로 데이터를 얻는 경우는 드뭅니다. 종종 데이터로 질문에 답하기 위해 새로운 변수나 요약을 생성해야 하거나, 데이터를 다루기 조금 더 쉽게 만들기 위해 변수의 이름을 바꾸거나 관측값의 순서를 변경하고 싶을 수도 있습니다. 이 장에서는 dplyr 패키지와 2013년 뉴욕시에서 출발한 항공편에 대한 새로운 데이터셋을 사용하여 데이터 변형을 소개하며 그 모든 작업(그리고 그 이상!)을 수행하는 방법을 배울 것입니다.

이 장의 목표는 데이터 프레임을 변형하기 위한 모든 주요 도구에 대한 개요를 제공하는 것입니다. 데이터 프레임의 행과 열에서 작동하는 함수로 시작한 다음, 동사들을 결합하는 데 사용하는 중요한 도구인 파이프에 대해 더 이야기하기 위해 다시 돌아올 것입니다. 그런 다음 그룹으로 작업하는 기능을 소개할 것입니다. 이 장은 이러한 함수들이 실제로 작동하는 것을 보여주는 사례 연구로 마무리할 것입니다. 이후 장에서는 특정 유형의 데이터(예: 숫자, 문자열, 날짜)를 파고들기 시작하면서 함수들에 대해 더 자세히 다룰 것입니다.

3.1.1 선수 지식

이 장에서는 tidyverse의 또 다른 핵심 멤버인 dplyr 패키지에 초점을 맞출 것입니다. nycflights13 패키지의 데이터를 사용하여 핵심 아이디어를 설명하고 데이터를 이해하는 데 도움이 되도록 ggplot2를 사용할 것입니다.

library(nycflights13)
library(tidyverse)
#> Warning: package 'ggplot2' was built under R version 4.5.2
#> Warning: package 'readr' was built under R version 4.5.2
#> ── Attaching core tidyverse packages ───────────────────── tidyverse 2.0.0 ──
#> ✔ dplyr     1.1.4     ✔ readr     2.1.6
#> ✔ forcats   1.0.1     ✔ stringr   1.6.0
#> ✔ ggplot2   4.0.1     ✔ tibble    3.3.0
#> ✔ lubridate 1.9.4     ✔ tidyr     1.3.1
#> ✔ purrr     1.2.0     
#> ── Conflicts ─────────────────────────────────────── tidyverse_conflicts() ──
#> ✖ dplyr::filter() masks stats::filter()
#> ✖ dplyr::lag()    masks stats::lag()
#> ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

tidyverse를 로드할 때 출력되는 충돌 메시지에 주의하세요. 이 메시지는 dplyr이 기본(base) R의 일부 함수를 덮어쓴다고 알려줍니다. dplyr을 로드한 후 이러한 함수의 기본 버전을 사용하려면 전체 이름인 stats::filter()stats::lag()를 사용해야 합니다. 지금까지는 함수가 어떤 패키지에서 왔는지 거의 무시했는데, 대개는 중요하지 않기 때문입니다. 하지만 패키지를 알면 도움말을 찾고 관련 함수를 찾는 데 도움이 될 수 있으므로, 함수가 어떤 패키지에서 왔는지 정확히 해야 할 때는 R과 동일한 구문인 packagename::functionname()을 사용할 것입니다.

3.1.2 nycflights13

기본적인 dplyr 동사를 탐색하기 위해 nycflights13::flights를 사용할 것입니다. 이 데이터셋에는 2013년 뉴욕시에서 출발한 모든 336,776개의 항공편이 포함되어 있습니다. 데이터는 미국 교통통계국(Bureau of Transportation Statistics)에서 가져왔으며 ?flights에 문서화되어 있습니다.

flights
#> # A tibble: 336,776 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

flights는 티블(tibble)로, 몇 가지 일반적인 문제를 피하기 위해 tidyverse에서 사용하는 특별한 유형의 데이터 프레임입니다. 티블과 데이터 프레임의 가장 중요한 차이점은 티블이 출력되는 방식입니다. 티블은 대규모 데이터셋을 위해 설계되었으므로 처음 몇 개의 행과 한 화면에 맞는 열만 보여줍니다. 모든 것을 볼 수 있는 몇 가지 옵션이 있습니다. RStudio를 사용하는 경우 가장 편리한 방법은 아마도 View(flights)일 텐데, 이는 대화형으로 스크롤 및 필터링이 가능한 뷰를 엽니다. 그렇지 않으면 print(flights, width = Inf)를 사용하여 모든 열을 표시하거나 glimpse()를 사용할 수 있습니다:

glimpse(flights)
#> Rows: 336,776
#> Columns: 19
#> $ year           <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013…
#> $ month          <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ day            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ dep_time       <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 55…
#> $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 60…
#> $ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2,…
#> $ arr_time       <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 8…
#> $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 8…
#> $ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7,…
#> $ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6"…
#> $ flight         <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301…
#> $ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N…
#> $ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LG…
#> $ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IA…
#> $ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149…
#> $ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 73…
#> $ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6…
#> $ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59…
#> $ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-0…

두 보기 모두 변수 이름 뒤에 각 변수의 유형을 알려주는 약어가 옵니다. <int>는 정수(integer)의 줄임말이고, <dbl>은 배정밀도 실수(double, 일명 실수)의 줄임말이며, <chr>은 문자(character, 일명 문자열), <dttm>은 날짜-시간(date-time)입니다. 열에 대해 수행할 수 있는 작업은 “유형”에 크게 의존하기 때문에 이것들은 중요합니다.

3.1.3 dplyr 기초

여러분은 데이터 조작 과제의 대다수를 해결할 수 있게 해주는 주요 dplyr 동사(함수)를 배우게 될 것입니다. 하지만 개별적인 차이점을 논의하기 전에 공통점을 언급할 가치가 있습니다:

  1. 첫 번째 인수는 항상 데이터 프레임입니다.

  2. 후속 인수는 일반적으로 변수 이름(따옴표 없이)을 사용하여 작업할 열을 설명합니다.

  3. 출력은 항상 새로운 데이터 프레임입니다.

각 동사는 한 가지 일을 잘 수행하므로 복잡한 문제를 해결하려면 일반적으로 여러 동사를 결합해야 하며, 파이프 |>를 사용하여 그렇게 할 것입니다. Section 3.4 에서 파이프에 대해 더 논의하겠지만, 간단히 말해 파이프는 왼쪽에 있는 것을 오른쪽의 함수로 전달하므로 x |> f(y)f(x, y)와 동일하고 x |> f(y) |> g(z)g(f(x, y), z)와 동일합니다. 파이프를 발음하는 가장 쉬운 방법은 “then(그 다음)”입니다. 덕분에 아직 세부 사항을 배우지 않았더라도 다음 코드의 의미를 파악할 수 있습니다:

flights |>
  filter(dest == "IAH") |> 
  group_by(year, month, day) |> 
  summarize(
    arr_delay = mean(arr_delay, na.rm = TRUE)
  )

dplyr의 동사는 작업 대상에 따라 행(rows), 열(columns), 그룹(groups), 테이블(tables) 의 네 그룹으로 구성됩니다. 다음 섹션에서는 행, 열, 그룹에 대한 가장 중요한 동사를 배울 것입니다. 그런 다음 Chapter 19 에서 테이블에서 작동하는 조인(join) 동사에 대해 다시 이야기할 것입니다. 시작해 봅시다!

3.2

데이터셋의 행에서 작동하는 가장 중요한 동사는 순서를 변경하지 않고 어떤 행이 존재하는지를 변경하는 filter()와 어떤 행이 존재하는지를 변경하지 않고 행의 순서를 변경하는 arrange()입니다. 두 함수 모두 행에만 영향을 미치며 열은 변경되지 않습니다. 또한 고유한 값을 가진 행을 찾는 distinct()에 대해서도 논의할 것입니다. arrange()filter()와 달리 선택적으로 열을 수정할 수도 있습니다.

3.2.1 filter()

filter()를 사용하면 열의 값에 따라 행을 유지할 수 있습니다1. 첫 번째 인수는 데이터 프레임입니다. 두 번째 및 후속 인수는 행을 유지하기 위해 참이어야 하는 조건입니다. 예를 들어, 120분(2시간) 이상 늦게 출발한 모든 항공편을 찾을 수 있습니다:

flights |> 
  filter(dep_delay > 120)
#> # A tibble: 9,723 × 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      848           1835       853     1001           1950
#> 2  2013     1     1      957            733       144     1056            853
#> 3  2013     1     1     1114            900       134     1447           1222
#> 4  2013     1     1     1540           1338       122     2020           1825
#> 5  2013     1     1     1815           1325       290     2120           1542
#> 6  2013     1     1     1842           1422       260     1958           1535
#> # ℹ 9,717 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

>(초과) 외에도 >=(이상), <(미만), <=(이하), ==(같음), !=(같지 않음)를 사용할 수 있습니다. 또한 & 또는 ,를 사용하여 “그리고(and)”(두 조건 모두 확인)를 나타내거나 |를 사용하여 “또는(or)”(둘 중 하나 확인)을 나타내어 조건을 결합할 수도 있습니다:

# 1월 1일에 출발한 항공편
flights |> 
  filter(month == 1 & day == 1)
#> # A tibble: 842 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 836 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

# 1월 또는 2월에 출발한 항공편
flights |> 
  filter(month == 1 | month == 2)
#> # A tibble: 51,955 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 51,949 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

|==를 결합할 때 유용한 단축키가 있습니다: %in%. 변수가 오른쪽 값 중 하나와 같으면 행을 유지합니다:

# 1월 또는 2월에 출발한 항공편을 선택하는 더 짧은 방법
flights |> 
  filter(month %in% c(1, 2))
#> # A tibble: 51,955 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 51,949 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

Chapter 12 에서 이러한 비교 및 논리 연산자에 대해 더 자세히 다시 다룰 것입니다.

filter()를 실행하면 dplyr은 필터링 작업을 실행하여 새 데이터 프레임을 생성한 다음 출력합니다. dplyr 함수는 입력을 절대 수정하지 않으므로 기존 flights 데이터셋을 수정하지 않습니다. 결과를 저장하려면 할당 연산자 <-를 사용해야 합니다:

jan1 <- flights |> 
  filter(month == 1 & day == 1)

3.2.2 일반적인 실수

R을 처음 시작할 때 가장 저지르기 쉬운 실수는 동일성(equality)을 테스트할 때 == 대신 =를 사용하는 것입니다. filter()는 이런 일이 발생하면 알려줍니다:

flights |> 
  filter(month = 1)
#> Error in `filter()`:
#> ! We detected a named input.
#> ℹ This usually means that you've used `=` instead of `==`.
#> ℹ Did you mean `month == 1`?

또 다른 실수는 영어에서처럼 “또는(or)” 문장을 작성하는 것입니다:

flights |> 
  filter(month == 1 | 2)

이것은 오류를 발생시키지 않는다는 의미에서 “작동”하지만, |가 먼저 조건 month == 1을 확인한 다음 조건 2를 확인하는데, 이는 확인할 합리적인 조건이 아니므로 원하는 작업을 수행하지 않습니다. 여기서 무슨 일이 일어나고 있는지와 그 이유에 대해서는 Section 12.3.2 에서 더 자세히 배울 것입니다.

3.2.3 arrange()

arrange()는 열의 값을 기준으로 행의 순서를 변경합니다. 데이터 프레임과 정렬 기준으로 삼을 일련의 열 이름(또는 더 복잡한 표현식)을 취합니다. 둘 이상의 열 이름을 제공하면 추가 열 각각이 이전 열 값의 동점(ties)을 깨는 데 사용됩니다. 예를 들어, 다음 코드는 4개의 열에 걸쳐 있는 출발 시간을 기준으로 정렬합니다. 가장 빠른 연도를 먼저 얻은 다음, 같은 연도 내에서 가장 빠른 달 등을 얻습니다.

flights |> 
  arrange(year, month, day, dep_time)
#> # A tibble: 336,776 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

arrange() 내부의 열에 desc()를 사용하여 내림차순(큰 것부터 작은 것 순)으로 해당 열을 기준으로 데이터 프레임을 재정렬할 수 있습니다. 예를 들어, 이 코드는 가장 많이 지연된 항공편부터 가장 적게 지연된 항공편 순으로 정렬합니다:

flights |> 
  arrange(desc(dep_delay))
#> # A tibble: 336,776 × 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     9      641            900      1301     1242           1530
#> 2  2013     6    15     1432           1935      1137     1607           2120
#> 3  2013     1    10     1121           1635      1126     1239           1810
#> 4  2013     9    20     1139           1845      1014     1457           2210
#> 5  2013     7    22      845           1600      1005     1044           1815
#> 6  2013     4    10     1100           1900       960     1342           2211
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

행의 수는 변경되지 않았습니다. 데이터를 정렬하기만 했고 필터링하지는 않았습니다.

3.2.4 distinct()

distinct()는 데이터셋에서 모든 고유한 행을 찾으므로 기술적으로는 주로 행에서 작동합니다. 그러나 대부분의 경우 일부 변수의 고유한 조합을 원할 것이므로 선택적으로 열 이름을 제공할 수도 있습니다:

# 중복 행이 있는 경우 제거
flights |> 
  distinct()
#> # A tibble: 336,776 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

# 모든 고유한 출발지 및 도착지 쌍 찾기
flights |> 
  distinct(origin, dest)
#> # A tibble: 224 × 2
#>   origin dest 
#>   <chr>  <chr>
#> 1 EWR    IAH  
#> 2 LGA    IAH  
#> 3 JFK    MIA  
#> 4 JFK    BQN  
#> 5 LGA    ATL  
#> 6 EWR    ORD  
#> # ℹ 218 more rows

또는 고유한 행을 필터링할 때 다른 열을 유지하려면 .keep_all = TRUE 옵션을 사용할 수 있습니다.

flights |> 
  distinct(origin, dest, .keep_all = TRUE)
#> # A tibble: 224 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 218 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

이 모든 고유한 항공편이 1월 1일인 것은 우연이 아닙니다. distinct()는 데이터셋에서 고유한 행의 첫 번째 발생을 찾고 나머지는 버리기 때문입니다.

대신 발생 횟수를 찾으려면 distinct()count()로 바꾸는 것이 좋습니다. sort = TRUE 인수를 사용하면 발생 횟수의 내림차순으로 정렬할 수 있습니다. count에 대해서는 Section 13.3 에서 더 자세히 배울 것입니다.

flights |>
  count(origin, dest, sort = TRUE)
#> # A tibble: 224 × 3
#>   origin dest      n
#>   <chr>  <chr> <int>
#> 1 JFK    LAX   11262
#> 2 LGA    ATL   10263
#> 3 LGA    ORD    8857
#> 4 JFK    SFO    8204
#> 5 LGA    CLT    6168
#> 6 EWR    ORD    6100
#> # ℹ 218 more rows

3.2.5 연습문제

  1. 각 조건에 대해 단일 파이프라인에서 조건을 충족하는 모든 항공편을 찾으세요:

    • 도착 지연이 2시간 이상이었다
    • 휴스턴(IAH 또는 HOU)으로 비행했다
    • 유나이티드(United), 아메리칸(American), 또는 델타(Delta) 항공에 의해 운항되었다
    • 여름(7월, 8월, 9월)에 출발했다
    • 2시간 이상 늦게 도착했지만 늦게 출발하지 않았다
    • 적어도 1시간 지연되었지만 비행 중 30분 이상 만회했다
  2. 출발 지연이 가장 긴 항공편을 찾으려면 flights를 정렬하세요. 아침에 가장 일찍 떠난 항공편을 찾으세요.

  3. 가장 빠른 항공편을 찾으려면 flights를 정렬하세요. (힌트: 함수 내부에 수학 계산을 포함해 보세요.)

  4. 2013년의 모든 날에 항공편이 있었습니까?

  5. 어떤 항공편이 가장 먼 거리를 이동했습니까? 어떤 것이 가장 짧은 거리를 이동했습니까?

  6. filter()arrange()를 모두 사용하는 경우 어떤 순서로 사용하는지가 중요합니까? 왜 그렇습니까/그렇지 않습니까? 결과와 함수가 수행해야 할 작업량에 대해 생각해보세요.

3.3

행을 변경하지 않고 열에 영향을 미치는 4가지 중요한 동사가 있습니다. mutate()는 기존 열에서 파생된 새 열을 생성하고, select()는 존재하는 열을 변경하고, rename()은 열의 이름을 변경하고, relocate()는 열의 위치를 변경합니다.

3.3.1 mutate()

mutate()의 역할은 기존 열에서 계산된 새 열을 추가하는 것입니다. 변형(transform) 장에서는 다양한 유형의 변수를 조작하는 데 사용할 수 있는 다양한 함수 세트를 배우게 될 것입니다. 지금은 지연된 항공편이 공중에서 얼마나 많은 시간을 만회했는지(gain)와 시간당 마일 단위의 속도(speed)를 계산할 수 있는 기본 대수학을 고수할 것입니다:

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60
  )
#> # A tibble: 336,776 × 21
#>    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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 13 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

기본적으로 mutate()는 데이터셋의 오른쪽에 새 열을 추가하므로 여기서 무슨 일이 일어나고 있는지 보기가 어렵습니다. 대신 .before 인수를 사용하여 변수를 왼쪽에 추가할 수 있습니다2:

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60,
    .before = 1
  )
#> # A tibble: 336,776 × 21
#>    gain speed  year month   day dep_time sched_dep_time dep_delay arr_time
#>   <dbl> <dbl> <int> <int> <int>    <int>          <int>     <dbl>    <int>
#> 1    -9  370.  2013     1     1      517            515         2      830
#> 2   -16  374.  2013     1     1      533            529         4      850
#> 3   -31  408.  2013     1     1      542            540         2      923
#> 4    17  517.  2013     1     1      544            545        -1     1004
#> 5    19  394.  2013     1     1      554            600        -6      812
#> 6   -16  288.  2013     1     1      554            558        -4      740
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: sched_arr_time <int>, arr_delay <dbl>, …

..before가 함수에 대한 인수이지 우리가 생성하는 세 번째 새 변수의 이름이 아님을 나타냅니다. .after를 사용하여 변수 뒤에 추가할 수도 있으며, .before.after 모두에서 위치 대신 변수 이름을 사용할 수 있습니다. 예를 들어, day 뒤에 새 변수를 추가할 수 있습니다:

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    speed = distance / air_time * 60,
    .after = day
  )

또는 .keep 인수로 유지되는 변수를 제어할 수 있습니다. 특히 유용한 인수는 "used"로, mutate() 단계에 포함되거나 생성된 열만 유지하도록 지정합니다. 예를 들어, 다음 출력에는 dep_delay, arr_delay, air_time, gain, hours, gain_per_hour 변수만 포함됩니다.

flights |> 
  mutate(
    gain = dep_delay - arr_delay,
    hours = air_time / 60,
    gain_per_hour = gain / hours,
    .keep = "used"
  )

위 계산의 결과를 flights에 다시 할당하지 않았기 때문에 새 변수 gain, hours, gain_per_hour는 출력만 되고 데이터 프레임에 저장되지 않는다는 점에 유의하세요. 나중에 사용할 수 있도록 데이터 프레임에서 사용할 수 있게 하려면 결과를 flights에 다시 할당하여 원본 데이터 프레임을 훨씬 더 많은 변수로 덮어쓸지, 아니면 새 객체에 할당할지 신중하게 생각해야 합니다. 종종 정답은 내용을 나타내도록 정보가 풍부하게 이름 지어진 새 객체(예: delay_gain)이지만, flights를 덮어쓰는 데에는 타당한 이유가 있을 수도 있습니다.

3.3.2 select()

수백 개 또는 수천 개의 변수가 있는 데이터셋을 얻는 것은 드문 일이 아닙니다. 이 상황에서 첫 번째 과제는 종종 관심 있는 변수에만 집중하는 것입니다. select()를 사용하면 변수 이름을 기반으로 한 작업을 사용하여 유용한 부분집합을 빠르게 확대할 수 있습니다:

  • 이름으로 열 선택:

    flights |> 
      select(year, month, day)
  • year와 day 사이의 모든 열 선택(포함):

    flights |> 
      select(year:day)
  • year에서 day까지의 열을 제외한 모든 열 선택(포함):

    flights |> 
      select(!year:day)

    역사적으로 이 작업은 ! 대신 -로 수행되었으므로 야생에서 볼 가능성이 높습니다. 이 두 연산자는 같은 목적을 수행하지만 동작에 미묘한 차이가 있습니다. !는 “not”으로 읽히고 &|와 잘 결합되므로 사용하는 것이 좋습니다.

  • 문자인 모든 열 선택:

    flights |> 
      select(where(is.character))

select() 내에서 사용할 수 있는 몇 가지 도우미 함수가 있습니다:

  • starts_with("abc"): “abc”로 시작하는 이름과 일치합니다.
  • ends_with("xyz"): “xyz”로 끝나는 이름과 일치합니다.
  • contains("ijk"): “ijk”를 포함하는 이름과 일치합니다.
  • num_range("x", 1:3): x1, x2, x3와 일치합니다.

자세한 내용은 ?select를 참조하세요. 정규 표현식(Chapter 15 의 주제)을 알고 나면 matches()를 사용하여 패턴과 일치하는 변수를 선택할 수도 있습니다.

select()할 때 =를 사용하여 변수의 이름을 바꿀 수 있습니다. 새 이름은 =의 왼쪽에 나타나고 이전 변수는 오른쪽에 나타납니다:

flights |> 
  select(tail_num = tailnum)
#> # A tibble: 336,776 × 1
#>   tail_num
#>   <chr>   
#> 1 N14228  
#> 2 N24211  
#> 3 N619AA  
#> 4 N804JB  
#> 5 N668DN  
#> 6 N39463  
#> # ℹ 336,770 more rows

3.3.3 rename()

기존 변수를 모두 유지하고 몇 가지 이름만 바꾸고 싶다면 select() 대신 rename()을 사용할 수 있습니다:

flights |> 
  rename(tail_num = tailnum)
#> # A tibble: 336,776 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

일관성 없이 명명된 열이 많아 모두 수동으로 수정하기 고통스럽다면 유용한 자동 정리를 제공하는 janitor::clean_names()를 확인해 보세요.

3.3.4 relocate()

변수를 이동하려면 relocate()를 사용하세요. 관련 변수를 함께 모으거나 중요한 변수를 앞으로 이동하고 싶을 수 있습니다. 기본적으로 relocate()는 변수를 앞으로 이동합니다:

flights |> 
  relocate(time_hour, air_time)
#> # A tibble: 336,776 × 19
#>   time_hour           air_time  year month   day dep_time sched_dep_time
#>   <dttm>                 <dbl> <int> <int> <int>    <int>          <int>
#> 1 2013-01-01 05:00:00      227  2013     1     1      517            515
#> 2 2013-01-01 05:00:00      227  2013     1     1      533            529
#> 3 2013-01-01 05:00:00      160  2013     1     1      542            540
#> 4 2013-01-01 05:00:00      183  2013     1     1      544            545
#> 5 2013-01-01 06:00:00      116  2013     1     1      554            600
#> 6 2013-01-01 05:00:00      150  2013     1     1      554            558
#> # ℹ 336,770 more rows
#> # ℹ 12 more variables: dep_delay <dbl>, arr_time <int>, …

mutate()에서처럼 .before.after 인수를 사용하여 어디에 둘지 지정할 수도 있습니다:

flights |> 
  relocate(year:dep_time, .after = time_hour)
flights |> 
  relocate(starts_with("arr"), .before = dep_time)

3.3.5 연습문제

  1. dep_time, sched_dep_time, dep_delay를 비교하세요. 이 세 숫자가 어떻게 관련될 것이라고 예상합니까?

  2. flights에서 dep_time, dep_delay, arr_time, arr_delay를 선택할 수 있는 가능한 많은 방법을 브레인스토밍하세요.

  3. select() 호출에서 동일한 변수의 이름을 여러 번 지정하면 어떻게 됩니까?

  4. any_of() 함수는 무엇을 합니까? 이 벡터와 함께 사용할 때 왜 도움이 될 수 있습니까?

    variables <- c("year", "month", "day", "dep_delay", "arr_delay")
  5. 다음 코드를 실행한 결과가 놀랍습니까? select 도우미는 기본적으로 대소문자를 어떻게 처리합니까? 그 기본값을 어떻게 변경할 수 있습니까?

    flights |> select(contains("TIME"))
  6. 측정 단위를 나타내기 위해 air_timeair_time_min으로 이름을 바꾸고 데이터 프레임의 맨 앞으로 이동하세요.

  7. 다음이 작동하지 않는 이유는 무엇이며 오류는 무엇을 의미합니까?

    flights |> 
      select(tailnum) |> 
      arrange(arr_delay)
    #> Error in `arrange()`:
    #> ℹ In argument: `..1 = arr_delay`.
    #> Caused by error:
    #> ! object 'arr_delay' not found

3.4 파이프

위에서 파이프의 간단한 예를 보여주었지만, 진정한 힘은 여러 동사를 결합하기 시작할 때 나타납니다. 예를 들어, 휴스턴의 IAH 공항으로 가는 가장 빠른 항공편을 찾고 싶다고 상상해 보세요. filter(), mutate(), select(), arrange()를 결합해야 합니다:

flights |> 
  filter(dest == "IAH") |> 
  mutate(speed = distance / air_time * 60) |> 
  select(year:day, dep_time, carrier, flight, speed) |> 
  arrange(desc(speed))
#> # A tibble: 7,198 × 7
#>    year month   day dep_time carrier flight speed
#>   <int> <int> <int>    <int> <chr>    <int> <dbl>
#> 1  2013     7     9      707 UA         226  522.
#> 2  2013     8    27     1850 UA        1128  521.
#> 3  2013     8    28      902 UA        1711  519.
#> 4  2013     8    28     2122 UA        1022  519.
#> 5  2013     6    11     1628 UA        1178  515.
#> 6  2013     8    27     1017 UA         333  515.
#> # ℹ 7,192 more rows

이 파이프라인에는 4단계가 있지만 동사가 각 줄의 시작 부분에 오기 때문에 훑어보기 쉽습니다. flights 데이터로 시작한 다음 필터링하고, 변형하고, 선택하고, 정렬합니다.

파이프가 없었다면 어떻게 되었을까요? 각 함수 호출을 이전 호출 안에 중첩할 수 있습니다:

arrange(
  select(
    mutate(
      filter(
        flights, 
        dest == "IAH"
      ),
      speed = distance / air_time * 60
    ),
    year:day, dep_time, carrier, flight, speed
  ),
  desc(speed)
)

또는 중간 객체를 많이 사용할 수도 있습니다:

flights1 <- filter(flights, dest == "IAH")
flights2 <- mutate(flights1, speed = distance / air_time * 60)
flights3 <- select(flights2, year:day, dep_time, carrier, flight, speed)
arrange(flights3, desc(speed))

두 형식 모두 때와 장소가 있지만 파이프는 일반적으로 쓰기 쉽고 읽기 쉬운 데이터 분석 코드를 생성합니다.

코드에 파이프를 추가하려면 내장 키보드 단축키인 Ctrl/Cmd + Shift + M을 사용하는 것이 좋습니다. Figure 3.1 에 표시된 대로 %>% 대신 |>를 사용하려면 RStudio 옵션을 하나 변경해야 합니다. %>%에 대해서는 잠시 후에 자세히 설명하겠습니다.

"Code" 옵션의 "Editing" 패널에서 찾을 수 있는 "Use native pipe operator"  옵션을 보여주는 스크린샷.
Figure 3.1: |>를 삽입하려면 “Use native pipe operator” 옵션이 선택되어 있는지 확인하세요.
Notemagrittr

한동안 tidyverse를 사용해 왔다면 magrittr 패키지에서 제공하는 %>% 파이프에 익숙할 수 있습니다. magrittr 패키지는 핵심 tidyverse에 포함되어 있으므로 tidyverse를 로드할 때마다 %>%를 사용할 수 있습니다:

간단한 경우 |>%>%는 동일하게 작동합니다. 그렇다면 왜 기본(base) 파이프를 권장할까요? 첫째, 기본 R의 일부이므로 tidyverse를 사용하지 않을 때도 항상 사용할 수 있습니다. 둘째, |>%>%보다 훨씬 간단합니다. 2014년에 %>%가 발명된 후 2021년 R 4.1.0에 |>가 포함되기까지의 시간 동안 우리는 파이프에 대해 더 잘 이해하게 되었습니다. 이를 통해 기본 구현에서 자주 사용되지 않고 덜 중요한 기능을 버릴 수 있었습니다.

3.5 그룹

지금까지 행과 열을 다루는 함수에 대해 배웠습니다. dplyr은 그룹으로 작업하는 기능을 추가하면 훨씬 더 강력해집니다. 이 섹션에서는 가장 중요한 함수인 group_by(), summarize() 및 슬라이스 함수 제품군에 초점을 맞출 것입니다.

3.5.1 group_by()

group_by()를 사용하여 데이터셋을 분석에 의미 있는 그룹으로 나누세요:

flights |> 
  group_by(month)
#> # A tibble: 336,776 × 19
#> # Groups:   month [12]
#>    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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

group_by()는 데이터를 변경하지 않지만, 출력을 자세히 살펴보면 출력이 월별로 “그룹화”되었음을 나타내는 것을 알 수 있습니다(Groups: month [12]). 즉, 후속 작업은 이제 “월별로” 작동합니다. group_by()는 이 그룹화된 기능(클래스라고 함)을 데이터 프레임에 추가하여 데이터에 적용되는 후속 동사의 동작을 변경합니다.

3.5.2 summarize()

가장 중요한 그룹화된 작업은 요약(summary)으로, 단일 요약 통계를 계산하는 데 사용되는 경우 데이터 프레임을 각 그룹에 대해 단일 행을 갖도록 축소합니다. dplyr에서 이 작업은 summarize()3에 의해 수행되며, 다음 예제는 월별 평균 출발 지연을 계산합니다:

flights |> 
  group_by(month) |> 
  summarize(
    avg_delay = mean(dep_delay)
  )
#> # A tibble: 12 × 2
#>   month avg_delay
#>   <int>     <dbl>
#> 1     1        NA
#> 2     2        NA
#> 3     3        NA
#> 4     4        NA
#> 5     5        NA
#> 6     6        NA
#> # ℹ 6 more rows

어라! 무언가 잘못되었고, 모든 결과가 R의 결측값 기호인 NA(“N-A”라고 발음)입니다. 이것은 관측된 일부 항공편의 지연 열에 누락된 데이터가 있기 때문에 발생했으며, 해당 값을 포함하여 평균을 계산했을 때 NA 결과가 나왔습니다. Chapter 18 에서 결측값에 대해 자세히 다시 다루겠지만, 지금은 mean() 함수에 na.rm 인수를 TRUE로 설정하여 모든 결측값을 무시하도록 지시할 것입니다:

flights |> 
  group_by(month) |> 
  summarize(
    avg_delay = mean(dep_delay, na.rm = TRUE)
  )
#> # A tibble: 12 × 2
#>   month avg_delay
#>   <int>     <dbl>
#> 1     1      10.0
#> 2     2      10.8
#> 3     3      13.2
#> 4     4      13.9
#> 5     5      13.0
#> 6     6      20.8
#> # ℹ 6 more rows

summarize()에 대한 단일 호출로 원하는 만큼의 요약을 생성할 수 있습니다. 다가오는 장에서 다양한 유용한 요약을 배우겠지만, 매우 유용한 요약 중 하나는 각 그룹의 행 수를 반환하는 n()입니다:

flights |> 
  group_by(month) |> 
  summarize(
    avg_delay = mean(dep_delay, na.rm = TRUE), 
    n = n()
  )
#> # A tibble: 12 × 3
#>   month avg_delay     n
#>   <int>     <dbl> <int>
#> 1     1      10.0 27004
#> 2     2      10.8 24951
#> 3     3      13.2 28834
#> 4     4      13.9 28330
#> 5     5      13.0 28796
#> 6     6      20.8 28243
#> # ℹ 6 more rows

평균과 개수는 데이터 과학에서 놀랍도록 많은 것을 얻게 해줍니다!

3.5.3 slice_ 함수들

각 그룹 내에서 특정 행을 추출할 수 있는 5가지 편리한 함수가 있습니다:

  • df |> slice_head(n = 1)은 각 그룹에서 첫 번째 행을 가져옵니다.
  • df |> slice_tail(n = 1)은 각 그룹에서 마지막 행을 가져옵니다.
  • df |> slice_min(x, n = 1)x 열의 값이 가장 작은 행을 가져옵니다.
  • df |> slice_max(x, n = 1)x 열의 값이 가장 큰 행을 가져옵니다.
  • df |> slice_sample(n = 1)은 임의의 행 하나를 가져옵니다.

n을 변경하여 둘 이상의 행을 선택하거나 n = 대신 prop = 0.1을 사용하여 각 그룹에서 (예를 들어) 10%의 행을 선택할 수 있습니다. 예를 들어, 다음 코드는 각 목적지에 도착했을 때 가장 많이 지연된 항공편을 찾습니다:

flights |> 
  group_by(dest) |> 
  slice_max(arr_delay, n = 1) |>
  relocate(dest)
#> # A tibble: 108 × 19
#> # Groups:   dest [105]
#>   dest   year month   day dep_time sched_dep_time dep_delay arr_time
#>   <chr> <int> <int> <int>    <int>          <int>     <dbl>    <int>
#> 1 ABQ    2013     7    22     2145           2007        98      132
#> 2 ACK    2013     7    23     1139            800       219     1250
#> 3 ALB    2013     1    25      123           2000       323      229
#> 4 ANC    2013     8    17     1740           1625        75     2042
#> 5 ATL    2013     7    22     2257            759       898      121
#> 6 AUS    2013     7    10     2056           1505       351     2347
#> # ℹ 102 more rows
#> # ℹ 11 more variables: sched_arr_time <int>, arr_delay <dbl>, …

목적지는 105개인데 여기서는 108개의 행을 얻습니다. 무슨 일일까요? slice_min()slice_max()는 동점 값을 유지하므로 n = 1은 가장 높은 값을 가진 모든 행을 제공하라는 의미입니다. 그룹당 정확히 하나의 행을 원하면 with_ties = FALSE를 설정할 수 있습니다.

이것은 summarize()로 최대 지연을 계산하는 것과 유사하지만, 단일 요약 통계 대신 전체 해당 행(또는 동점이 있는 경우 행들)을 얻습니다.

3.5.4 여러 변수로 그룹화하기

하나 이상의 변수를 사용하여 그룹을 만들 수 있습니다. 예를 들어 각 날짜에 대한 그룹을 만들 수 있습니다.

daily <- flights |>  
  group_by(year, month, day)
daily
#> # A tibble: 336,776 × 19
#> # 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

둘 이상의 변수로 그룹화된 티블을 요약하면 각 요약이 마지막 그룹을 벗겨냅니다. 지나고 보니 이것은 이 기능을 작동시키는 좋은 방법이 아니었지만, 기존 코드를 깨지 않고 변경하기는 어렵습니다. 무슨 일이 일어나고 있는지 명확하게 하기 위해 dplyr은 이 동작을 변경하는 방법을 알려주는 메시지를 표시합니다:

daily_flights <- daily |> 
  summarize(n = n())
#> `summarise()` has grouped output by 'year', 'month'. You can override using
#> the `.groups` argument.

이 동작에 만족하면 메시지를 억제하기 위해 명시적으로 요청할 수 있습니다:

daily_flights <- daily |> 
  summarize(
    n = n(), 
    .groups = "drop_last"
  )

또는 다른 값을 설정하여 기본 동작을 변경하세요. 예: 모든 그룹화를 삭제하려면 "drop", 동일한 그룹을 보존하려면 "keep".

3.5.5 그룹화 해제하기

summarize()를 사용하지 않고 데이터 프레임에서 그룹화를 제거하고 싶을 수도 있습니다. ungroup()으로 이 작업을 수행할 수 있습니다.

daily |> 
  ungroup()
#> # A tibble: 336,776 × 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      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 336,770 more rows
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>, …

이제 그룹화되지 않은 데이터 프레임을 요약할 때 어떤 일이 발생하는지 봅시다.

daily |> 
  ungroup() |>
  summarize(
    avg_delay = mean(dep_delay, na.rm = TRUE), 
    flights = n()
  )
#> # A tibble: 1 × 2
#>   avg_delay flights
#>       <dbl>   <int>
#> 1      12.6  336776

dplyr은 그룹화되지 않은 데이터 프레임의 모든 행을 하나의 그룹에 속하는 것으로 취급하기 때문에 단일 행을 반환합니다.

3.5.6 .by

dplyr 1.1.0에는 작업별 그룹화를 위한 새로운 실험적 구문인 .by 인수가 포함되어 있습니다. group_by()ungroup()이 사라지는 것은 아니지만 이제 .by 인수를 사용하여 단일 작업 내에서 그룹화할 수도 있습니다:

flights |> 
  summarize(
    delay = mean(dep_delay, na.rm = TRUE), 
    n = n(),
    .by = month
  )

또는 여러 변수로 그룹화하려면:

flights |> 
  summarize(
    delay = mean(dep_delay, na.rm = TRUE), 
    n = n(),
    .by = c(origin, dest)
  )

.by는 모든 동사와 함께 작동하며 작업이 완료되었을 때 그룹화 메시지를 억제하기 위해 .groups 인수를 사용하거나 ungroup()을 사용할 필요가 없다는 장점이 있습니다.

우리가 책을 썼을 때 이 구문이 매우 새로운 것이었기 때문에 이 장에서는 이 구문에 집중하지 않았습니다. 우리는 이것이 많은 가능성을 가지고 있고 꽤 인기가 있을 것 같다고 생각하기 때문에 언급하고 싶었습니다. dplyr 1.1.0 블로그 게시물에서 자세한 내용을 확인할 수 있습니다.

3.5.7 연습문제

  1. 평균 지연 시간이 가장 나쁜 항공사는 어디입니까? 도전 과제: 나쁜 공항 대 나쁜 항공사의 영향을 풀 수 있습니까? 왜 그렇습니까/그렇지 않습니까? (힌트: flights |> group_by(carrier, dest) |> summarize(n())을 생각해보세요)

  2. 각 목적지로 출발할 때 가장 많이 지연된 항공편을 찾으세요.

  3. 지연은 하루 동안 어떻게 변합니까? 플롯으로 답을 설명하세요.

  4. slice_min()과 친구들에게 음수 n을 제공하면 어떻게 됩니까?

  5. 방금 배운 dplyr 동사 측면에서 count()가 수행하는 작업을 설명하세요. count()에 대한 sort 인수는 무엇을 합니까?

  6. 다음과 같은 작은 데이터 프레임이 있다고 가정해 봅시다:

    df <- tibble(
      x = 1:5,
      y = c("a", "b", "a", "a", "b"),
      z = c("K", "K", "L", "L", "K")
    )
    1. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 group_by()가 무엇을 하는지 설명하세요.

      df |>
        group_by(y)
    2. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 arrange()가 무엇을 하는지 설명하세요. 또한 (a) 부분의 group_by()와 어떻게 다른지 언급하세요.

      df |>
        arrange(y)
    3. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 파이프라인이 무엇을 하는지 설명하세요.

      df |>
        group_by(y) |>
        summarize(mean_x = mean(x))
    4. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 파이프라인이 무엇을 하는지 설명하세요. 그런 다음 메시지가 무엇을 말하는지 언급하세요.

      df |>
        group_by(y, z) |>
        summarize(mean_x = mean(x))
    5. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 파이프라인이 무엇을 하는지 설명하세요. 출력이 (d) 부분의 출력과 어떻게 다릅니까?

      df |>
        group_by(y, z) |>
        summarize(mean_x = mean(x), .groups = "drop")
    6. 출력이 어떻게 보일지 생각나는 대로 적은 다음 맞았는지 확인하고 각 파이프라인이 무엇을 하는지 설명하세요. 두 파이프라인의 출력은 어떻게 다릅니까?

      df |>
        group_by(y, z) |>
        summarize(mean_x = mean(x))
      
      df |>
        group_by(y, z) |>
        mutate(mean_x = mean(x))

3.6 사례 연구: 집계 및 표본 크기

집계를 수행할 때마다 항상 개수(n())를 포함하는 것이 좋습니다. 그렇게 하면 매우 적은 양의 데이터에 근거하여 결론을 내리지 않도록 할 수 있습니다. Lahman 패키지의 야구 데이터를 사용하여 이를 보여줄 것입니다. 구체적으로 선수가 안타를 친 횟수(H) 대 공을 인플레이(in play)하려고 시도한 횟수(AB)의 비율을 비교할 것입니다:

batters <- Lahman::Batting |> 
  group_by(playerID) |> 
  summarize(
    performance = sum(H, na.rm = TRUE) / sum(AB, na.rm = TRUE),
    n = sum(AB, na.rm = TRUE)
  )
batters
#> # A tibble: 20,985 × 3
#>   playerID  performance     n
#>   <chr>           <dbl> <int>
#> 1 aardsda01      0          4
#> 2 aaronha01      0.305  12364
#> 3 aaronto01      0.229    944
#> 4 aasedo01       0          5
#> 5 abadan01       0.0952    21
#> 6 abadfe01       0.111      9
#> # ℹ 20,979 more rows

타자의 기량(타율 performance로 측정) 대 공을 칠 기회(타수 n으로 측정)를 플롯하면 두 가지 패턴이 나타납니다:

  1. 타석 수가 적은 선수들 사이에서 performance의 변동이 더 큽니다. 이 플롯의 모양은 매우 특징적입니다. 평균(또는 기타 요약 통계) 대 그룹 크기를 플롯할 때마다 표본 크기가 커짐에 따라 변동이 감소하는 것을 볼 수 있습니다4.

  2. 기량(performance)과 공을 칠 기회(n) 사이에는 양의 상관관계가 있습니다. 팀은 최고의 타자에게 공을 칠 기회를 가장 많이 주고 싶어하기 때문입니다.

batters |> 
  filter(n > 100) |> 
  ggplot(aes(x = n, y = performance)) +
  geom_point(alpha = 1 / 10) + 
  geom_smooth(se = FALSE)

타격 성적 대 타격 기회의 산점도에 매끄러운 선이 겹쳐져 있습니다.  평균 성적은 n이 ~100일 때 0.2에서 n이 ~1000일 때 0.25로 급격히  증가합니다. 평균 성적은 훨씬 완만한 기울기로 선형적으로 계속 증가하여  n이 ~12,000일 때 0.3에 도달합니다.

ggplot2와 dplyr을 결합하는 편리한 패턴을 주목하세요. 데이터셋 처리를 위한 |>에서 플롯에 레이어를 추가하기 위한 +로 전환해야 한다는 점만 기억하면 됩니다.

이것은 순위 매기기에도 중요한 의미를 갖습니다. desc(performance)로 순진하게 정렬하면 타율이 가장 좋은 사람들은 분명히 공을 인플레이하려고 시도한 횟수가 매우 적고 우연히 안타를 친 사람들이며, 반드시 가장 숙련된 선수는 아닙니다:

batters |> 
  arrange(desc(performance))
#> # A tibble: 20,985 × 3
#>   playerID  performance     n
#>   <chr>           <dbl> <int>
#> 1 abramge01           1     1
#> 2 alberan01           1     1
#> 3 banisje01           1     1
#> 4 bartocl01           1     1
#> 5 bassdo01            1     1
#> 6 birasst01           1     2
#> # ℹ 20,979 more rows

이 문제에 대한 좋은 설명과 해결 방법은 http://varianceexplained.org/r/empirical_bayes_baseball/https://www.evanmiller.org/how-not-to-sort-by-average-rating.html에서 찾을 수 있습니다.

3.7 요약

이 장에서는 dplyr이 데이터 프레임 작업을 위해 제공하는 도구에 대해 배웠습니다. 도구는 대략 세 가지 범주로 분류됩니다: 행을 조작하는 도구(filter()arrange() 등), 열을 조작하는 도구(select()mutate() 등), 그룹을 조작하는 도구(group_by()summarize() 등). 이 장에서는 이러한 “전체 데이터 프레임” 도구에 중점을 두었지만 개별 변수로 무엇을 할 수 있는지에 대해서는 아직 많이 배우지 않았습니다. 각 장에서 특정 유형의 변수에 대한 도구를 제공하는 책의 변형(Transform) 파트에서 다시 다룰 것입니다.

다음 장에서는 워크플로우로 다시 돌아가 코드 스타일의 중요성과 코드를 잘 정리하여 자신과 다른 사람들이 읽고 이해하기 쉽게 만드는 방법에 대해 논의할 것입니다.


  1. 나중에 위치에 따라 행을 선택할 수 있는 slice_*() 제품군에 대해 배울 것입니다.↩︎

  2. RStudio에서 열이 많은 데이터셋을 보는 가장 쉬운 방법은 View()임을 기억하세요.↩︎

  3. 영국식 영어를 선호하는 경우 summarise()도 가능합니다.↩︎

  4. *쿨럭* 대수의 법칙 *쿨럭*.↩︎