如何通过引用删除data.table中的行?


150

我的问题与通过引用进行分配而不是在中进行复制有关data.table。我想知道是否可以通过引用删除行,类似于

DT[ , someCol := NULL]

我想知道

DT[someRow := NULL, ]

我猜这是为什么没有此功能的充分原因,因此也许您可以指出一个替代常规复制方法的好方法,如下所示。特别要注意的是,我喜欢example(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的不同操作与data.table。对我而言,对“技术”的引用在某种程度上意味着它data.table位于某个地方的SQL数据库的顶部,而AFAIK并非如此。
大通

1
谢谢追。是的,我想SQL类比是一个疯狂的猜测。
弗洛里安·奥斯瓦尔德

1
通常,定义一个标记来保留行就足够了,例如DT[ , keep := .I > 1],然后是子集以用于以后的操作:DT[(keep), ...]甚至setindex(DT, keep)该子集的速度。不是万能的,但值得在工作流程中考虑作为设计选择-您是否真的要从内存中删除所有这些行,还是希望排除它们?答案因用例而异。
MichaelChirico

Answers:


125

好问题。data.table尚无法通过引用删除行。

data.table您可以通过引用添加和删​​除,因为它过度分配了列指针的向量。该计划是对行执行类似的操作,并允许快速insertdelete。行删除将memmove在C语言中使用,以使被删除的行之后的项目(每一列中的所有内容)堆积起来。与行存储数据库(例如SQL)相比,删除表中间的行仍然非常低效,而SQL更适合于在表中这些行的任何位置快速插入和删除行。但是,这比复制没有删除行的新大对象要快得多。

另一方面,由于列向量将被过度分配,因此可以立即在end处插入(和删除)行;例如,时间序列不断增长。


这是一个问题:按引用删除行


1
@Matthew Dowle有什么新闻吗?
statquant

15
@statquant我想我应该修复37个bug,然后fread先完成。在那之后,它相当高。
Matt Dowle

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]。后者的优势将与[]诸如行号in i,join in iroll受益于[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
}

5
+1不错的内存有效方法。因此,理想情况下,我们需要通过引用删除一组行,实际上我们不是,我没有想到这一点。要memmove弥补差距,必须要有一系列s,但是没关系。
Matt Dowle 2014年

这是否可以作为函数使用,还是在函数中使用并返回强制它进行内存复制?
russellpierce 2014年

1
它可以在函数中工作,因为data.tables始终是引用。
vc273 2014年

1
谢谢,很好。要加快一点点(尤其是多列),你改变DT[, col:= NULL, with = F]set(DT, NULL, col, NULL)
米歇尔

2
2014年10月发布的v1.9.4中已弃用了更新的习惯用法,并警告“ with = FALSE和:=一起使用。请用括号将:=的LHS括起来;例如DT [,(myVar):= sum(b) ,by = 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”是data.table。在我的笔记本电脑上,从1.4M行中删除14k行需要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”的函数但与保留内容有关的arg有点奇怪。顺便说一句,通常最好使用可复制的示例,而不要对自己的数据显示暗淡的颜色。例如,您可以从问题中重用DT。
弗兰克(Frank)

我不明白您为什么要参考引用,但后来却使用dat <
skan

1
@skan,该分配将“ dat”分配给指向本身已通过子集原始data.table创建的修改后的data.table。<-评估不会复制返回数据,只是为其分配新名称。 链接
Jarno P.

@Frank,我已经更新了您指出的奇怪功能。
Jarno P.

好,谢谢。我留下评论,因为我仍然认为值得注意的是,此处不鼓励显示控制台输出而不是可复制的示例。此外,一个基准测试也不是那么有用。如果您还测量了子集所花费的时间,它会提供更多信息(因为我们大多数人不直观地知道要花多长时间,更不用说花多长时间了)。无论如何,我并不是要暗示这是一个错误的答案。我是它的支持者之一。
弗兰克,

4

相反,或尝试将其设置为NULL,请尝试将其设置为NA(与第一列的NA类型匹配)

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

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),尽管从某种意义上讲它可能是有效的,但它并没有被引用删除(基于)。
法兰克(Frank)

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.Length为最小值,选择所有物种的前三名(按“隔片长度”)并返回所有伴随数据:

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

最重要的是,删除行时,这些方法会按顺序对data.table进行重新排序。您可以转置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),]

这并不能真正回答问题(关于通过引用删除),并且t在data.frame上使用通常不是一个好主意;检查str(m_iris)以确保所有数据都已成为字符串/字符。顺便说一句,您也可以通过使用d_iris[duplicated(Key), which = TRUE]而无需创建计数器列来获取行号。
弗兰克

1
是的,你是对的。我没有具体回答这个问题。但是,通过引用删除行还没有官方功能或文档,因此许多人会来此帖子,寻求通用功能来做到这一点。我们可以创建一个帖子来回答有关如何删除行的问题。堆栈溢出非常有用,我非常理解保持准确回答问题的必要性。不过有时,我认为SO在这方面可能只是一个小法西斯主义者...但是也许有充分的理由。
rferrisx

好的,谢谢您的解释。我认为到目前为止,对于在这种情况下感到困惑的任何人,我们在这里的讨论已经足够一个路标。
弗兰克,
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.