tidyverse vs. data.table

Author

Pozdniakov Ivan

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

Возьмем, например, задачу агрегации: вам нужно посчитать средний размер кредитов отдельно для каждого вида активности. Групп много, поэтому считать для каждой группы отдельно – это неудобно, долго и попросту некрасиво. В базовом R для этого есть специальная функция aggregate(), но она довольно неудобная.

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

Подход {data.table}

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

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

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

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

library(data.table)
loans_dt <- fread("data/kiva/kiva_loans.csv")

Давайте запустим бенчмарк на наших данных для сравнения чтения данных с помощью {data.table}, read.csv() из базового R, а так же двух подходов из tidyverse.

library(microbenchmark)                   

bench_results <- microbenchmark(
  read.csv("data/kiva/kiva_loans.csv"),
  data.table::fread("data/kiva/kiva_loans.csv"),
  readr::read_csv("data/kiva/kiva_loans.csv", show_col_types = FALSE),
  vroom::vroom("data/kiva/kiva_loans.csv", show_col_types = FALSE)
)
Warning in microbenchmark(read.csv("data/kiva/kiva_loans.csv"),
data.table::fread("data/kiva/kiva_loans.csv"), : less accurate nanosecond times
to avoid potential integer overflows

Как видим, {data.table} быстрее примерно в 5 раз, чем базовая функция read.csv() и в 2 раза быстрее, чем функция из read_csv() из пакета {readr}. Только пакет {vroom} оказался быстрее (про него мы поговорим позже).

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

loans_dt
             id funded_amount loan_amount            activity         sector
     1:  653051           300         300 Fruits & Vegetables           Food
     2:  653053           575         575            Rickshaw Transportation
     3:  653068           150         150      Transportation Transportation
     4:  653063           200         200          Embroidery           Arts
     5:  653084           400         400          Milk Sales           Food
    ---                                                                     
671201: 1340323             0          25           Livestock    Agriculture
671202: 1340316            25          25           Livestock    Agriculture
671203: 1340334             0          25               Games  Entertainment
671204: 1340338             0          25           Livestock    Agriculture
671205: 1340339             0          25           Livestock    Agriculture
                                                                                                                               use
     1:                                                                                    To buy seasonal, fresh fruits to sell. 
     2:                                                           to repair and maintain the auto rickshaw used in their business.
     3:                                        To repair their old cycle-van and buy another one to rent out as a source of income
     4:                                               to purchase an embroidery machine and a variety of new embroidery materials.
     5:                                                                                                   to purchase one buffalo.
    ---                                                                                                                           
671201: [True, u'para compara: cemento, arenya y ladriollo para construir una pila.'] - this loan use has been approved by VIVA QA
671202:                                         [True, u'to start a turducken farm.'] - this loan use has been approved by VIVA QA
671203:                                                                                                                           
671204:                                         [True, u'to start a turducken farm.'] - this loan use has been approved by VIVA QA
671205:                                         [True, u'to start a turducken farm.'] - this loan use has been approved by VIVA QA
        country_code  country       region currency partner_id
     1:           PK Pakistan       Lahore      PKR        247
     2:           PK Pakistan       Lahore      PKR        247
     3:           IN    India    Maynaguri      INR        334
     4:           PK Pakistan       Lahore      PKR        247
     5:           PK Pakistan Abdul Hakeem      PKR        245
    ---                                                       
671201:           PY Paraguay   Concepción      USD         58
671202:           KE    Kenya                   KES        138
671203:           KE    Kenya                   KES        138
671204:           KE    Kenya                   KES        138
671205:           KE    Kenya                   KES        138
                posted_time      disbursed_time         funded_time
     1: 2014-01-01 06:12:39 2013-12-17 08:00:00 2014-01-02 10:06:32
     2: 2014-01-01 06:51:08 2013-12-17 08:00:00 2014-01-02 09:17:23
     3: 2014-01-01 09:58:07 2013-12-17 08:00:00 2014-01-01 16:01:36
     4: 2014-01-01 08:03:11 2013-12-24 08:00:00 2014-01-01 13:00:00
     5: 2014-01-01 11:53:19 2013-12-17 08:00:00 2014-01-01 19:18:51
    ---                                                            
671201: 2017-07-25 16:55:34 2017-07-25 07:00:00                <NA>
671202: 2017-07-25 06:14:08 2017-07-24 07:00:00 2017-07-26 02:09:43
671203: 2017-07-26 00:02:07 2017-07-25 07:00:00                <NA>
671204: 2017-07-26 06:12:55 2017-07-25 07:00:00                <NA>
671205: 2017-07-26 06:31:46 2017-07-25 07:00:00                <NA>
        term_in_months lender_count                         tags
     1:             12           12                             
     2:             11           14                             
     3:             43            6 user_favorite, user_favorite
     4:             11            8                             
     5:             14           16                             
    ---                                                         
671201:             13            0                             
671202:             13            1                             
671203:             13            0                             
671204:             13            0                             
671205:             13            0                             
        borrower_genders repayment_interval       date
     1:           female          irregular 2014-01-01
     2:   female, female          irregular 2014-01-01
     3:           female             bullet 2014-01-01
     4:           female          irregular 2014-01-01
     5:           female            monthly 2014-01-01
    ---                                               
671201:           female            monthly 2017-07-25
671202:           female            monthly 2017-07-25
671203:                             monthly 2017-07-26
671204:           female            monthly 2017-07-26
671205:           female            monthly 2017-07-26
class(loans_dt)
[1] "data.table" "data.frame"

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

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

loans_dt[, .(mean_loan = mean(loan_amount)), by = activity]
                   activity mean_loan
  1:    Fruits & Vegetables  828.7252
  2:               Rickshaw  531.2891
  3:         Transportation  864.0621
  4:             Embroidery  676.1450
  5:             Milk Sales  739.8786
 ---                                 
159:         Event Planning 1435.0000
160:           Celebrations  280.0000
161:               Computer  985.1852
162: Personal Care Products  514.2857
163:    Mobile Transactions  875.1157

Сразу уже усложним задачу: возьмем только агрикультурный сектор и отранжируем по среднему кредиту:

loans_dt[, .N, by = sector]
            sector      N
 1:           Food 136657
 2: Transportation  15518
 3:           Arts  12060
 4:       Services  45140
 5:    Agriculture 180302
 6:  Manufacturing   6208
 7:      Wholesale    634
 8:         Retail 124494
 9:       Clothing  32742
10:   Construction   6268
11:         Health   9223
12:      Education  31013
13:   Personal Use  36385
14:        Housing  33731
15:  Entertainment    830
loans_dt[sector == "Agriculture", .(mean_loan = mean(loan_amount)), by = activity][order(-mean_loan)]
            activity mean_loan
 1:       Beekeeping 1472.5000
 2:     Animal Sales 1137.0304
 3:        Livestock 1127.6670
 4:      Agriculture 1015.4628
 5:           Cattle 1012.5048
 6:          Flowers  852.8319
 7:          Poultry  845.8295
 8:      Land Rental  809.8375
 9:    Farm Supplies  787.7585
10: Veterinary Sales  787.7525
11:      Aquaculture  762.2685
12:          Farming  708.4624
13:            Dairy  659.3063
14:             Pigs  471.4168

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

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

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

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

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

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

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

Подход tidyverse

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

library(tidyverse)
── Attaching packages ─────────────────────────────────────── tidyverse 1.3.2 ──
✔ ggplot2 3.4.0     ✔ purrr   1.0.1
✔ tibble  3.2.1     ✔ dplyr   1.1.0
✔ tidyr   1.3.0     ✔ stringr 1.5.0
✔ readr   2.1.3     ✔ forcats 1.0.0
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::between()   masks data.table::between()
✖ dplyr::filter()    masks stats::filter()
✖ dplyr::first()     masks data.table::first()
✖ dplyr::lag()       masks stats::lag()
✖ dplyr::last()      masks data.table::last()
✖ purrr::transpose() masks data.table::transpose()
loan_tbl <- read_csv("data/kiva/kiva_loans.csv")
Rows: 671205 Columns: 20
── Column specification ────────────────────────────────────────────────────────
Delimiter: ","
chr  (10): activity, sector, use, country_code, country, region, currency, t...
dbl   (6): id, funded_amount, loan_amount, partner_id, term_in_months, lende...
dttm  (3): posted_time, disbursed_time, funded_time
date  (1): date

ℹ 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.

За пределами ядра tidyverse есть специальный пакет для скоростной загрузки данных – {vroom}.

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

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

loan_tbl %>%
  filter(sector == "Agriculture") %>%
  group_by(activity) %>%
  summarise(mean_loan = mean(loan_amount)) %>%
  arrange(desc(mean_loan))
# A tibble: 14 × 2
   activity         mean_loan
   <chr>                <dbl>
 1 Beekeeping           1472.
 2 Animal Sales         1137.
 3 Livestock            1128.
 4 Agriculture          1015.
 5 Cattle               1013.
 6 Flowers               853.
 7 Poultry               846.
 8 Land Rental           810.
 9 Farm Supplies         788.
10 Veterinary Sales      788.
11 Aquaculture           762.
12 Farming               708.
13 Dairy                 659.
14 Pigs                  471.

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

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

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

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

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

{data.table} vs tidyverse

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

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