Как удалить строку по ссылке в data.table?

150

Мой вопрос связан с назначением по ссылке, а не копированием в data.table. Я хочу знать, если можно удалить строки по ссылке, аналогично

DT[ , someCol := NULL]

Я хочу знать о

DT[someRow := NULL, ]

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

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
#      x y v
# [1,] a 1 1
# [2,] a 3 2
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Скажем, я хочу удалить первую строку из этого data.table. Я знаю, что могу это сделать:

DT <- DT[-1, ]

но часто мы можем захотеть избежать этого, потому что мы копируем объект (а это требует около 3 * N памяти, если N object.size(DT), как указано здесь . Теперь я обнаружил set(DT, i, j, value). Я знаю, как установить конкретные значения (как здесь: установить все значения в строках 1 и 2 и столбцах 2 и 3 равны нулю)

set(DT, 1:2, 2:3, 0) 
DT
#      x y v
# [1,] a 0 0
# [2,] a 0 0
# [3,] a 6 3
# [4,] b 1 4
# [5,] b 3 5
# [6,] b 6 6
# [7,] c 1 7
# [8,] c 3 8
# [9,] c 6 9

Но как я могу стереть первые два ряда, скажем? дела

set(DT, 1:2, 1:3, NULL)

устанавливает весь DT в NULL.

Мои знания SQL очень ограничены, так что вы, ребята, скажите мне: данный data.table использует технологию SQL, есть ли эквивалент команды SQL

DELETE FROM table_name
WHERE some_column=some_value

в data.table?

Флориан Освальд
источник
17
Я не думаю, что это data.table()использует технологию SQL настолько, насколько можно провести параллель между различными операциями в SQL и различными аргументами a data.table. Для меня ссылка на «технологию» в некоторой степени подразумевает, что она data.tableнаходится где-то на вершине базы данных SQL, что не соответствует AFAIK.
Погоня
1
спасибо погоня да, я думаю, что аналогия с sql была дикой догадкой.
Флориан Освальд
1
Часто должно быть достаточно определить флаг для хранения строк, например DT[ , keep := .I > 1], затем подмножество для последующих операций: DT[(keep), ...]возможно, даже setindex(DT, keep)скорость этого подмножества. Не панацея, но стоит рассмотреть в качестве варианта дизайна в вашем рабочем процессе - вы действительно хотите удалить все эти строки из памяти , или вы бы предпочли исключить их? Ответ зависит от варианта использования.
MichaelChirico

Ответы:

125

Хороший вопрос. data.tableне может удалить строки по ссылке.

data.tableМожно добавлять и удалять столбцы по ссылке, поскольку, как вы знаете, он перераспределяет вектор указателей столбцов. План состоит в том, чтобы сделать что-то подобное для рядов и позволить быстро insertи delete. Удаление строки будет использоваться memmoveв C для составления бюджета элементов (в каждом столбце) после удаленных строк. Удаление строки в середине таблицы будет по-прежнему весьма неэффективным по сравнению с базой данных хранилища строк, такой как SQL, которая больше подходит для быстрой вставки и удаления строк, где бы эти строки не находились в таблице. Но все же, это было бы намного быстрее, чем копировать новый большой объект без удаленных строк.

С другой стороны, поскольку векторы столбцов будут перераспределены, строки могут быть вставлены (и удалены) в конце , немедленно; например, растущий временной ряд.


Это подано как проблема: удалить строки по ссылке .

Мэтт Доул
источник
1
@ Мэтью Доул Есть ли новости по этому поводу?
statquant
15
@statquant Я думаю, что должен исправить 37 ошибок и закончить freadпервым. После этого это довольно высоко.
Мэтт Доул
15
@ MatthewDowle, конечно, еще раз спасибо за все, что вы делаете.
statquant
1
@rbatt Правильно. DT[b<8 & a>3]возвращает новый data.table. Мы хотели бы добавить delete(DT, b>=8 | a<=3)и DT[b>=8 | a<=8, .ROW:=NULL]. Преимущество последнего будет сочетаться с другими функциями, []такими как номера строк i, объединение iи rollизвлечение выгоды от [i,j,by]оптимизации.
Мэтт Доул
2
@charliealpha Нет обновлений. Взносы приветствуются. Я готов вести Для этого нужны навыки C - опять же, я готов вести.
Мэтт
29

подход, который я использовал для того, чтобы использование памяти было похоже на удаление на месте, состоит в том, чтобы подбирать столбец за раз и удалять. не так быстро, как правильное решение C memmove, но использование памяти - это все, что меня волнует. что-то вроде этого:

DT = data.table(col1 = 1:1e6)
cols = paste0('col', 2:100)
for (col in cols){ DT[, (col) := 1:1e6] }
keep.idxs = sample(1e6, 9e5, FALSE) # keep 90% of entries
DT.subset = data.table(col1 = DT[['col1']][keep.idxs]) # this is the subsetted table
for (col in cols){
  DT.subset[, (col) := DT[[col]][keep.idxs]]
  DT[, (col) := NULL] #delete
}
vc273
источник
5
+1 Приятный эффективный подход к памяти. Поэтому в идеале нам нужно удалить набор строк по ссылке, не правда ли, я об этом не думал. Это должно быть серией memmoveсекунд, чтобы сократить расходы, но это нормально.
Мэтт Доул
Будет ли это работать как функция или использование функции и возврата заставит ее делать копии памяти?
Russellpierce
1
это будет работать в функции, поскольку data.tables всегда являются ссылками.
vc273
1
спасибо, хороший Для ускорения немного (особенно с большим количеством столбцов) изменить DT[, col:= NULL, with = F]вset(DT, NULL, col, NULL)
Микеле
2
Обновление в свете изменения идиомы и предупреждения "с = FALSE вместе с: = устарело в v1.9.4, выпущенном в октябре 2014 года. Пожалуйста, заключите LHS в: = в скобки; например, DT [, (myVar): = sum (b) , с помощью = a], чтобы назначить имена столбцов, которые хранятся в переменной myVar. См. другие примеры? ': ='. Как предупреждено в 2014 году, теперь это предупреждение. "
Франк
6

Вот рабочая функция, основанная на ответе @ vc273 и отзывах @ Frank.

delete <- function(DT, del.idxs) {           # pls note 'del.idxs' vs. 'keep.idxs'
  keep.idxs <- setdiff(DT[, .I], del.idxs);  # select row indexes to keep
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs]); # this is the subsetted table
  setnames(DT.subset, cols[1]);
  for (col in cols[2:length(cols)]) {
    DT.subset[, (col) := DT[[col]][keep.idxs]];
    DT[, (col) := NULL];  # delete
  }
   return(DT.subset);
}

