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")
  )

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

heroes_dt
      V1            name Gender Eye color              Race       Hair color
  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
  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
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
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")
library(tidyverse)
Warning: package 'dplyr' was built under R version 4.2.3
Warning: package 'stringr' was built under R version 4.2.3
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.4
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.4.4     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.0
✔ 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"))
New names:
• `` -> `...1`
Warning: One or more parsing issues, call `problems()` on your data frame for details,
e.g.:
  dat <- vroom(...)
  problems(dat)
Rows: 734 Columns: 11
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr (8): name, Gender, Eye color, Race, Hair color, Publisher, Skin color, A...
dbl (3): ...1, Height, Weight

ℹ Use `spec()` to retrieve the full column specification for this data.
ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.

Функция 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.table}2. Кроме того, сами разработчики tidyverse пытаются приладить суперскоростной {data.table} в tidyverse: пакет {dtplyr} позволяет “переводить” код, написанный в tidyverse в код на {data.table}.

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


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

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