Обновляет data.table при передаче в качестве аргумента функции
Когда я передаю data.table в качестве аргумента функции, я могу обновить эту таблицу «по ссылке» в вызываемой функции, и результаты будут применены к исходному объекту. Однако, если я делаю что-то, что требует «глубокой копии» (например, rbindlist для добавления строк), копия существует только в вызываемой функции. Исходный объект остается в вызывающем кадре без изменений.
library(data.table)
l1 <- function(a1, action='update'){
b <- l2(a1, action)
print('l1')
print(a1)
}
l2 <- function(a2, action){
c <- l3(a2, action)
print('l2')
print(a2)
}
l3 <- function(a3, action){
if (action == 'update') a3[, col2 := col + 1]
if (action == 'append') a3 <- rbindlist(list(a3, data.table(col = c(21, 22))), fill=TRUE)
if (action == 'forceupdate') assign('DT',
rbindlist(list(a3, data.table(col = c(21, 22))), fill=TRUE),
envir = parent.frame(3))
print('l3')
print(a3)
a3
}
DT <- data.table(col = c(1, 2, 3))
print(DT)
#> col
#> 1: 1
#> 2: 2
#> 3: 3
l1(DT, 'update')
#> [1] "l3"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> [1] "l2"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> [1] "l1"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
print(DT)
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
l1(DT, 'append')
#> [1] "l3"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> 4: 21 NA
#> 5: 22 NA
#> [1] "l2"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> [1] "l1"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
print(DT)
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
l1(DT, 'forceupdate')
#> [1] "l3"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> [1] "l2"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> [1] "l1"
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
print(DT)
#> col col2
#> 1: 1 2
#> 2: 2 3
#> 3: 3 4
#> 4: 21 NA
#> 5: 22 NA
Создано 22.04.2021 пакетом REPEX (v1.0.0)
В этом примере есть трехуровневый стек вызовов функций. Аргумент на первом уровне передается вниз по стеку и обновляется в функции l3.
Используя синтаксис обновления data.table, l3 добавляет к объекту новый столбец, в результате чего исходный объект изменяется «на месте», а результаты видны на каждом уровне в стеке вызовов.
Однако, если я добавляю строки с помощью rbindlist, копия создается внутри кадра l3, и это не влияет на исходный объект или любое его представление в родительских вызовах.
Если я назначу изменение «исходному кадру», оно будет там видно, но промежуточные вызовы не видят изменения.
Есть ли способ отразить результаты этой «глубокой копии» в вызывающем стеке?
Еслиassign
это путь, я был бы признателен за пример того, как установить имя и среду базового объекта данных, чтобы это назначение можно было выполнить без жесткого кодирования.

2 ответа
(Пожалуйста, не используйте это случайно в производстве. :-)
Я думаю, что этот вопрос действительно дублирует Вставить строку в data.table , Добавить строку по ссылке в конце объекта data.table и Как удалить строку по ссылке в data.table? . Несколько раз было сказано, что возможность добавлять строки по ссылке «можно» сделать, но это нетривиально (и еще не было сделано). Итог, единорогdata.table::insert
функция не существует (пока).
Хотя мне не нравится идея использования<<-
иassign
, вот * хак *, который достигает этого ... с огромными оговорками.
func <- function(.x) {
nm <- deparse(substitute(.x))
stopifnot(
"'.x' must be a whole data.table" =
nm == make.names(nm)
)
env <- parent.frame()
while (!is.null(env) && !exists(nm, envir=env, inherits=FALSE)) {
env <- parent.env(env)
}
stopifnot(!is.null(env))
assign(nm, rbindlist(list(.x, .x[1,])), envir=env)
}
Здесь он работает по назначению:
DT <- data.table(col1=1:3,col2=11:13)
DT
# col1 col2
# <int> <int>
# 1: 1 11
# 2: 2 12
# 3: 3 13
func(DT)
DT
# col1 col2
# <int> <int>
# 1: 1 11
# 2: 2 12
# 3: 3 13
# 4: 1 11
Однако, если мы передаем отфильтрованную / разбитую на подмножество таблицу, мы должны ошибиться:
func(DT[1,])
# Error in func(DT[1, ]) : '.x' must be a whole data.table
Почему? Позвольте мне продемонстрировать. Во-первых, я собираюсь изменитьfunc
так что мы найдем оригиналDT
, а не только частичное:
func <- function(.x) {
nm <- deparse(substitute(.x))
nm <- gsub("[[({].*", "", nm) # turns 'DT[1,]' --> 'DT'
env <- parent.frame()
# ... everything else here
}
DT <- data.table(col1=1:3,col2=11:13)
func(DT[1,])
DT
# col1 col2
# <int> <int>
# 1: 1 11
# 2: 1 11
DT <- data.table(col1=1:3,col2=11:13)
func(DT[,1])
DT
# col1
# <int>
# 1: 1
# 2: 2
# 3: 3
# 4: 1
func(copy(DT))
# Error in assign(nm, rbindlist(list(.x, .x[1, ])), envir = env) :
# cannot change value of locked binding for 'copy'
Все три из этих сценариев правдоподобны в обычном коде R, где функции может потребоваться только подмножество данных. И все три демонстрируют опасность попыток сделать вывод об исходном объекте на основе данных, переданных в функцию. Я уверен, что кто-нибудь может добавить логики кfunc
таким образом, что он уловил бы некоторые из этих условий, но это выходит за рамки моего уровня * взлома *, чем я уже достиг с вышеупомянутым.
Цель этого упражнения заключалась в том, чтобы продемонстрировать, что в идеальных обстоятельствах, когда вызывающий абонент знает о рисках и всегда передает все (не подмножество / фильтр)data.table
, эта функция будет эффективно добавлять строки по ссылке . Но, как мы знаем, это не просто ссылка, а просто скрытие уловки и вздутия памяти за дымом и зеркалами. И теряя немногоdata.table
Скрытность / потрясающая эффективность в процессе.
(Пожалуйста, не используйте это случайно в производстве. :-)
-
0Очень полезный анализ @ r2evans.
data.table::insert
будет решить проблему, и вашfunc
адрес последней части моего вопроса. Однако еще одним ограничением этого присваивания является то, что оно обновляет базовую таблицу, но этого не видит промежуточная функция в стеке вызовов, которая передала таблицу «по ссылке». -
0Правда, с этим много проблем. Я не считаю, что этот ответ является предпочтительным или каноническим подходом,
data.table
-devs, это просто ** хак *. Альтернативный подход - передатьdata.table
через среду и внести все изменения / дополнения в env. Это допускает семантику по ссылке, но требует немного больше усилий как для вызывающего, так и для вызываемого.
Хотя вы не можете добавлять строки по ссылке наdata.table
объект, вы можете заранее выделить дополнительные строки для будущих обновлений по ссылке. В зависимости от вашего фактического варианта использования этот подход стоит рассмотреть.
В приведенном ниже примере я начинаю с выделения 1000 строк вDB
стол, хотя стартовыйDT
имеет только 3 значения. Затем я могу выполнить произвольное количество «вставок», если я не превысил исходное выделение.
## Create a "Data Base" table with an over-allocated number of rows
DB <- data.table(ID = seq_len(1e3),
InUse = FALSE,
col1 = as.integer(NA),
col2 = as.integer(NA), key = "ID")
## Starting Table DT
DT <- data.table(InUse = TRUE,
col1 = c(1, 2, 3),
col2 = as.integer(NA))
## Add an ID lookup key
DT[, ID := seq_len(.N)]
## Populate the "Data Base" from Starting DT
setkey(DT,ID)
DB[DT, c("InUse","col1","col2") := .(i.InUse,i.col1, i.col2)]
l3 <- function(x, action){
if (action == 'update') x[, col2 := col1 + 1L]
if (action == 'append') {
## Define rows to be appended
NR <- data.table(InUse = TRUE,
col1 = sample.int(10L,sample.int(n = 3L, size = 1L)),
col2 = as.integer(NA))
## Allocate unused ID's for new rows
NR[,ID := seq_len(.N) + x[InUse == TRUE,ID[.N]]]
## Allocate values for new rows by joining on ID
x[NR, c("InUse","col1","col2") := .(i.InUse,i.col1, i.col2), on = .(ID)]
}
}
set.seed(1L)
DB[InUse == TRUE]
# ID InUse col1 col2
# 1: 1 TRUE 1 NA
# 2: 2 TRUE 2 NA
# 3: 3 TRUE 3 NA
l3(DB, "update")
DB[InUse == TRUE]
# ID InUse col1 col2
# 1: 1 TRUE 1 2
# 2: 2 TRUE 2 3
# 3: 3 TRUE 3 4
l3(DB,"append")
l3(DB, "update")
DB[InUse == TRUE]
# ID InUse col1 col2
# 1: 1 TRUE 1 2
# 2: 2 TRUE 2 3
# 3: 3 TRUE 3 4
# 4: 4 TRUE 4 5
l3(DB,"append")
l3(DB, "update")
DB[InUse == TRUE]
# ID InUse col1 col2
# 1: 1 TRUE 1 2
# 2: 2 TRUE 2 3
# 3: 3 TRUE 3 4
# 4: 4 TRUE 4 5
# 5: 5 TRUE 1 2
# 6: 6 TRUE 2 3
# 7: 7 TRUE 5 6
На самом деле это достаточно производительно, поскольку создание новых строк, которые будут добавлены, является самой медленной частью процесса. В приведенном ниже примере я упростил функцию, сделав ее «только для вставки», используяdata.table::set
за производительность, с двумя аргументами -DB
для «базы данных» и таблицыnew
со строками, которые нужно вставить.
insert <- function(x, new){
## Allocate unused ID's for new rows
set(new, j = "InUse", value = TRUE)
set(new, j = "ID", value = seq_len(nrow(new)) + x[InUse == FALSE,min(ID)] - 1L)
setkey(new,ID)
## Allocate values for new rows
x[NR, c("InUse","col","col2") := .(i.InUse,i.col, i.col2), on = .(ID)]
}
## Create a "Data Base" table with an over-allocated number of rows
DB <- data.table(ID = seq_len(1e5),
InUse = FALSE,
col1 = as.integer(NA),
col2 = as.integer(NA), key = "ID")
NR <- data.table(col = 1L,
col2 = 2L)
microbenchmark(insert(DB,NR),times = 1000)
# Unit: milliseconds
# expr min lq mean median uq max neval
# insert(DB, NR) 4.081703 4.231624 4.645903 4.278256 4.362353 60.53741 1000
Существует множество показателей производительности для «скорости вставки» базы данных, и все они различаются в зависимости от размера / формы вставляемых данных, но 4,6 миллисекунды (217 вставок в секунду) кажутся достаточно быстрыми для многих случаев использования. могу представить (возможно, блестящее приложение) .
-
0Очень полезно в хорошо контролируемых ситуациях, когда можно гарантировать, что предварительного распределения будет достаточно. Я видел код, который «расширяется» блоками, если предварительное выделение памяти вот-вот будет нарушено, но для этого снова требуется «глубокая копия».
Другие вопросы
- Создайте сводную таблицу двух категориальных и числовых переменных1 ответ
У меня есть следующий гипотетический фреймворкRegion <- c("District A", "District B","District A","District A","District B") Gender <- c("Male","Male","Female", "Male","Female") Age <- c(20, ...
- Создайте цикл для генерации имен столбцов в большом списке1 ответ
Я хотел бы создать цикл, чтобы изменить имена столбцов, как показано:a <- c("day", "month", "year", "flow") У меня есть большой список из 6937 элементов, которые мне удалось импортировать в R:library(tidyverse) library(readtext) txt_files_ls <- paste...
- Построение лаговой матрицы в R1 ответ
Каждый год несколько человек переезжают в деревню. Это представлено следующим вектором:x <- c(304, 213, 688, 400, 122, 449, 143, 90) Ежегодно из села уезжает 10% людей. Доля людей из каждой когорты, остающихся через определенное количество лет (за 10 лет):decay <- (1-0.1)^(0:10) Используя R, к...
- назначать имена столбцам фреймов данных в списке1 ответ
У меня есть список фреймов данных# Create dummy data df1<-data.frame( c(1,2,3),c(2,3,4)) df2<-data.frame(c(5,6,7),c(4,5,6)) # Create a list l<-list(df1, df2) Я хочу присвоить столбцам имена. В видеl[[1]][,1] дает мне доступ к первому столбцу, я думал, что могу назначить 'names' в ...
- Моя горизонтальная полоса обрезает текст, когда я сохраняю изображение. Как сделать полосы короче, а текст длиннее?1 ответ
Я новичок в R, так что простите мое невежество. Итак, я создал базовую горизонтальную гистограмму, используяbarplot( ). Мой текст на оси Y в основном исчезает. [IMG_OUR_ID=67.png] [IMG_OUR_ID=68.png] Есть ли способ удлинить и отобразить текст? Это мой код:barplot(mydata$`Sum Impact Risk`~mydata$`Eco...
- Почему case_when () вычисляет ложное условие?3 ответ
У меня есть data.frame с групповой переменной и целочисленной переменной с отсутствующими данными.df<-data.frame(group=c(1,1,2,2,3,3),a=as.integer(c(1,2,NA,NA,1,NA))) Я хочу вычислить максимально доступное значение переменнойa внутри каждой группы: в моем примере я должен получить 2 для группы 1...
- Удаление повторяющихся строк на основе самой последней записи3 ответ
Приношу свои извинения, поскольку я знаю, что варианты этого вопроса задавались раньше, но я попытался найти предоставленные ответы и не смог применить их к своим данным. У меня есть следующие данные по результатам опроса. Несколько человек прошли опросы для участия в различных вебинарах. Человек мо...
- Объедините набор переменных списка в фрейм данных, используя список, который ссылается на эти переменные списка1 ответ
Это было сложно сформулировать, поэтому создали пример. У меня есть набор переменных списка, которые я хотел бы объединить в фрейм данных. Это примеры списковa <- 2:7 b <- 9:14 c <- 25:30 d <- 31:36 У меня есть список, в котором есть элементы, которые ссылаются на приведенные выше имен...
- Я хочу добавить процент подсчета для каждой категории в качестве метки на мою круговую диаграмму ggplot1 ответ
Я использую код:age_pie_chart <- ggplot(data = data , aes(x = "", fill = `Q1: How old are you?`))+ geom_bar(position = "fill", width = 1) + coord_polar(theta = "y") + xlab("") + ylab("") + blank_theme + scale_fill_grey()+ theme(axis.text.x...
- Как вернуться к старому фрейму данных при удалении вновь добавленного столбца?1 ответ
Проблема Я объединил большое количество наборов данных в один фрейм данных. Затем я хочу использовать этот большой фрейм данных во всем своем коде. Однако иногда я хочу использовать только один из более мелких ранних наборов данных, но делать это с моим существующим большим фреймом данных. Я чувству...

"Завидую тестировщикам: все хотят с ними дружить."
deep copy
, просто используйтеcopy(dt)
. Peace Wang<<-
в этой ситуации, которая постепенно выполняет поиск в родительских средах до тех пор, пока не достигнет глобального уровня, где он что-то создаст. Это несовершенно, но может удовлетворить ваши потребности. Danielle McCool