Como excluir uma linha por referência em data.table?


150

Minha pergunta está relacionada à atribuição por referência versus cópia data.table. Quero saber se é possível excluir linhas por referência, semelhante a

DT[ , someCol := NULL]

Eu quero saber sobre

DT[someRow := NULL, ]

Eu acho que há uma boa razão para que essa função não exista, então talvez você possa apenas apontar uma boa alternativa para a abordagem de cópia usual, como abaixo. Em particular, indo com o meu favorito do exemplo (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

Digamos que eu queira excluir a primeira linha desta data.table. Eu sei que posso fazer isso:

DT <- DT[-1, ]

mas frequentemente podemos evitar isso, porque estamos copiando o objeto (e isso requer cerca de 3 * N de memória, se N object.size(DT), conforme indicado aqui . Agora eu encontrei set(DT, i, j, value). Eu sei como definir valores específicos (como aqui: definir tudo valores nas linhas 1 e 2 e nas colunas 2 e 3 a zero)

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

Mas como posso apagar as duas primeiras linhas, digamos? Fazendo

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

define o DT inteiro como NULL.

Meu conhecimento de SQL é muito limitado, então vocês me dizem: dado o data.table usa a tecnologia SQL, existe um equivalente ao comando SQL

DELETE FROM table_name
WHERE some_column=some_value

em data.table?


17
Eu não acho que data.table()use a tecnologia SQL tanto quanto se possa traçar um paralelo entre as diferentes operações no SQL e os vários argumentos de a data.table. Para mim, a referência a "tecnologia" implica, de certa forma, que ela data.tableesteja no topo de um banco de dados SQL em algum lugar, o que não é o caso do AFAIK.
Chase

1
obrigado perseguição. Sim, acho que a analogia sql foi um palpite.
Florian Oswald

1
Freqüentemente, deve ser suficiente definir um sinalizador para manter linhas, como DT[ , keep := .I > 1], então, subconjunto para operações posteriores:, DT[(keep), ...]talvez até setindex(DT, keep)a velocidade desse subconjunto. Não é uma panacéia, mas vale a pena considerar como uma opção de design em seu fluxo de trabalho - você realmente deseja excluir todas essas linhas da memória ou prefere excluí-las? A resposta difere por caso de uso.
22917

Respostas:


125

Boa pergunta. data.tableainda não pode excluir linhas por referência.

data.tablepode adicionar e excluir colunas por referência, pois aloca demais o vetor de ponteiros de coluna, como você sabe. O plano é fazer algo semelhante para linhas e permitir rápido inserte delete. Uma exclusão de linha seria usada memmoveem C para mover os itens (em cada coluna) após as linhas excluídas. A exclusão de uma linha no meio da tabela ainda seria bastante ineficiente em comparação com um banco de dados de armazenamento de linhas, como o SQL, que é mais adequado para inserção e exclusão rápidas de linhas onde quer que essas linhas estejam na tabela. Ainda assim, seria muito mais rápido do que copiar um novo objeto grande sem as linhas excluídas.

Por outro lado, como os vetores das colunas seriam superalocados, as linhas poderiam ser inseridas (e excluídas) no final , instantaneamente; por exemplo, uma série temporal crescente.


Está arquivado como um problema: exclua linhas por referência .


1
@ Matthew Dowle Existe alguma notícia sobre isso?
statquant

15
@statquant Acho que devo corrigir os 37 erros e terminar freadprimeiro. Depois disso, é bem alto.
precisa

15
@ MatthewDowle claro, obrigado novamente por tudo o que você está fazendo.
statquant

1
@rbatt Correto. DT[b<8 & a>3]retorna uma nova data.table. Gostaríamos de adicionar delete(DT, b>=8 | a<=3)e DT[b>=8 | a<=8, .ROW:=NULL]. A vantagem deste último seria a combinação com outros recursos [], como números de linhas i, participação ie rollbenefícios da [i,j,by]otimização.
quer

2
@charliealpha Sem atualização. Contribuições são bem-vindas. Estou disposto a orientar. Precisa de habilidades em C - novamente, estou disposto a orientar.
precisa

29

a abordagem adotada para tornar o uso da memória semelhante à exclusão no local é subconjunto de uma coluna por vez e exclusão. não tão rápido quanto uma solução C memmove adequada, mas o uso da memória é o que me interessa aqui. algo assim:

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 Abordagem eficiente de memória agradável. Então, idealmente, precisamos excluir um conjunto de linhas por referência, não é verdade, eu não tinha pensado nisso. Terá que ser uma série de memmoves para preencher as lacunas, mas tudo bem.
precisa saber é o seguinte

Isso funcionaria como uma função ou o uso de uma função e o retorno a força a fazer cópias de memória?
russellpierce

1
funcionaria em uma função, já que data.tables são sempre referências.
vc273

1
obrigado, legal. Para acelerar um pouco (especialmente com muitas colunas) de alterar DT[, col:= NULL, with = F]emset(DT, NULL, col, NULL)
Michele

2
Atualizando à luz da mudança de idioma e aviso "com = FALSE junto com: = foi descontinuado na v1.9.4 lançada em outubro de 2014. Queira envolver o LHS de: = entre parênteses; por exemplo, DT [, (myVar): = sum (b) , by = a] para atribuir ao (s) nome (s) da coluna mantida na variável myVar. Veja? ': =' para outros exemplos. Conforme advertido em 2014, isso agora é um aviso. "
Frank

6

Aqui está uma função de trabalho baseada na resposta de @ vc273 e no feedback de @ 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);
}

E exemplo de seu uso:

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

Onde "dat" é uma tabela de dados. Remover 14k linhas de 1.4M linhas leva 0.25 segundos no meu laptop.

> 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. Como sou novo no SO, não pude adicionar comentários ao tópico do @ vc273 :-(


Comentei na resposta do vc explicando a sintaxe alterada para (col): =. É meio estranho ter uma função chamada "delete", mas um argumento relacionado ao que manter. Btw, geralmente é preferível usar um exemplo reproduzível em vez de mostrar dim para seus próprios dados. Você pode reutilizar a TD da pergunta, por exemplo.
22416 Frank #

Eu não entendo por que você fazê-lo por referência, mas depois de usar uma atribuição dat <-
skan

1
@skan, Essa atribuição atribui "dat" para apontar para a tabela de dados modificada que foi criada por meio da subconjunto da tabela de dados original. A <declaração não copia os dados retornados, apenas atribui um novo nome a eles. link
Jarno P.

@ Frank, eu atualizei a função para a estranheza que você apontou.
Jarno P.

Ok obrigado. Estou deixando o comentário, pois ainda acho que vale a pena notar que mostrar a saída do console em vez de um exemplo reproduzível não é incentivado aqui. Além disso, uma única referência não é tão informativa. Se você também medisse o tempo necessário para o subconjunto, seria mais informativo (já que a maioria de nós não sabe intuitivamente quanto tempo leva, muito menos quanto tempo leva em sua comp). De qualquer forma, não pretendo sugerir que esta seja uma resposta ruim; Eu sou um dos seus defensores.
Frank

4

Em vez disso, ou tentando definir como NULL, tente definir para NA (correspondendo ao tipo de NA da primeira coluna)

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

3
Sim, isso funciona, eu acho. Meu problema é que tenho muitos dados e quero me livrar exatamente dessas linhas com NA, possivelmente sem ter que copiar o DT para me livrar dessas linhas. obrigado pelo seu comentário de qualquer maneira!
Florian Oswald

4

O tópico ainda é interessante para muitas pessoas (inclusive eu).

Que tal? Eu costumava assignsubstituir glovalenvo código descrito anteriormente. Seria melhor capturar o ambiente original, mas pelo menos globalenvnele é eficiente em memória e age como uma alteração por 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)

Só para esclarecer, isso não é excluído por referência (com base em address(DT); delete(DT, 3); address(DT)), embora possa ser eficiente em algum sentido.
Frank

1
Não, não tem. Emula o comportamento e é eficiente na memória. Foi por isso que eu disse: age assim . Mas, estritamente falando, você está certo, o endereço foi alterado.
JRR

3

Aqui estão algumas estratégias que eu usei. Acredito que uma função .ROW possa estar chegando. Nenhuma dessas abordagens abaixo é rápida. Estas são algumas estratégias um pouco além dos subconjuntos ou da filtragem. Eu tentei pensar como dba apenas tentando limpar os dados. Como observado acima, você pode selecionar ou remover linhas em 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

Nota: .SD cria um subconjunto dos dados originais e permite que você trabalhe bastante em j ou na tabela de dados subsequente. Consulte https://stackoverflow.com/a/47406952/305675 . Aqui eu encomendei minhas íris por Comprimento Sepal, pegue um Sepal.Length especificado, no mínimo, selecione os três primeiros (por Comprimento Sepal) de todas as Espécies e retorne todos os dados que os acompanham:

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

As abordagens acima de tudo reordenam uma tabela de dados sequencialmente ao remover linhas. Você pode transpor uma tabela de dados e remover ou substituir as linhas antigas que agora são colunas transpostas. Ao usar ': = NULL' para remover uma linha transposta, o nome da coluna subsequente também é removido:

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

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

Quando você transpõe o data.frame de volta para uma data.table, convém renomear a partir da data.table original e restaurar os atributos da classe no caso de exclusão. A aplicação ": = NULL" a uma tabela de dados agora transposta cria todas as classes de caracteres.

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

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

Você pode apenas querer remover linhas duplicadas que você pode fazer com ou sem uma chave:

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

Também é possível adicionar um contador incremental com '.I'. Você pode procurar chaves ou campos duplicados e removê-los removendo o registro do contador. Isso é computacionalmente caro, mas tem algumas vantagens, pois você pode imprimir as linhas a serem removidas.

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.

Você também pode preencher uma linha com 0s ou NAs e, em seguida, usar uma consulta i para excluí-los:

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

Isso realmente não responde à pergunta (sobre remoção por referência) e o uso tem um data.frame geralmente não é uma boa idéia; verifique str(m_iris)se todos os dados se tornaram string / caractere. Aliás, você também pode obter números de linhas usando d_iris[duplicated(Key), which = TRUE]sem criar uma coluna de contador.
24418 Frank

1
Sim você está certo. Eu não respondo a pergunta especificamente. Porém, a remoção de uma linha por referência ainda não possui funcionalidade ou documentação oficial e muitas pessoas irão a este post procurando funcionalidade genérica para fazer exatamente isso. Poderíamos criar uma postagem para responder apenas à pergunta sobre como remover uma linha. O estouro de pilha é muito útil e eu realmente entendo a necessidade de manter as respostas exatas para a pergunta. Às vezes, porém, acho que o SO pode ser um pouco fascista nesse sentido ... mas talvez haja uma boa razão para isso.
Rferrisx

Ok, obrigado por explicar. Acho que, por enquanto, nossa discussão aqui é suficiente para quem se confunde nesse caso.
24818 Frank
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.