9  За пределами base R: tidyverse и data.table

Автор

И.С. Поздняков

Как вы уже, наверное, убедились, базовый R умеет очень много, в том числе и для работы с данными. Однако какие-то операции все равно выполнить довольно непросто.

Возьмем, например, задачу агрегации: вам нужно посчитать средний рост супергероев отдельно для мужчин и для женщин (а еще и для NA, за компанию). Три группы еще ничего, а если бы их было 10, 50 или 200? В базовом R для этого есть специальная функция aggregate(), но она довольно неудобная.

Поэтому стали появляться пакеты, которые пытаются сделать агрегацию и другие непростые операции максимально безболезненными способами. Основных таких пакетов два: data.table и tidyverse. Это огромные пакеты, которые очень сильно изменяют работу в R, в том числе в плане стиля и используемой парадигмы. Тем не менее, в основе своей стоит все то, что мы прошли раньше.

9.1 Подход {data.table}

data.table – это распространенный пакет, который позволяет анализировать датафреймы максимально быстро и с помощью очень лаконичного кода.

install.packages("data.table")

Давайте импортируем наш набор данных про супергероев. Для этого воспользуемся функцией fread() из пакета data.table. Эта функция нам уже знакома как функция для импорта больших наборов данных Глава 6.7.

“f” в fread() означает fast and friendly”: эта функция очень быстрая и довольно хорошо угадывает формат текстовой таблицы.

library(data.table)
heroes_dt <-
  fread(
    "https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv",
    na.strings = c("NA", "-", "-99", " ")
  )
