пятница, 9 октября 2015 г.

data.table. Семантика ссылок (перевод)


В этой виньетке обсуждается семантика ссылок в data.table, которая позволяет добавлять/обновлять/удалять столбцы в data.table по ссылке, а также комбинирование с i и by. Она предназначена для тех, кто уже знаком с синтаксисом data.table, его общим видом, способом выбора строк в i, выбором и вычислением столбцов, выполнением агрегирования по группам. Если вы не знакомы с этими концепциями, пожалуйста, прочтите сперва виньетку “Введение в data.table”.

Данные

Мы будем использовать набор данных flights, так же как в виньетке “Введение в data.table”.
flights <- fread("https://raw.githubusercontent.com/wiki/arunsrinivasan/flights/NYCflights14/flights14.csv")
flights
#         year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
#      1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1
#      2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3
#      3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21
#      4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29
#      5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117
#     ---                                                                                      
# 253312: 2014    10  31     1459         1     1747       -30         0      UA  N23708   1744
# 253313: 2014    10  31      854        -5     1147       -14         0      UA  N33132   1758
# 253314: 2014    10  31     1102        -8     1311        16         0      MQ  N827MQ   3591
# 253315: 2014    10  31     1106        -4     1325        15         0      MQ  N511MQ   3592
# 253316: 2014    10  31      824        -5     1045         1         0      MQ  N813MQ   3599
#         origin dest air_time distance hour min
#      1:    JFK  LAX      359     2475    9  14
#      2:    JFK  LAX      363     2475   11  57
#      3:    JFK  LAX      351     2475   19   2
#      4:    LGA  PBI      157     1035    7  22
#      5:    JFK  LAX      350     2475   13  47
#     ---                                       
# 253312:    LGA  IAH      201     1416   14  59
# 253313:    EWR  IAH      189     1400    8  54
# 253314:    LGA  RDU       83      431   11   2
# 253315:    LGA  DTW       75      502   11   6
# 253316:    LGA  SDF      110      659    8  24
dim(flights)
# [1] 253316     17

Введение

В этой виньетке мы:
  1. сперва коротко обсудим семантику ссылок и рассмотрим две разные формы использования оператора :=
  2. затем увидим, как мы можем добавлять/обновлять/удалять столбцы по ссылке в j с использованием :=, и как это комбинировать с i и by
  3. и, наконец, мы увидим, как использовать оператор := ради его побочного эффекта, и как мы можем его избежать при помощи copy().

1. Семантика ссылок

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

a) Бэкграунд

Прежде чем заняться семантикой ссылок, рассмотрим следующую таблицу data.frame:
DF = data.frame(ID = c("b","b","b","a","a","c"), a = 1:6, b = 7:12, c=13:18)
DF
#   ID a  b  c
# 1  b 1  7 13
# 2  b 2  8 14
# 3  b 3  9 15
# 4  a 4 10 16
# 5  a 5 11 17
# 6  c 6 12 18
Когда вы выполняем:
DF$c <- 18:13               # (1) -- replace entire column
# or
DF$c[DF$ID == "b"] <- 15:13 # (2) -- subassign in column 'c'
и (1), и (2) приводят к созданию глубокой копии всей таблицы data.frame в R версии < 3.1. Данные копируются больше одного раза. Для увеличения производительности путем избегания этих ненужных копий data.table использует доступный, но неиспользуемый в R оператор :=.
Большое увеличение производительности было сделано в R v3.1, в результате чего в случае (1) создается поверхностная, а не глубокая копия. Тем не менее, для (2) по-прежнему создается глубокая копия всего столбца даже в R v3.1+. Это означает, что чем больше столбцов участвуют в частичном присвоении в одном запросе, тем более глубокие копии создает R.

Поверхностная копия против глубокой копии

