3 Семантика ссылок
В этой виньетке обсуждается семантика ссылок в data.table, которая позволяет добавлять/обновлять/удалять столбцы в data.table по ссылке, а также комбинирование с i
и by
. Она предназначена для тех, кто уже знаком с синтаксисом data.table, его общим видом, способом выбора строк в i
, выбором и вычислением столбцов, выполнением агрегирования по группам. Если вы не знакомы с этими концепциями, пожалуйста, прочтите сперва виньетку “Введение в data.table”.
3.1 Данные
Мы будем использовать набор данных 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
3.2 Введение
В этой виньетке мы:
сперва коротко обсудим семантику ссылок и рассмотрим две разные формы использования оператора
:=
затем увидим, как мы можем добавлять/обновлять/удалять столбцы по ссылке в
j
с использованием:=
, и как это комбинировать сi
иby
и, наконец, мы увидим, как использовать оператор
:=
ради его побочного эффекта, и как мы можем его избежать при помощиcopy()
.
3.3 1. Семантика ссылок
Результатом всех операций, которые мы видели в предыдущей виньетке, был новый набор данных. Мы увидим, как добавлять новые столбцы, обновлять или удалять существующие столбцы в исходных данных.
3.3.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.
3.3.1.1 Поверхностная копия против глубокой копии
Поверхностная копия является всего лишь копией вектора-указателя столбцов (в соответствии со столбцами в data.frame или data.table). Настоящие данные физически не копируются в памяти.
С другой стороны, глубокая копия создает новую копию всех данных в новой области памяти.
С использованием оператора :=
в data.table никакие копии не создаются ни в случае (1), ни в случае (2) независимо от используемой версии R. Причина этого в том, что оператор :=
на месте обновляет столбцы data.table (по ссылке).
3.3.2 b) Оператор :=
Может быть использован в j
двумя способами:
- Форма
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]
- Функциональная форма
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
.
3.4 2. Добавление/обновление/удаление столбцов по ссылке
3.4.1 a) Добавление столбцов по ссылке
3.4.1.0.1 - Как мы можем добавить столбцы скорость и общая задержка каждого рейса в таблицу 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)]
3.4.1.1 Обратите внимание
Мы не присвоили разультат переменной
flights
.Таблица
flights
теперь содержит два новых столбца. Это то, что мы подразумеваем под добавлением по ссылке.Мы использовали функциональную форму, так что мы можем добавлять комментарии сбоку для объяснения, что делают эти вычисления. Вы также можете видеть форму
LHS := RHS
(закомментированную).
3.4.2 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.
3.4.2.1 – Заменить строки, где 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
3.4.3 c) Удаление столбца по ссылке
3.4.3.1 – Удаление столбца 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]
3.4.4 d) :=
вместе с группировкой при помощи by
В разделе 2b мы уже видели, как использовать :=
совместно с i
. Давайте посмотрим, как мы можем использовать :=
в сочетании с by
.
3.4.4.1 - Как мы можем добавить новый столбец, содержащий максимальную скорость для каждой пары 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.tableflights
изменяется на месте.Мы также можем задать
by
как символьный вектор, как мы видели в виньетке “Введение в data.table”, например,by = c("origin", "dest")
.
3.4.5 e) Множественные столбцы и :=
3.4.5.1 - Как мы может добавить еще две колонки, рассчитав 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.5 3 :=
и copy()
:=
изменяет исходный объект по ссылке. Помимо возможностей, которые мы уже обсудили, иногда мы можем захотеть использовать возможность обновления по ссылке ради его побочного эффекта. А в других случаях может быть нежелательно изменять исходный объект, и тогда мы можем использовать функцию copy()
, как мы сейчас увидим.
3.5.1 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.tableflights
, поскольку:=
выполняет операции по ссылке. ПосколькуDT
(аргумент функции) иflights
ссылаются на один и тот же объект в памяти, изменениеDT
также отражается на flights.ans
содержит максимальную скорость для каждого месяца.
3.5.2 b) Функция copy()
В предыдущем разделе мы использовали оператор :=
ради его побочного эффекта. Но, разумеется, это не всегда может быть желательно. Иногда мы хотели бы передать объект data.table
функции и использовать оператор :=
, но не хотели бы обновлять исходный объект. Мы можем достичь этого, используя функцию copy()
.
Функция copy()
создает глубокую копию исходного объекта, и поэтому любые последующие операции обновления по ссылке, выполняемые на скопированном объекте, не влияют на исходный объект.
Есть два конкретных случая, когда функция copy()
имеет важное значение:
- В отличие от ситуации, которую мы видели в предыдущем пункте, мы можем не хотеть, чтобы таблица 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.tableflights
. Эта таблица не содержит столбцаspeed
.ans
содержит максимальную скорость, соответствующую каждому месяцу.
Однако мы могли еще улучшить эту функциональность, делая поверхностное копирование вместо глубокого. На самом деле, мы бы очень хотели обеспечить эту функциональность в v1.9.8
. Мы коснемся этой темы еще раз в виньетке “data.table design”.
- Когда мы сохраняем имена столбцов в переменной, например,
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"
3.6 Резюме
3.6.0.1 - Оператор :=
Используется для добавления/обновления/удаления столбцов по ссылке.
Мы также увидели, как использовать
:=
вместе сi
иby
таким же образом, как мы видели в виньетке “Введение в data.table”. Еще мы можем использоватьkeyby
, цепочечные операции, передавать выражения вby
. Синтаксис согласован.Мы можем использовать оператор
:=
ради его побочного эффекта, или использоватьcopy()
, чтобы не изменять исходный объект при обновлении по ссылке.
Пока что мы увидели много всего, что может j
, как это комбинировать с by
, а также немного возможностей i
. Давайте обратим наше внимание на i
в следующей виньетке “Keys and fast binary search based subset” для выполнения молниеносного создания поднаборов при помощи установки ключей (keying) в data.tables.