И пример его использования:

dat <- delete(dat,del.idxs)   ## Pls note 'del.idxs' instead of 'keep.idxs'

Где «dat» - это таблица данных. На моем ноутбуке удаление 14 тысяч строк из 1,4 миллионов строк занимает 0,25 секунды.

> dim(dat)
[1] 1419393      25
> system.time(dat <- delete(dat,del.idxs))
   user  system elapsed 
   0.23    0.02    0.25 
> dim(dat)
[1] 1404715      25
> 

PS. Так как я новичок в SO, я не могу добавить комментарий в ветку @ vc273 :-(

Ярно П.
источник
Я прокомментировал под ответом vc объяснение измененного синтаксиса для (col): =. Довольно странно иметь функцию с именем «delete», но аргумент, связанный с тем, что оставить. Кстати, как правило, предпочтительнее использовать воспроизводимый пример, а не показывать dim для ваших собственных данных. Вы можете использовать DT из вопроса, например.
Франк
Я не понимаю, почему вы делаете это по ссылке, но позже используйте dat <-
skan
1
@skan, это назначение назначает «dat» для указания на измененный data.table, который сам был создан путем поднабора исходного data.table. <- assingment не делает копию возвращаемых данных, просто присваивает им новое имя. ссылка
Ярно П.
@Frank, я обновил функцию для странности, которую вы указали.
Ярно П.
Хорошо, спасибо. Я оставляю комментарий, так как все еще думаю, что стоит отметить, что показ консольного вывода вместо воспроизводимого примера здесь не рекомендуется. Кроме того, один тест не так информативен. Если бы вы также измерили время, затрачиваемое на подмножество, оно было бы более информативным (поскольку большинство из нас интуитивно не знает, сколько времени это займет, а тем более - сколько времени занимает ваш комп). Во всяком случае, я не хочу сказать, что это плохой ответ; Я один из его продюсеров.
Фрэнк
4

Вместо этого или пытаясь установить значение NULL, попробуйте установить значение NA (соответствует типу NA для первого столбца).

set(DT,1:2, 1:3 ,NA_character_)
IRTFM
источник
3
да, это работает, я думаю. Моя проблема в том, что у меня много данных, и я хочу избавиться именно от этих строк с помощью NA, возможно, без необходимости копировать DT, чтобы избавиться от этих строк. все равно спасибо за ваш комментарий!
Флориан Освальд
4

Тема по-прежнему интересна многим людям (включая меня).

Что об этом? Я использовал assignдля замены glovalenvи код, описанный ранее. Было бы лучше захватить исходную среду, но, по крайней мере, globalenvона эффективна для памяти и действует как изменение по ссылке.

delete <- function(DT, del.idxs) 
{ 
  varname = deparse(substitute(DT))

  keep.idxs <- setdiff(DT[, .I], del.idxs)
  cols = names(DT);
  DT.subset <- data.table(DT[[1]][keep.idxs])
  setnames(DT.subset, cols[1])

  for (col in cols[2:length(cols)]) 
  {
    DT.subset[, (col) := DT[[col]][keep.idxs]]
    DT[, (col) := NULL];  # delete
  }

  assign(varname, DT.subset, envir = globalenv())
  return(invisible())
}

DT = data.table(x = rep(c("a", "b", "c"), each = 3), y = c(1, 3, 6), v = 1:9)
delete(DT, 3)
Дж.Р.Р.
источник
Просто чтобы быть ясно, это не удаляет по ссылке (на основе address(DT); delete(DT, 3); address(DT)), хотя это может быть эффективным в некотором смысле.
Фрэнк
1
Нет. Он эмулирует поведение и эффективно использует память. Вот почему я сказал: это действует как . Но, строго говоря, вы правы, адрес изменился.
JRR
3

Вот несколько стратегий, которые я использовал. Я считаю, что функция .ROW может появиться. Ни один из этих подходов ниже не является быстрым. Это некоторые стратегии, которые выходят за рамки подмножеств или фильтрации. Я пытался думать как dba, просто пытаясь очистить данные. Как отмечено выше, вы можете выбрать или удалить строки в data.table:

data(iris)
iris <- data.table(iris)

iris[3] # Select row three

iris[-3] # Remove row three

You can also use .SD to select or remove rows:

iris[,.SD[3]] # Select row three

iris[,.SD[3:6],by=,.(Species)] # Select row 3 - 6 for each Species

iris[,.SD[-3]] # Remove row three

iris[,.SD[-3:-6],by=,.(Species)] # Remove row 3 - 6 for each Species

Примечание: .SD создает подмножество исходных данных и позволяет вам проделать немалую работу в j или последующих data.table. См. Https://stackoverflow.com/a/47406952/305675 . Здесь я упорядочил свои ирисы по длине Sepal, взял как минимум заданную длину Sepal.Length, выбрал три первых (по длине Sepal) всех видов и вернул все сопутствующие данные:

iris[order(-Sepal.Length)][Sepal.Length > 3,.SD[1:3],by=,.(Species)]

Подходы прежде всего переупорядочивают таблицу данных последовательно при удалении строк. Вы можете транспонировать таблицу data.table и удалять или заменять старые строки, которые теперь являются транспонированными столбцами. При использовании ': = NULL' для удаления транспонированной строки также удаляется имя следующего столбца:

m_iris <- data.table(t(iris))[,V3:=NULL] # V3 column removed

d_iris <- data.table(t(iris))[,V3:=V2] # V3 column replaced with V2

Когда вы перемещаете data.frame обратно в data.table, вы можете захотеть переименовать исходный data.table и восстановить атрибуты класса в случае удаления. Применение ": = NULL" к теперь транспонированному data.table создает все классы символов.

m_iris <- data.table(t(d_iris));
setnames(d_iris,names(iris))

d_iris <- data.table(t(m_iris));
setnames(m_iris,names(iris))

Вы можете просто удалить дубликаты строк, которые вы можете сделать с ключом или без него:

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]     

