Введение в dplyr*

При обработке данных вы должны:

Пакет dplyr делает каждый из этих шагов на столько простым и быстрым на сколько это возможно следующим образом:

Цель этого документа – дать представление о базовых инструментах предоставляемых dplyr, и продемонстрировать их применение к структурам данных типа data.frame. Также существуют отдельные руководства по следующим темам:

Набор данных nycflights13

Для исследования основных операций манипулирования данными в dplyr мы будем использовать встроенный набор данных nycflights13. Он содержит данные всех 336776 рейсов из Нью Йорка за 2013 год, и предоставлен, как написано в ?nycflights13, бюро по перемещениям США.

library(nycflights13)
dim(flights)
#> [1] 336776     16
head(flights)
#> Source: local data frame [6 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

dplyr может работать с data.frame непосредственно, но при работе с большими объёмами данных стоит конвертировать их в tbl_df, обёртку вокруг data.frame которая не станет по случайности выводить кучу данных на экран (это может очень медленным процессом).

Операции манипулирующие одной таблицей

Dplyr ставит своей целью дать функции для всех основных пераций манипулирования данным:

  • filter()slice())
  • arrange()
  • select()rename())
  • distinct()
  • mutate()transmute())
  • summarise()
  • sample_n() и sample_frac()

Для тех кто пользовался до этого plyr многие из них будут знакомы.

Фильтрация строк при помощи filter()

filter() позволяет вам выбрать подмножество строк из data.frame. Первый аргумент – имя набора данных, второй и последующие – условия фильтра в контексте этого набора данных.

Например, мы можем выбрать все вылет 1-го января следующим образом:

filter(flights, month == 1, day == 1)
#> Source: local data frame [842 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

Что будет эквивалентно более многословному (длиннее на целых 10 символов!) варианту:

flights[flights$month == 1 & flights$day == 1, ]

filter() работает аналогично subset() за тем исключением, что вы можете ему передать любое количество условий для фильтра, которые будут объединены вместе через & (логическое И, но не && с которым можно случайно перепутать). Вы можете также использовать любые другие логически связки:

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

Для выборки строк по позициям используется slice():

slice(flights, 1:10)
#> Source: local data frame [10 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

Упорядочение строк при помощи arrange()

arrange(), в свою очередь, работает аналогично filter() за исключением того, что вместо выбора строк она переупорядочивает их. Функция получает на вход имя таблицы данных и список имён колонок (или более сложное выражение) для упорядочения по ним. Если будет указано больше одной колонки, то каждая следующая колонка будет упорядочиваться в пределах каждого отдельного набора значений предыдущих:

arrange(flights, year, month, day)
#> Source: local data frame [336,776 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

Необходимо использовать desc() чтобы задать порядок по убыванию:

arrange(flights, desc(arr_delay))
#> Source: local data frame [336,776 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   9      641      1301     1242      1272      HA  N384HA
#> 2  2013     6  15     1432      1137     1607      1127      MQ  N504MQ
#> 3  2013     1  10     1121      1126     1239      1109      MQ  N517MQ
#> 4  2013     9  20     1139      1014     1457      1007      AA  N338AA
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

dplyr::arrange() работает так же как и plyr::arrange(). Это простая обёртка над order(), только позволяющая меньше набирать на клавиатуре. Предыдущий пример эквивалентен такому выражению:

flights[order(flights$year, flights$month, flights$day), ]
flights[order(desc(flights$arr_delay)), ]

Выбор колонок при помощи select()

Часто случается что при работе с большим набором данных вам на самом деле интересны только некоторые колонки. select() позволяет вам быстро сфокусироваться на интересующей части колонок, используя при этом символические имена на манер номеров колонок:

# Select columns by name
select(flights, year, month, day)
#> Source: local data frame [336,776 x 3]
#> 
#>    year month day
#> 1  2013     1   1
#> 2  2013     1   1
#> 3  2013     1   1
#> 4  2013     1   1
#> ..  ...   ... ...
# Select all columns between year and day (inclusive)
select(flights, year:day)
#> Source: local data frame [336,776 x 3]
#> 
#>    year month day
#> 1  2013     1   1
#> 2  2013     1   1
#> 3  2013     1   1
#> 4  2013     1   1
#> ..  ...   ... ...
# Select all columns except those from year to day (inclusive)
select(flights, -(year:day))
#> Source: local data frame [336,776 x 13]
#> 
#>    dep_time dep_delay arr_time arr_delay carrier tailnum flight origin
#> 1       517         2      830        11      UA  N14228   1545    EWR
#> 2       533         4      850        20      UA  N24211   1714    LGA
#> 3       542         2      923        33      AA  N619AA   1141    JFK
#> 4       544        -1     1004       -18      B6  N804JB    725    JFK
#> ..      ...       ...      ...       ...     ...     ...    ...    ...
#>    dest
#> 1   IAH
#> 2   IAH
#> 3   MIA
#> 4   BQN
#> ..  ...
#> Variables not shown: air_time (dbl), distance (dbl), hour (dbl), minute
#>   (dbl)