Warning in
fread("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv",
: na.strings[4]==" " consists only of whitespace, ignoring. Since
strip.white=TRUE (default), use na.strings="" to specify that any number of
spaces in a string column should be read as <NA>.

Функция fread() создает не просто датафрейм, а дататейбл (datatable):

heroes_dt
        V1            name Gender Eye color              Race       Hair color
     <int>          <char> <char>    <char>            <char>           <char>
  1:     0          A-Bomb   Male    yellow             Human          No Hair
  2:     1      Abe Sapien   Male      blue     Icthyo Sapien          No Hair
  3:     2        Abin Sur   Male      blue           Ungaran          No Hair
  4:     3     Abomination   Male     green Human / Radiation          No Hair
  5:     4         Abraxas   Male      blue     Cosmic Entity            Black
 ---                                                                          
730:   729 Yellowjacket II Female      blue             Human Strawberry Blond
731:   730            Ymir   Male     white       Frost Giant          No Hair
732:   731            Yoda   Male     brown    Yoda's species            White
733:   732         Zatanna Female      blue             Human            Black
734:   733            Zoom   Male       red              <NA>            Brown
     Height         Publisher Skin color Alignment Weight
      <num>            <char>     <char>    <char>  <int>
  1:  203.0     Marvel Comics       <NA>      good    441
  2:  191.0 Dark Horse Comics       blue      good     65
  3:  185.0         DC Comics        red      good     90
  4:  203.0     Marvel Comics       <NA>       bad    441
  5:     NA     Marvel Comics       <NA>       bad     NA
 ---                                                     
730:  165.0     Marvel Comics       <NA>      good     52
731:  304.8     Marvel Comics      white      good     NA
732:   66.0      George Lucas      green      good     17
733:  170.0         DC Comics       <NA>      good     57
734:  185.0         DC Comics       <NA>       bad     81
class(heroes_dt)
[1] "data.table" "data.frame"

Дататейбл – это “улучшенный” датафрейм: с ним работают все те функции, которые мы применяли для датафрейма, специальные функции для дататейбла, а что-то работает немного по-другому по сравнению с датафреймом. Например, оператор [, т.е. квадратные скобки.

Давайте посмотрим по-внимательнее как это происходит на примере расчета среднего роста супергероев, группируя по полу:

heroes_dt[, mean(Height, na.rm = TRUE), by = Gender]
   Gender       V1
   <char>    <num>
1:   Male 191.9749
2: Female 174.6840
3:   <NA> 177.0667

Сразу уже усложним задачу: возьмем только хороших (у кого в колонке Alignment стоит "good"), а потом еще отсортируем по среднему росту.

heroes_dt[Alignment == "good", 
          .(mean_height = mean(Height, na.rm = TRUE)), 
          by = Gender][
            order(-mean_height)
          ]
   Gender mean_height
   <char>       <num>
1:   Male    188.9601
2:   <NA>    179.5000
3: Female    174.7607

Уух! Выглядит монструозно, да? Зато как мы все сделали используя минимальное количество знаков. Заметьте, что здесь необычного для нас:

  • Не нужно прописывать heroes_dt$Alignment, поиск переменной будет начинаться с колонок дататейбла.

  • Там, где мы раньше выбирали колонки, мы еще и расчеты можем вести.

  • Внутри квадратных скобок появилась вторая запятая, т.е. третье поле, в котором мы прописали группировку.

  • Несколько операций прописываются путем соединения квадратных скобочек, код превращается в эдакий паровозик1.

И это не все отличия!

На сайте пакета {data.table} особенно уделяется вниманию скорости data.table, приводя в качестве доказательства бэнчмарк, где сравниваются по скорости различные инструменты для работы с данными. data.table почти на порядок обгоняет как dplyr, так и питоновский pandas – самый используемый пакет для анализа данных в Python.

Разработчики data.table делают особый акцент на “консервативности” пакета: у него нет никаких зависимостей (в этом плане пакет data.table обгоняет большинство российских экспатов в Тбилиси), ему достаточно очень старой версии R, функционирование пакета не будет ломаться из-за выкинутых устаревших функций. В общем, data.table очень суров и уважаем программистами. Он и не особо пытается понравиться рядовым пользователям. Зато освоив его, вы сможете творить магию: то, что с помощью базового R, tidyverse или Python будет выполняться очень долго (если выполнится вообще), data.table сможет сделать гораздо быстрее, иногда в десятки и сотни раз!

Очень сильно, не правда ли? Чем же может ответить tidyverse?

9.2 Подход tidyverse

Давайте посмотрим, как будет выглядеть решение тех же задач (отбор строк по условию, агрегация и сортировка) в tidyverse.

install.packages("tidyverse")
Warning: пакет 'ggplot2' был собран под R версии 4.4.3
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   4.0.2     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::between()     masks data.table::between()
✖ dplyr::filter()      masks stats::filter()
✖ dplyr::first()       masks data.table::first()
✖ lubridate::hour()    masks data.table::hour()
✖ lubridate::isoweek() masks data.table::isoweek()
✖ dplyr::lag()         masks stats::lag()
✖ dplyr::last()        masks data.table::last()
✖ lubridate::mday()    masks data.table::mday()
✖ lubridate::minute()  masks data.table::minute()
✖ lubridate::month()   masks data.table::month()
✖ lubridate::quarter() masks data.table::quarter()
✖ lubridate::second()  masks data.table::second()
✖ purrr::transpose()   masks data.table::transpose()
✖ lubridate::wday()    masks data.table::wday()
✖ lubridate::week()    masks data.table::week()
✖ lubridate::yday()    masks data.table::yday()
✖ lubridate::year()    masks data.table::year()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors

Не пугайтесь сообщений, все в порядке. Во-первых, пакет tidyverse – это не просто пакет, а “пакет с пакетами” (да-да, как у вас дома), который подключает сразу несколько других пакетов, которые составляют ядро tidyverse. Список и версии этих пакетов tidyverse выводит при подключении. Разные пакеты tidyverse мы очень детально разберем позже (Глава 10 ), а сейчас просто посмотрите, как это все выглядит.

heroes_tbl <- read_csv("https://raw.githubusercontent.com/Pozdniakov/tidy_stats/master/data/heroes_information.csv",
    na = c("NA", "-", "-99", " "))

Функция read_csv() (не путать с функцией из базового R – read.csv()!) возвращает тиббл – “улучшенный” датафрейм, примерно как это было с дататейблом.

heroes_tbl
# A tibble: 734 × 11
    ...1 name          Gender `Eye color` Race     `Hair color` Height Publisher
   <dbl> <chr>         <chr>  <chr>       <chr>    <chr>         <dbl> <chr>    
 1     0 A-Bomb        Male   yellow      Human    No Hair         203 Marvel C…
 2     1 Abe Sapien    Male   blue        Icthyo … No Hair         191 Dark Hor…
 3     2 Abin Sur      Male   blue        Ungaran  No Hair         185 DC Comics
 4     3 Abomination   Male   green       Human /… No Hair         203 Marvel C…
 5     4 Abraxas       Male   blue        Cosmic … Black            NA Marvel C…
 6     5 Absorbing Man Male   blue        Human    No Hair         193 Marvel C…
 7     6 Adam Monroe   Male   blue        <NA>     Blond            NA NBC - He…
 8     7 Adam Strange  Male   blue        Human    Blond           185 DC Comics
 9     8 Agent 13      Female blue        <NA>     Blond           173 Marvel C…
10     9 Agent Bob     Male   brown       Human    Brown           178 Marvel C…
# ℹ 724 more rows
# ℹ 3 more variables: `Skin color` <chr>, Alignment <chr>, Weight <dbl>
class(heroes_tbl)
[1] "spec_tbl_df" "tbl_df"      "tbl"         "data.frame" 

Теперь же сделаем то же самое с нашими данными, что мы делали с помощью data.table:

heroes_tbl %>%
  filter(Alignment == "good") %>%
  group_by(Gender) %>%
  summarise(mean_height = mean(Height, na.rm = TRUE)) %>%
  arrange(desc(mean_height))
# A tibble: 3 × 2
  Gender mean_height
  <chr>        <dbl>
1 Male          189.
2 <NA>          180.
3 Female        175.

Очень сильно отличается от того, как мы работали раньше! Хотя в основе лежит все тот же R. Код, написанный в tidyverse, нарочито многословен (особенно по сравнению с data.table), каждая отдельная операция имеет свою функцию. Писать нужно больше, зато это гораздо легче: меньше нужно думать, какими хитрыми трюками сделать преобразование данных. Нужно просто разделить весь процесс преобразования данных на отдельные операции и последовательно прописать их. Код получается аккуратный и очень читаемый, даже для человека, который не знает tidyverse или даже R в целом. Даже этот новый оператор %>% выглядит довольно понятно: его можно прочитать как “затем”.

Заметьте, что tidyverse выводит очень подробные сообщения, которые даже выглядят очень красиво: со всякими иконками, красивым форматированием. Разработчики tidyverse работают над тем, чтобы делать свой интерфейс максимально понятным для пользователя: говорящие сами за себя названия функций, куча удобных фишек на все случаи жизни.

tidyverse постоянно обновляется, регулярно появляются новые функции, а старые функции заменяются на более удобные новые. И это не всегда плюс: обновив пакеты, установленные год назад, вы можете обнаружить, что старый код перестал работать! Мол, мы тут придумали, как сделать лучше, переписывайте код заново (или используйте старые версии пакетов).

Разработчики tidyverse, в целом, не стремится за высокой скоростью. Часто можно заметить, что новые функции работают довольно медленно. Но если у вас строчек меньше миллиона, то разницу в скорости с data.table вы едва ли заметите.

Команда разработчиков tidyverse работает на компанию Posit (бывшая RStudio). Поэтому в RStudio вы найдете несколько “шпаргалок” для tidyverse, но не для data.table. Они также активно активно работают над популяризацией tidyverse, стараясь сделать вход в него максимально комфортным, особенно для людей без опыта программирования. tidyverse команда открыто заявляет о своей политике diversity, некоторые члены этой команды – открытые представители гендерных и сексуальных меньшинств.

9.3 {data.table} vs tidyverse

Так что же лучше: data.table или tidyverse? Это один из самых частых споров в R-комьюнити. У обоих подходов есть плюсы, которые можно обсуждать вечно. Сегодня tidyverse выигрывает в популярности, особенно за пределами русскоязычного пространства.

В последнее время data.table и tidyverse все меньше противостоят друг другу и все больше взаимодополняют. Например, некоторые используют в качестве основного инструмента tidyverse, но при работе с данными побольше переключаются на data.table2. Кроме того, сами разработчики tidyverse пытаются приладить суперскоростной data.table в tidyverse: пакет dtplyr позволяет “переводить” код, написанный в tidyverse в код на data.table.

Таким образом, выбирая из tidyverse и data.table, начинать лучше с более удобного и популярного tidyverse, чем и займемся далее.


  1. Это работает и в базовом R, но именно в data.table это очень частая конструкция.↩︎

  2. Автор книги поступает именно так =)↩︎