r语言 - 如何在 data.table 中通过引用删除行



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

DT[ , someCol := NULL]

我想知道

DT[someRow := NULL, ]

我想为什么这个函数不存在是有充分理由的,所以也许你可以指出一个通常的复制方法的好替代方案,如下所示。特别是,从例子(数据表)中加入我最喜欢的,

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

假设我想从此数据表中删除第一行。我知道我可以这样做:

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还不能按引用删除行。

如您所知,data.table可以通过引用添加和删除,因为它过度分配了列指针的向量。计划是对行执行类似操作,并允许 快速insertdelete 。行删除将使用 C 中的memmove来在删除的行之后移动项目(在每一列中)。与行存储数据库(如 SQL)相比,删除表中间的行仍然效率低下,SQL 更适合快速插入和删除表中任何行的行。但是,它仍然比复制没有已删除行的新大对象要快得多。

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


它作为一个问题归档:按引用删除行。

为了使内存使用类似于就地删除,我采取的方法是一次对列进行子集并删除。 不如适当的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的回答和@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"是一个数据表。在我的笔记本电脑上,从 14M 行中删除 1.4k 行需要 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
> 

附言。由于我是 SO 的新手,我无法向@vc273的线程添加评论:-(

这个话题仍然很有趣很多人(包括我)。

那怎么办?我使用 assign 来替换前面描述的glovalenv和代码。最好捕获原始环境,但至少在globalenv它是内存效率的,并且就像通过 ref 进行更改一样。

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)

而不是或尝试设置为 NULL,请尝试设置为 NA(匹配第一列的 NA 类型)

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

以下是我使用过的一些策略。我相信一个.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。在这里,我按萼片长度对虹膜进行排序,以指定的萼片长度为最小值,选择所有物种的前三名(按萼片长度)并返回所有随附数据:

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),]

这个版本受 vc273 和 user7114184 版本的启发。当我们想要删除"按引用"时,我们不需要为此创建新的DT。但这实际上不是必需的:如果我们从数据表中删除所有列,它将成为一个空数据表,这将允许任意数量的行。因此,与其将列移动到新的数据表并继续,我们实际上可以将列移回原始数据表,并继续使用它。

这给了我们两个函数,一个data_table_add_rows允许我们将"按引用"的其他行添加到 data.table 中。另一个data_table_remove_rows删除"按引用"的行。第一个获取值列表,而第二个将评估用于过滤的 DT 调用,这使我们能够做一些好事。

#' Add rows to a data table in a memory efficient, by-referencesque manner
#'
#' This mimics the by-reference functionality `DT[, new_col := value]`, but
#' for rows instead. The rows in question are assigned at the end of the data
#' table. If the data table is keyed it is automatically reordered after the
#' operation. If not this function will preserve order of existing rows, but
#' will not preserve sortedness.
#'
#' This function will take the rows to add from a list of columns or generally
#' anything that can be named and converted or coerced to data frame.
#' The list may specify less columns than present in the data table. In this
#' case the rest is filled with NA. The list may not specify more columns than
#' present in the data table. Columns are matched by names if the list is named
#' or by position if not. The list may not have names not present in the data
#' table.
#'
#' Note that this operation is memory efficient as it will add the rows for
#' one column at a time, only requiring reallocation of single columns at a
#' time. This function will change the original data table by reference.
#'
#' This function will not affect shallow copies of the data table.
#'
#' @param .dt A data table
#' @param value A list (or a data frame). Must have at most as many elements as
#'        there are columns in param{.dt}. If unnamed this will be applied to
#'        first columns in param{.dt}, else it will by applied by name. Must
#'        not have names not present in param{.dt}.
#' @return param{.dt} (invisible)
data_table_add_rows <- function(.dt, value) {
  if (length(value) > ncol(.dt)) {
    rlang::abort(glue::glue("Trying to update data table with {ncol(.dt)
      } columns with {length(value)} columns."))
  }
  if (is.null(names(value))) names(value) <- names(.dt)[seq_len(length(value))]
  value <- as.data.frame(value)
  if (any(!(names(value) %in% names(.dt)))) {
    rlang::abort(glue::glue("Trying to update data table with columns {
        paste(setdiff(names(value), names(.dt)), collapse = ', ')
      } not present in original data table."))
  }
  value[setdiff(names(.dt), names(value))] <- NA
  
  k <- data.table::key(.dt)
  
  temp_dt <- data.table::data.table()
  
  for (col in c(names(.dt))) {
    set(temp_dt, j = col,value = c(.dt[[col]], value[[col]]))
    set(.dt, j = col, value = NULL)
  }
  
  for (col in c(names(temp_dt))) {
    set(.dt, j = col, value = temp_dt[[col]])
    set(temp_dt, j = col, value = NULL)
  }
  
  if (!is.null(k)) data.table::setkeyv(.dt, k)
  
  .dt
}
#' Remove rows from a data table in a memory efficient, by-referencesque manner
#'
#' This mimics the by-reference functionality `DT[, new_col := NULL]`, but
#' for rows instead. This operation preserves order. If the data table is keyed
#' it will preserve the key.
#'
#' This function will determine the rows to delete by passing all additional
#' arguments to a data.table filter call of the form
#' code{DT[, .idx = .I][..., j = .idx]}
#' Thus we can pass a simple index vector or a condition, or even delete by
#' using join syntax code{data_table_remove_rows(DT1, DT2, on = cols)} (or
#' reversely keep by join using
#' code{data_table_remove_rows(DT1, !DT2, on = cols)}
#'
#' Note that this operation is memory efficient as it will add the rows for
#' one column at a time, only requiring reallocation of single columns at a
#' time. This function will change the original data table by reference.
#'
#' This function will not affect shallow copies of the data table.
#'
#' @param .dt A data table
#' @param ... Any arguments passed to `[` for filtering the data.table. Must not
#'        specify `j`.
#' @return param{.dt} (invisible)
data_table_remove_rows <- function(.dt, ...) {
  k <- data.table::key(.dt)
  
  env <- parent.frame()
  args <- as.list(sys.call()[-1])
  if (!is.null(names(args)) && ".dt" %in% names(args)) args[.dt] <- NULL
  else args <- args[-1]
  
  if (!is.null(names(args)) && "j" %in% names(args)) {
    rlang::abort("... must not specify j")
  }
  
  call <- substitute(
    .dt[, .idx := .I][j = .idx],
    env = list(.dt = .dt))
  
  .nc <- names(call)
  
  for (i in seq_along(args)) {
    call[[i + 3]] <- args[[i]]
  }
  
  if (!is.null(names(args))) names(call) <- c(.nc, names(args))
  which <- eval(call, envir = env)
  set(.dt, j = ".idx", value = NULL)
  
  temp_dt <- data.table::data.table()
  
  for (col in c(names(.dt))) {
    set(temp_dt, j = col,value = .dt[[col]][-which])
    set(.dt, j = col, value = NULL)
  }
  
  for (col in c(names(temp_dt))) {
    set(.dt,j = col, value = temp_dt[[col]])
    set(temp_dt, j = col, value = NULL)
  }
  
  if (!is.null(k)) data.table::setattr(.dt, "sorted", k)
  
  .dt
}

现在这使我们能够做相当不错的通话。例如,我们可以做:

library(data.table)
d <- data.table(x = 1:10, y = runif(10))
#>         x          y
#>     <int>      <num>
#>  1:     1 0.77326131
#>  2:     2 0.88699627
#>  3:     3 0.15553784
#>  4:     4 0.71221778
#>  5:     5 0.11964578
#>  6:     6 0.73692709
#>  7:     7 0.05382835
#>  8:     8 0.61129007
#>  9:     9 0.18292229
#> 10:    10 0.22569555
# add some rows (y = NA)
data_table_add_rows(d, list(x=11:13))
# add some rows (y = 0)
data_table_add_rows(d, list(x=14:15, y = 0))
#>         x          y
#>     <int>      <num>
#>  1:     1 0.77326131
#>  2:     2 0.88699627
#>  3:     3 0.15553784
#>  4:     4 0.71221778
#>  5:     5 0.11964578
#>  6:     6 0.73692709
#>  7:     7 0.05382835
#>  8:     8 0.61129007
#>  9:     9 0.18292229
#> 10:    10 0.22569555
#> 11:    11         NA
#> 12:    12         NA
#> 13:    13         NA
#> 14:    14 0.00000000
#> 15:    15 0.00000000
# remove all added rows
data_table_remove_rows(d, is.na(y) | y == 0)
#>         x          y
#>     <int>      <num>
#>  1:     1 0.77326131
#>  2:     2 0.88699627
#>  3:     3 0.15553784
#>  4:     4 0.71221778
#>  5:     5 0.11964578
#>  6:     6 0.73692709
#>  7:     7 0.05382835
#>  8:     8 0.61129007
#>  9:     9 0.18292229
#> 10:    10 0.22569555
# remove by join
e <- data.table(x = 2:5)
data_table_remove_rows(d, e, on = "x")
#>        x          y
#>    <int>      <num>
#> 1:     1 0.77326131
#> 2:     6 0.73692709
#> 3:     7 0.05382835
#> 4:     8 0.61129007
#> 5:     9 0.18292229
#> 6:    10 0.22569555
# add back
data_table_add_rows(d, c(e, list(y = runif(nrow(e)))))
#>         x          y
#>     <int>      <num>
#>  1:     1 0.77326131
#>  2:     6 0.73692709
#>  3:     7 0.05382835
#>  4:     8 0.61129007
#>  5:     9 0.18292229
#>  6:    10 0.22569555
#>  7:     2 0.99372144
#>  8:     3 0.03363720
#>  9:     4 0.69880083
#> 10:     5 0.67863547
# keep by join
data_table_remove_rows(d, !e, on = "x")
#>        x         y
#>    <int>     <num>
#> 1:     2 0.9937214
#> 2:     3 0.0336372
#> 3:     4 0.6988008
#> 4:     5 0.6786355

编辑:感谢Matt Summersgill,我提供了一个性能稍好的版本!

最新更新