Эта функция работает подобно аргументу select в base::subset(). Отдельная функция добавлена для поддержания стройности идеологии dplyr, заключающейся в наличии функций каждая из которых делает только одну конкретную операцию и наиболее подходящим образом.

С select() используется ряд вспомогательных, такие как starts_with(), ends_with(), matches() и contains(). Они упрощают получение больших блоков колонок которые удовлетворяют некоторому критерию. Для получения подробностей смотрите ?select.

При помощи select() вы можете переименовывать колонки используя именованные аргументы:

select(flights, tail_num = tailnum)
#> Source: local data frame [336,776 x 1]
#> 
#>    tail_num
#> 1    N14228
#> 2    N24211
#> 3    N619AA
#> 4    N804JB
#> ..      ...

Но поскольку она отбрасывает все явно не указанные аргументы, используйте вместо неё для этого rename():

rename(flights, tail_num = tailnum)
#> Source: local data frame [336,776 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tail_num
#> 1  2013     1   1      517         2      830        11      UA   N14228
#> 2  2013     1   1      533         4      850        20      UA   N24211
#> 3  2013     1   1      542         2      923        33      AA   N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6   N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...      ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

Извлечение не повторяющихся (уникальных) строк

Чаще всего select() используют для выяснения какие значения принимают значения колонки. В частности это удобно использовать совместно с distinct(), операцией, которая возвращает только строки с уникальными значениями.

distinct(select(flights, tailnum))
#> Source: local data frame [4,044 x 1]
#> 
#>    tailnum
#> 1   N14228
#> 2   N24211
#> 3   N619AA
#> 4   N804JB
#> ..     ...
distinct(select(flights, origin, dest))
#> Source: local data frame [224 x 2]
#> 
#>    origin dest
#> 1     EWR  IAH
#> 2     LGA  IAH
#> 3     JFK  MIA
#> 4     JFK  BQN
#> ..    ...  ...

(Это очень похоже на base::unique(), но должно работать быстрее)

Добавление новых колонок при помощи mutate()

Бывает полезно не только выбрать интересующие колонки, но и вычислить на их основании значения некоторой другой колонки. Для этих целей предназначена mutate():

mutate(flights,
  gain = arr_delay - dep_delay,
  speed = distance / air_time * 60)
#> Source: local data frame [336,776 x 18]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl), gain (dbl), speed (dbl)

dplyr::mutate() работает аналогично plyr::mutate() и аналогично base::transform(). Главное отличие между mutate() and transform() в том что первая может ссылаться в вычислениях на колонку которая создаётся в рамках того же вызова функции:

mutate(flights,
  gain = arr_delay - dep_delay,
  gain_per_hour = gain / (air_time / 60)
)
#> Source: local data frame [336,776 x 18]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1   1      517         2      830        11      UA  N14228
#> 2  2013     1   1      533         4      850        20      UA  N24211
#> 3  2013     1   1      542         2      923        33      AA  N619AA
#> 4  2013     1   1      544        -1     1004       -18      B6  N804JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl), gain (dbl),
#>   gain_per_hour (dbl)
transform(flights,
  gain = arr_delay - delay,
  gain_per_hour = gain / (air_time / 60)
)
#> Error: object 'gain' not found

Если вам нужно сохранить только новые колонки используйте transmute():

transmute(flights,
  gain = arr_delay - dep_delay,
  gain_per_hour = gain / (air_time / 60)
)
#> Source: local data frame [336,776 x 2]
#> 
#>    gain gain_per_hour
#> 1     9      2.378855
#> 2    16      4.229075
#> 3    31     11.625000
#> 4   -17     -5.573770
#> ..  ...           ...

Вычисление итогов при помощи summarise()

Ещё одна операция – summarise(), она просто сворачивает таблицу данных в одну строку. Пока это не слишком полезно:

summarise(flights,
  delay = mean(dep_delay, na.rm = TRUE))
#> Source: local data frame [1 x 1]
#> 
#>      delay
#> 1 12.63907

Эта функция в точности соответствует plyr::summarise().

Выбор случайного набора строк при помощи sample_n() и sample_frac()

Вы можете воспользоваться sample_n() или sample_frac() для получения случайного набора строк, фиксированного числа с sample_n() или в фиксированной пропорции от всего набора с sample_frac().