Поверхностная копия является всего лишь копией вектора-указателя столбцов (в соответствии со столбцами в data.frame или data.table). Настоящие данные физически не копируются в памяти.
С другой стороны, глубокая копия создает новую копию всех данных в новой области памяти.
С использованием оператора := в data.table никакие копии не создаются ни в случае (1), ни в случае (2) независимо от используемой версии R. Причина этого в том, что оператор := на месте обновляет столбцы data.table (по ссылке).

b) Оператор :=

Может быть использован в j двумя способами:
  1. Форма LHS := RHS
DT[, c("colA", "colB", ...) := list(valA, valB, ...)]

# when you have only one column to assign to you 
# can drop the quotes and list(), for convenience
DT[, colA := valA]
  1. Функциональная форма
DT[, `:=`(colA = valA, # valA is assigned to colA
          colB = valB, # valB is assigned to colB
          ...
)]
Обратите внимаени, что приведенныый выше код объясняет, как можно использовать :=. Это не рабочие примеры. Мы начнем использовать этот оператор с таблицей data.table flights в следующем разделе.
  • Форма (a) удобна для программирования и особенно полезна, когда вы не знаете заранее столбцы для присваивания значений.
  • С другой стороны, форма (b) удобна, когда вы хотите записать комментарии на будущее.
  • Результат возвращается скрыто.
  • Поскольку оператор := доступен в j, мы можем комбинировать его с операциями i и by, подобно операциям агрегирования, которые мы видели в предыдущей виньетке.
Обратите внимание, что в двух формах :=, показанных выше, мы не присваиваем результат переменной, потому что не нуждаемся в этом. Исходная таблица data.table изменяется по ссылке. Давайте рассмотрим примеры, чтобы понять, что под этим подразумевается.
В оставшейся части виньетки мы будем работать с набором данных flights.

2. Добавление/обновление/удаление столбцов по ссылке

a) Добавление столбцов по ссылке

- Как мы можем добавить столбцы скорость и общая задержка каждого рейса в таблицу data.table flights?
flights[, `:=`(speed = distance / (air_time/60), # speed in km/hr
               delay = arr_delay + dep_delay)]   # delay in minutes
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed delay
# 1:  LAX      359     2475    9  14 413.6490    27
# 2:  LAX      363     2475   11  57 409.0909    10
# 3:  LAX      351     2475   19   2 423.0769    11
# 4:  PBI      157     1035    7  22 395.5414   -34
# 5:  LAX      350     2475   13  47 424.2857     3
# 6:  LAX      339     2454   18  24 434.3363     4

## alternatively, using the 'LHS := RHS' form
# flights[, c("speed", "delay") := list(distance/(air_time/60), arr_delay + dep_delay)]

Обратите внимание

  • Мы не присвоили разультат переменной flights.
  • Таблица flights теперь содержит два новых столбца. Это то, что мы подразумеваем под добавлением по ссылке.
  • Мы использовали функциональную форму, так что мы можем добавлять комментарии сбоку для объяснения, что делают эти вычисления. Вы также можете видеть форму LHS := RHS (закомментированную).

b) Обновление некоторых строк в столбцах по ссылке - частичное присваивание по ссылке

Давайте взглянем на все значения hours, доступные в таблице data.table flights:
# get all 'hours' in flights
flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24
Мы видим, что имеется 25 уникальных значений - есть и 0, и 24. Давайте заменим 24 на 0.

– Заменить строки, где hour == 24, на 0

# subassign by reference
flights[hour == 24L, hour := 0L]
  • Мы можем использовать i вместе с := в j тем же способом, как мы видели в виньетке “Введение в data.table”.
  • Столбец hour заменяется 0 только для индексов строк, для которых условие hour == 24L, определенное в i, возвращает TRUE.
  • := возвращает результат скрыто. Иногда бывает нужно увидеть результат после присваивания. Мы можем добиться этого, добавив пустой оператор [] в конце запроса, как показано ниже:
flights[hour == 24L, hour := 0L][]
#         year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight
#      1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1
#      2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3
#      3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21
#      4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29
#      5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117
#     ---                                                                                      
# 253312: 2014    10  31     1459         1     1747       -30         0      UA  N23708   1744
# 253313: 2014    10  31      854        -5     1147       -14         0      UA  N33132   1758
# 253314: 2014    10  31     1102        -8     1311        16         0      MQ  N827MQ   3591
# 253315: 2014    10  31     1106        -4     1325        15         0      MQ  N511MQ   3592
# 253316: 2014    10  31      824        -5     1045         1         0      MQ  N813MQ   3599
#         origin dest air_time distance hour min    speed delay
#      1:    JFK  LAX      359     2475    9  14 413.6490    27
#      2:    JFK  LAX      363     2475   11  57 409.0909    10
#      3:    JFK  LAX      351     2475   19   2 423.0769    11
#      4:    LGA  PBI      157     1035    7  22 395.5414   -34
#      5:    JFK  LAX      350     2475   13  47 424.2857     3
#     ---                                                      
# 253312:    LGA  IAH      201     1416   14  59 422.6866   -29
# 253313:    EWR  IAH      189     1400    8  54 444.4444   -19
# 253314:    LGA  RDU       83      431   11   2 311.5663     8
# 253315:    LGA  DTW       75      502   11   6 401.6000    11
# 253316:    LGA  SDF      110      659    8  24 359.4545    -4
Давайте взглянем на все значения hours для проверки.
# check again for '24'
flights[, sort(unique(hour))]
#  [1]  0  1  2  3  4  5  6  7  8  9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

c) Удаление столбца по ссылке

– Удаление столбца delay

flights[, c("delay") := NULL]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed
# 1:  LAX      359     2475    9  14 413.6490
# 2:  LAX      363     2475   11  57 409.0909
# 3:  LAX      351     2475   19   2 423.0769
# 4:  PBI      157     1035    7  22 395.5414
# 5:  LAX      350     2475   13  47 424.2857
# 6:  LAX      339     2454   18  24 434.3363

## or using the functional form
# flights[, `:=`(delay = NULL)]
  • Присвоение значения NULL столбцу удаляет его. И это происходит мгновенно.
  • Мы можем такое передавать имена столбцов вместо их имен в LHS, хотя хорошая практика программирования заключается в использовании имен.
  • Когда нужно удалить всего один столбец, мы можем опустить c() и двойные кавычки и использовать для удобства просто имя столбца. Эквивалент кода выше:
flights[, delay := NULL]

d) := вместе с группировкой при помощи by

В разделе 2b мы уже видели, как использовать := совместно с i. Давайте посмотрим, как мы можем использовать := в сочетании с by.

- Как мы можем добавить новый столбец, содержащий максимальную скорость для каждой пары origin, dest?

flights[, max_speed := max(speed), by=.(origin, dest)]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed max_speed
# 1:  LAX      359     2475    9  14 413.6490  526.5957
# 2:  LAX      363     2475   11  57 409.0909  526.5957
# 3:  LAX      351     2475   19   2 423.0769  526.5957
# 4:  PBI      157     1035    7  22 395.5414  517.5000
# 5:  LAX      350     2475   13  47 424.2857  526.5957
# 6:  LAX      339     2454   18  24 434.3363  518.4507
  • Мы добавляем новый столбец max_speed по ссылке, используя оператор :=.
  • Мы задаем столбцы для группировки, как было показано в виньетке “Введение в data.table”. Для каждой группы было вычислено выражение max(speed), которое возвращает единственное значение. Это выражение повторяется, чтобы соответствовать длине группы. Еще раз: никакие копии не создаются. Таблица data.table flights изменяется на месте.
  • Мы также можем задать by как символьный вектор, как мы видели в виньетке “Введение в data.table”, например, by = c("origin", "dest").

e) Множественные столбцы и :=

- Как мы может добавить еще две колонки, рассчитав max() для dep_delay и arr_delay для каждого месяца, используя .SD?

in_cols  = c("dep_delay", "arr_delay")
out_cols = c("max_dep_delay", "max_arr_delay")
flights[, c(out_cols) := lapply(.SD, max), by = month, .SDcols = in_cols]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed max_speed max_dep_delay max_arr_delay
# 1:  LAX      359     2475    9  14 413.6490  526.5957           973           996
# 2:  LAX      363     2475   11  57 409.0909  526.5957           973           996
# 3:  LAX      351     2475   19   2 423.0769  526.5957           973           996
# 4:  PBI      157     1035    7  22 395.5414  517.5000           973           996
# 5:  LAX      350     2475   13  47 424.2857  526.5957           973           996
# 6:  LAX      339     2454   18  24 434.3363  518.4507           973           996
  • Мы используем форму LHS := RHS. Сохраняем имена исходных и результирующих столбцов в отдельных переменных и передаем их в SDcols и LHS (для лучшей читаемости).
  • Отметим, что поскольку мы допускаем присваивание по ссылке без заключения имени столбца в кавычки для случая отдельного столбца, как объясняется в разделе 2c, мы не можем записать out_cols := lapply(.SD, max). Это приведет к добавлению единственного столбца с именем out_col. Вместо этого мы должны использовать c(out_cols) или просто (out_cols). Использования ( вокруг имени переменной достаточно, чтобы различать эти два случая.
  • Форма LHS := RHS позволяет нам работать с несколькими столбцами. В RHS для расчета max для столбцов, заданных в .SDcols, мы используем базовую функцию lapply() вместе с .SD тем же способом, как мы видели ранее в виньетке “Введение в data.table”. Возвращается список из двух элементов, содержащих максимальные значения dep_delay и arr_delay для каждой группы.
Прежде чем перейти к следующему разделу, давайте удалим новые столбцы speed, max_speed, max_dep_delay и max_arr_delay.
# RHS gets automatically recycled to length of LHS
flights[, c("speed", "max_speed", "max_dep_delay", "max_arr_delay") := NULL]
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
# 3:  LAX      351     2475   19   2
# 4:  PBI      157     1035    7  22
# 5:  LAX      350     2475   13  47
# 6:  LAX      339     2454   18  24

3) := и copy()

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

a) Использование оператора := ради его побочного эффекта

Скажем, мы хотели бы создать функцию, которая будет возвращать максимальную скорость для каждого месяца. Но, в то же время, мы хотели бы добавить столбец speed в таблицу flight. Мы можем написать следующую простую функцию:
foo <- function(DT) {
  DT[, speed := distance / (air_time/60)]
  DT[, .(max_speed = max(speed)), by=month]
}
ans = foo(flights)
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min    speed
# 1:  LAX      359     2475    9  14 413.6490
# 2:  LAX      363     2475   11  57 409.0909
# 3:  LAX      351     2475   19   2 423.0769
# 4:  PBI      157     1035    7  22 395.5414
# 5:  LAX      350     2475   13  47 424.2857
# 6:  LAX      339     2454   18  24 434.3363
head(ans)
#    month max_speed
# 1:     1  535.6425
# 2:     2  535.6425
# 3:     3  549.0756
# 4:     4  585.6000
# 5:     5  544.2857
# 6:     6  608.5714
  • Обратите внимание, что новый столбец speed был добавлен в таблицу data.table flights, поскольку := выполняет операции по ссылке. Поскольку DT (аргумент функции) и flights ссылаются на один и тот же объект в памяти, изменение DT также отражается на flights.
  • ans содержит максимальную скорость для каждого месяца.

b) Функция copy()

В предыдущем разделе мы использовали оператор := ради его побочного эффекта. Но, разумеется, это не всегда может быть желательно. Иногда мы хотели бы передать объект data.table функции и использовать оператор :=, но не хотели бы обновлять исходный объект. Мы можем достичь этого, используя функцию copy().
Функция copy() создает глубокую копию исходного объекта, и поэтому любые последующие операции обновления по ссылке, выполняемые на скопированном объекте, не влияют на исходный объект.
Есть два конкретных случая, когда функция copy() имеет важное значение:
  1. В отличие от ситуации, которую мы видели в предыдущем пункте, мы можем не хотеть, чтобы таблица data.table, передаваемая функции, изменялась по ссылке. В качестве примера давайте рассмотрим задачу из предыдущего раздела, за исключением того, что не хотим изменять flights по ссылке.
Сперва удалим столбец speed, созданный в предыдущем разделе.
flights[, speed := NULL]
Теперь мы можем выполнить задачу следующим образом:
foo <- function(DT) {
  DT <- copy(DT)                             ## deep copy
  DT[, speed := distance / (air_time/60)]    ## doesn't affect 'flights'
  DT[, .(max_speed = max(speed)), by=month]
}
ans <- foo(flights)
head(flights)
#    year month day dep_time dep_delay arr_time arr_delay cancelled carrier tailnum flight origin
# 1: 2014     1   1      914        14     1238        13         0      AA  N338AA      1    JFK
# 2: 2014     1   1     1157        -3     1523        13         0      AA  N335AA      3    JFK
# 3: 2014     1   1     1902         2     2224         9         0      AA  N327AA     21    JFK
# 4: 2014     1   1      722        -8     1014       -26         0      AA  N3EHAA     29    LGA
# 5: 2014     1   1     1347         2     1706         1         0      AA  N319AA    117    JFK
# 6: 2014     1   1     1824         4     2145         0         0      AA  N3DEAA    119    EWR
#    dest air_time distance hour min
# 1:  LAX      359     2475    9  14
# 2:  LAX      363     2475   11  57
# 3:  LAX      351     2475   19   2
# 4:  PBI      157     1035    7  22
# 5:  LAX      350     2475   13  47
# 6:  LAX      339     2454   18  24
head(ans)
#    month max_speed
# 1:     1  535.6425
# 2:     2  535.6425
# 3:     3  549.0756
# 4:     4  585.6000
# 5:     5  544.2857
# 6:     6  608.5714
  • Использование функции copy() не обновляет по ссылке таблицу data.table flights. Эта таблица не содержит столбца speed.
  • ans содержит максимальную скорость, соответствующую каждому месяцу.
Однако мы могли еще улучшить эту функциональность, делая поверхностное копирование вместо глубокого. На самом деле, мы бы очень хотели обеспечить эту функциональность в v1.9.8. Мы коснемся этой темы еще раз в виньетке “data.table design”.
  1. Когда мы сохраняем имена столбцов в переменной, например, DT_n = names(DT), а затем добавляем/обновляем/удаляем столбцы по ссылке, это также изменит DT_n, если мы не выполним copy(names(DT)).
DT = data.table(x=1, y=2)
DT_n = names(DT)
DT_n
# [1] "x" "y"

## add a new column by reference
DT[, z := 3]

## DT_n also gets updated
DT_n
# [1] "x" "y" "z"

## use `copy()`
DT_n = copy(names(DT))
DT[, w := 4]

## DT_n doesn't get updated
DT_n
# [1] "x" "y" "z"

Резюме

- Оператор :=

  • Используется для добавления/обновления/удаления столбцов по ссылке.
  • Мы также увидели, как использовать := вместе с i и by таким же образом, как мы видели в виньетке “Введение в data.table”. Еще мы можем использовать keyby, цепочечные операции, передавать выражения в by. Синтаксис согласован.
  • Мы можем использовать оператор := ради его побочного эффекта, или использовать copy(), чтобы не изменять исходный объект при обновлении по ссылке.
Пока что мы увидели много всего, что может j, как это комбинировать с by , а также немного возможностей i. Давайте обратим наше внимание на i в следующей виньетке “Keys and fast binary search based subset” для выполнения молниеносного создания поднаборов при помощи установки ключей (keying) в data.tables.

Комментариев нет:

Отправить комментарий