d_iris[!duplicated(Key),]

d_iris[!duplicated(paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)),]  

Также можно добавить инкрементный счетчик с помощью «.I». Затем вы можете найти дублированные ключи или поля и удалить их, удалив запись со счетчиком. Это вычислительно дорого, но имеет некоторые преимущества, так как вы можете напечатать строки, которые нужно удалить.

d_iris[,I:=.I,] # add a counter field

d_iris[,Key:=paste0(Sepal.Length,Sepal.Width,Petal.Length,Petal.Width,Species)]

for(i in d_iris[duplicated(Key),I]) {print(i)} # See lines with duplicated Key or Field

for(i in d_iris[duplicated(Key),I]) {d_iris <- d_iris[!I == i,]} # Remove lines with duplicated Key or any particular field.

Вы также можете просто заполнить строку 0 или NA, а затем использовать запрос i, чтобы удалить их:

 X 
   x v foo
1: c 8   4
2: b 7   2

X[1] <- c(0)

X
   x v foo
1: 0 0   0
2: b 7   2

X[2] <- c(NA)
X
    x  v foo
1:  0  0   0
2: NA NA  NA

X <- X[x != 0,]
X <- X[!is.na(x),]
rferrisx
источник
Это на самом деле не отвечает на вопрос (об удалении по ссылке), и использование tна data.frame обычно не очень хорошая идея; проверьте, str(m_iris)что все данные стали строкой / символом. Кстати, вы также можете получить номера строк с помощью d_iris[duplicated(Key), which = TRUE]без создания счетчика столбца.
Фрэнк
1
Да ты прав. Я не отвечаю на вопрос конкретно. Но удаление строки по ссылке еще не имеет официальной функциональности или документации, и многие люди собираются прийти к этому сообщению в поисках универсальной функциональности, чтобы сделать именно это. Мы могли бы создать пост, чтобы просто ответить на вопрос о том, как удалить строку. Переполнение стека очень полезно, и я действительно понимаю необходимость держать ответы точными на вопрос. Хотя иногда я думаю, что ТАК может быть немного фашистом в этом отношении ... но, возможно, есть веская причина для этого.
rferrisx
Хорошо, спасибо за объяснение. Я думаю, что сейчас наша дискуссия здесь является достаточным указателем для тех, кто запутался в этом деле.
Фрэнк