sample_n(flights, 10)
#> Source: local data frame [10 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     1  21      652        -8      946       -11      B6  N636JB
#> 2  2013     5  17     2144        63       47        31      UA  N841UA
#> 3  2013    12   6     1521        -8     1721       -19      EV  N11165
#> 4  2013     2   1      703        -2     1035        14      B6  N591JB
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)
sample_frac(flights, 0.01)
#> Source: local data frame [3,368 x 16]
#> 
#>    year month day dep_time dep_delay arr_time arr_delay carrier tailnum
#> 1  2013     4  21      715         0      903       -13      DL  N309US
#> 2  2013    12  18     2041        71     2313        38      AA  N482AA
#> 3  2013     3  30     1833        13     2113       -22      AS  N549AS
#> 4  2013     2  17     1859        75     2051       103      UA  N78509
#> ..  ...   ... ...      ...       ...      ...       ...     ...     ...
#> Variables not shown: flight (int), origin (chr), dest (chr), air_time
#>   (dbl), distance (dbl), hour (dbl), minute (dbl)

Аргумент replace = TRUE позволяет повторно выбирать одну и ту же строку (как при бутстрепинге), опционально можно указать вес в итоговой выборке каждой исходной строки при помощи аргумента weight.

Итог

У вас может возникнуть впечатление что все перечисленные функции имеют что-то общее:

  • Первый аргумент – data.frame.
  • Последующие аргументы описываю то что вы хотите проделать с первым, и вы можете обращаться к атрибутам первого аргумента по именам, не используя для доступа $.
  • Возвращаемый результат – новый data.frame.

Все эти свойства вместе позволяют легко комбинировать множество вызовов в одну цепочку для вычисления сложного результата.

Эти пять функций создают основу для языка манипулирования данными. На самом простом уровне вы можете только изменить что-либо в наборе данных пятью способами: изменить порядок строк (arrange()), выбрать наблюдения или атрибуты (filter() и select()), добавить вычисляемые атрибуты (mutate()) или свернуть набор в итог (summarise()). Оставшаяся часть этого языка образуется из применения перечисленных функций к различным типам данных, например группированным данным, как описано ниже.

Операции над сгруппированными данными

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

Команды реагируют на группировку следующим образом:

В следующем примере мы разделим набор данных по самолётам и рассчитаем количество вылетов (count = n()) и средние дальность полёта (dist = mean(Distance, na.rm = TRUE)) и задержку вылета (delay = mean(ArrDelay, na.rm = TRUE)). Затем построим график при помощи ggplot2.

planes <- group_by(flights, tailnum)
delay <- summarise(planes,
  count = n(),
  dist = mean(distance, na.rm = TRUE),
  delay = mean(arr_delay, na.rm = TRUE))
delay <- filter(delay, count > 20, dist < 2000)

# Интересно, на сколько зависит 
# средняя задержка от средней дальности полёта 
ggplot(delay, aes(dist, delay)) +
  geom_point(aes(size = count), alpha = 1/2) +
  geom_smooth() +
  scale_size_area()

Вы использовали summarise() с функциями агрегирования, получающими вектор значений и возвращающими скалярное значение. Множество таких функций есть в базовой среде R, например min(), max(), mean(), sum(), sd(), median() и IQR(). dplyr предлагает ещё несколько полезных:

Например, мы можем использовать их чтобы найти количество самолётов и количество вылетов во все возможные пункты назначения:

destinations <- group_by(flights, dest)
summarise(destinations,
  planes = n_distinct(tailnum),
  flights = n()
)
#> Source: local data frame [105 x 3]
#> 
#>    dest planes flights
#> 1   ABQ    108     254
#> 2   ACK     58     265
#> 3   ALB    172     439
#> 4   ANC      6       8
#> ..  ...    ...     ...

Также вы можете применить собственную функцию. Для повышения производительности многие функции dplyr написаны на C++. Если вы хотите использовать собственную функцию на C++ обратите внимание на соответствующее руководство* для уточнения деталей.

При группировке по нескольким переменным вы можете также получить “послойные” итоги для каждого уровня в отдельности и по очереди. Этот приём позволяет кумулятивно получать свёртку набора данных:

daily <- group_by(flights, year, month, day)
(per_day   <- summarise(daily, flights = n()))
#> Source: local data frame [365 x 4]
#> Groups: year, month
#> 
#>    year month day flights
#> 1  2013     1   1     842
#> 2  2013     1   2     943
#> 3  2013     1   3     914
#> 4  2013     1   4     915
#> ..  ...   ... ...     ...
(per_month <- summarise(per_day, flights = sum(flights)))
#> Source: local data frame [12 x 3]
#> Groups: year
#> 
#>    year month flights
#> 1  2013     1   27004
#> 2  2013     2   24951
#> 3  2013     3   28834
#> 4  2013     4   28330
#> ..  ...   ...     ...
(per_year  <- summarise(per_month, flights = sum(flights)))
#> Source: local data frame [1 x 2]
#> 
#>   year flights
#> 1 2013  336776

Однако необходимо проявлять осторожность при таком методе получения итогов: он подойдёт для сумм и количеств, но нужно задуматься при использовании средних и дисперсий, и уж точно не применять с медианой.

Последовательные (конвейерные) манипуляции

API (легендарный мощный ЭПиАй) dplyr функционален, в том смысле, что вызовы функций не имеют побочных эффектов, и вы должны сами сохранять результаты. Это обстоятельство не способствует написанию элегантного кода если вам нужно выполнить множество операций подряд. Вы, конечно, можете выполнять их шаг за шагом:

a1 <- group_by(flights, year, month, day)
a2 <- select(a1, arr_delay, dep_delay)
a3 <- summarise(a2,
  arr = mean(arr_delay, na.rm = TRUE),
  dep = mean(dep_delay, na.rm = TRUE))
a4 <- filter(a3, arr > 30 | dep > 30)

Или, если вы не желаете сохранять промежуточные результаты, можно передавать вызовы функций как аргументы, вкладывая один в другой:

filter(
  summarise(
    select(
      group_by(flights, year, month, day),
      arr_delay, dep_delay
    ),
    arr = mean(arr_delay, na.rm = TRUE),
    dep = mean(dep_delay, na.rm = TRUE)
  ),
  arr > 30 | dep > 30
)
#> Source: local data frame [49 x 5]
#> Groups: year, month
#> 
#>    year month day      arr      dep
#> 1  2013     1  16 34.24736 24.61287
#> 2  2013     1  31 32.60285 28.65836
#> 3  2013     2  11 36.29009 39.07360
#> 4  2013     2  27 31.25249 37.76327
#> ..  ...   ... ...      ...      ...

Что создаёт сложности с чтением, так как порядок выполнения начинается из середины выражения и аргументы отдаляются от текста вызова функции. Чтобы обойти это проблему dplyr использует оператор %>%. x %>% f(y) преобразуется в f(x, y), что можно использовать для преобразования кода вызовов в удобный для чтения слева направо, сверху вниз:

flights %>%
  group_by(year, month, day) %>%
  select(arr_delay, dep_delay) %>%
  summarise(
    arr = mean(arr_delay, na.rm = TRUE),
    dep = mean(dep_delay, na.rm = TRUE)
  ) %>%
  filter(arr > 30 | dep > 30)

Other data sources

Другие источники данных

Также как с data.frame, dplyr может работать с данными представленными другими способами, такими как data tables, базы данных и многомерные массивы.

Data table

dplyr также предлагает все перечисленные методы манипулирования данным и для объектов типа data.tables*, вы просто должны заменить набор данных на data.table.

data.table может оказаться быстрее во многих случаях, потому что возможно выполнение нескольких операций одновременно. Например, вы можете выполнить mutate и select за один вызов, и data.table сообразит что нет нужды рассчитывать новую переменную в строках которые должны быть отфильтрованы.

Преимущества при использовании data.tables следующие:

  • В большинстве случаев data.tables изолирует вас от данных непосредственно, и защищает их таким образом от непреднамеренного изменения.
  • Вместо изощрённого использования встроенного оператора [, предлагается множество относительно простых методов.

Базы данных

dplyr позволяет использовать удалённые базы данных так же как data.frame. Избавляя таким образом от необходимости постоянно переключать мышление между языками. Для уточнения деталей необходимо обратиться к соответствующему руководству*.

По сравнению с другим вариантами использования баз данных: * скрывается, на сколько это возможно, факт использования удалённой базы данных * исчезает необходимость изучать какой-либо диалект sql (хотя это может быть полезно!) * предоставляется прослойка между множеством различных реализаций интерфейсов баз данных.

Многомерные массивы/кубы

tbl_cube() предлагает экспериментальный интерфейс к многомерным массивам или кубам (как OLAP-кубы) . Если вы используете подобное представление в R, пожалуйста, свяжитесь с автором чтобы он лучше понял ваши потребности.

Сравнение с другими подходами

В сравнение со всеми существующими альтернативами, dplyr:

В сравнении с функциями из основной поставки:

В сравнении с plyr:

В сравнении с использованием виртуальных data.frame: