Come eliminare una riga per riferimento in data.table?


150

La mia domanda è relativa all'assegnazione per riferimento rispetto alla copia in data.table. Voglio sapere se uno può eliminare le righe per riferimento, simile a

DT[ , someCol := NULL]

Voglio sapere

DT[someRow := NULL, ]

Immagino che ci sia una buona ragione per cui questa funzione non esiste, quindi forse potresti semplicemente indicare una buona alternativa al solito approccio alla copia, come di seguito. In particolare, andando con il mio preferito dall'esempio (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

Di 'che voglio eliminare la prima riga da questo data.table. So che posso fare questo:

DT <- DT[-1, ]

ma spesso potremmo volerlo evitare, perché stiamo copiando l'oggetto (e ciò richiede circa 3 * N di memoria, se N object.size(DT), come indicato qui . Ora ho trovato set(DT, i, j, value). So come impostare valori specifici (come qui: imposta tutto valori nelle righe 1 e 2 e colonne 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

Ma come posso cancellare le prime due righe, diciamo? fare

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

imposta l'intero DT su NULL.

La mia conoscenza di SQL è molto limitata, quindi voi ragazzi mi dite: dato data.table utilizza la tecnologia SQL, esiste un equivalente al comando SQL

DELETE FROM table_name
WHERE some_column=some_value

in data.table?


17
Non credo sia che data.table()usi la tecnologia SQL tanto quanto si può tracciare un parallelo tra le diverse operazioni in SQL e i vari argomenti a data.table. Per me, il riferimento alla "tecnologia" implica in qualche modo che data.tablesi trovi sopra un database SQL da qualche parte, il che non è il caso di AFAIK.
Insegui

1
grazie inseguimento. sì, immagino che l'analogia sql fosse un'ipotesi selvaggia.
Florian Oswald,

1
Spesso dovrebbe essere sufficiente definire un flag per mantenere le righe, come DT[ , keep := .I > 1], quindi, il sottoinsieme per le operazioni successive:, DT[(keep), ...]forse anche setindex(DT, keep)la velocità di questo sottoinsieme. Non è una panacea, ma vale la pena considerarla come una scelta progettuale nel tuo flusso di lavoro: vuoi davvero eliminare tutte quelle righe dalla memoria o preferiresti escluderle? La risposta varia in base al caso d'uso.
MichaelChirico,

Risposte:


125

Buona domanda. data.tablenon è ancora possibile eliminare le righe per riferimento.

data.tablepuò aggiungere ed eliminare colonne per riferimento poiché alloca in modo eccessivo il vettore dei puntatori di colonna, come sai. Il piano è fare qualcosa di simile per le file e consentire velocemente inserte delete. Un'eliminazione riga dovrebbe essere utilizzata memmovein C per spostare gli elementi (in ogni colonna) dopo le righe eliminate. L'eliminazione di una riga al centro della tabella sarebbe comunque piuttosto inefficiente rispetto a un database dell'archivio di righe come SQL, che è più adatto all'inserimento e all'eliminazione rapidi delle righe ovunque tali righe siano nella tabella. Tuttavia, sarebbe molto più veloce della copia di un nuovo oggetto di grandi dimensioni senza le righe eliminate.

D'altra parte, poiché i vettori di colonna sarebbero sovrallocati, le righe potrebbero essere inserite (ed eliminate) alla fine , istantaneamente; ad esempio, una serie storica in crescita.


È stato archiviato come problema: elimina le righe per riferimento .


1
@Matthew Dowle Ci sono novità su questo?
statquant

15
@statquant Penso che dovrei correggere i 37 bug e finire freadprima. Dopo di che è piuttosto alto.
Matt Dowle,

15
@MatthewDowle certo, grazie ancora per tutto quello che stai facendo.
statquant

1
@rbatt Correct. DT[b<8 & a>3]restituisce un nuovo data.table. Vorremmo aggiungere delete(DT, b>=8 | a<=3)e DT[b>=8 | a<=8, .ROW:=NULL]. Il vantaggio di quest'ultimo sarebbe combinarsi con altre caratteristiche []come numeri di riga i, unirsi ie rolltrarre vantaggio [i,j,by]dall'ottimizzazione.
Matt Dowle,

2
@charliealpha Nessun aggiornamento. Contributi benvenuti. Sono disposto a guidare. Ha bisogno di abilità C - di nuovo, sono disposto a guidare.
Matt Dowle,

29

l'approccio che ho adottato al fine di rendere l'utilizzo della memoria simile alla cancellazione sul posto è quello di sottoinsieme di una colonna alla volta ed eliminazione. non veloce come una corretta soluzione memmove in C, ma l'uso della memoria è tutto ciò che mi interessa qui. qualcosa come questo:

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 Approccio efficiente alla memoria. Quindi idealmente dobbiamo eliminare un set di righe per riferimento, in realtà no, non ci avevo pensato. Dovrà essere una serie di memmoves per colmare le lacune, ma va bene.
Matt Dowle,

Funzionerebbe come una funzione o l'uso in una funzione e il ritorno la forzerebbero a fare copie di memoria?
Russellpierce,

1
funzionerebbe in una funzione, dato che data.tables sono sempre riferimenti.
vc273,

1
grazie bello. Per accelerare un po '(specialmente con molte colonne) cambi DT[, col:= NULL, with = F]inset(DT, NULL, col, NULL)
Michele

2
Aggiornamento alla luce del cambiamento di linguaggio e avvertimento "con = FALSO insieme a: = è stato deprecato nella v1.9.4 rilasciata nell'ottobre 2014. Avvolgere l'LHS di: = tra parentesi; ad esempio, DT [, (myVar): = sum (b) , da = a] da assegnare ai nomi di colonna contenuti nella variabile myVar. Vedi? ': =' per altri esempi. Come avvertito nel 2014, questo è ora un avvertimento. "
Frank,

6

Ecco una funzione funzionante basata sulla risposta di @ vc273 e sul feedback di @ 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);
}

Ed esempio del suo utilizzo:

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

Dove "dat" è un data.table. La rimozione di 14.000 righe da 1,4 milioni di righe richiede 0,25 secondi sul mio 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. Dato che sono nuovo di SO, non ho potuto aggiungere commenti alla discussione di @ vc273 :-(


Ho commentato sotto la risposta di vc spiegando la sintassi modificata per (col): =. È strano avere una funzione chiamata "delete" ma un argomento relativo a cosa mantenere. A proposito, generalmente si preferisce usare un esempio riproducibile piuttosto che mostrare fioco per i propri dati. Ad esempio, potresti riutilizzare DT dalla domanda.
Frank,

Non capisco perché lo fai per riferimento ma in seguito usi un incarico dat <-
skan

1
@skan, quell'assegnazione assegna "dat" in modo che punti alla data.table modificata che è stata creata sottosopra la data.table originale. La <- assingment non copia i dati di ritorno, ma assegna semplicemente un nuovo nome per esso. link
Jarno P.

@Frank, ho aggiornato la funzione per la stranezza che hai sottolineato.
Jarno P.

Ok grazie. Lascio il commento poiché penso ancora che valga la pena notare che mostrare l'output della console anziché un esempio riproducibile non è incoraggiato qui. Inoltre, un singolo benchmark non è così informativo. Se misurassi anche il tempo impiegato per il sottoinsieme, sarebbe più informativo (dal momento che la maggior parte di noi non sa intuitivamente quanto tempo impiega, tanto meno quanto tempo impiega il tuo comp). Comunque, non intendo suggerire che questa sia una cattiva risposta; Sono uno dei suoi sostenitori.
Frank

4

Invece o cercando di impostare su NULL, prova a impostare su NA (facendo corrispondere il tipo NA per la prima colonna)

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

3
sì, credo che funzioni. Il mio problema è che ho molti dati e voglio eliminare esattamente quelle righe con NA, possibilmente senza dover copiare DT per sbarazzarmi di quelle righe. grazie comunque per il tuo commento!
Florian Oswald,

4

L'argomento è ancora interessante per molte persone (me incluso).

Che ne dici? Ho usato assignper sostituire il glovalenve il codice descritto in precedenza. Sarebbe meglio catturare l'ambiente originale ma almeno in globalenvesso è efficiente la memoria e si comporta come un cambiamento da 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)

Giusto per essere chiari, questo non viene eliminato per riferimento (basato su address(DT); delete(DT, 3); address(DT)), sebbene possa essere efficace in un certo senso.
Frank,

1
No non lo fa. Emula il comportamento ed è efficiente in termini di memoria. Ecco perché ho detto: si comporta come . Ma a rigor di termini hai ragione l'indirizzo è cambiato.
JRR,

3

Ecco alcune strategie che ho usato. Credo che potrebbe arrivare una funzione .ROW. Nessuno di questi approcci di seguito è veloce. Queste sono alcune strategie un po 'oltre i sottoinsiemi o i filtri. Ho provato a pensare come dba solo cercando di ripulire i dati. Come notato sopra, è possibile selezionare o rimuovere le righe in 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 crea un sottoinsieme dei dati originali e consente di fare un bel po 'di lavoro in j o data.table successivo. Vedi https://stackoverflow.com/a/47406952/305675 . Qui ho ordinato le mie iridi per Sepal Length, prendere una Sepal.Length minima, selezionare i primi tre (per Sepal Length) di tutte le specie e restituire tutti i dati di accompagnamento:

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

Gli approcci soprattutto riordinano un data.table in sequenza quando si rimuovono le righe. È possibile trasporre un file data.table e rimuovere o sostituire le vecchie righe che ora sono colonne trasposte. Quando si utilizza ': = NULL' per rimuovere una riga trasposta, viene rimosso anche il nome della colonna successiva:

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 si traspone data.frame su data.table, è possibile che si desideri rinominare da data.table originale e ripristinare gli attributi della classe in caso di eliminazione. Applicando ": = NULL" a un data.table ora trasposto si creano tutte le classi di caratteri.

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

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

Potresti semplicemente voler rimuovere le righe duplicate che puoi fare con o senza una chiave:

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

È anche possibile aggiungere un contatore incrementale con '.I'. È quindi possibile cercare chiavi o campi duplicati e rimuoverli rimuovendo il record con il contatore. Questo è computazionalmente costoso, ma presenta alcuni vantaggi poiché è possibile stampare le linee da rimuovere.

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.

Puoi anche semplicemente riempire una riga con 0 o NA e quindi utilizzare una query i per eliminarli:

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

Questo in realtà non risponde alla domanda (sulla rimozione per riferimento) e l'utilizzo tsu un data.frame di solito non è una buona idea; controlla str(m_iris)che tutti i dati siano diventati stringa / carattere. A proposito, puoi anche ottenere i numeri di riga usando d_iris[duplicated(Key), which = TRUE]senza creare una colonna del contatore.
Frank,

1
Si hai ragione. Non rispondo alla domanda in modo specifico. Ma rimuovere una riga per riferimento non ha ancora funzionalità o documentazione ufficiali e molte persone verranno a questo post alla ricerca di funzionalità generiche per fare esattamente questo. Potremmo creare un post per rispondere alla domanda su come rimuovere una riga. Stack Overflow è molto utile e capisco davvero la necessità di mantenere le risposte esatte alla domanda. A volte, però, penso che SO possa essere solo un po 'fascista in questo senso ... ma forse c'è una buona ragione per questo.
rferrisx,

Ok, grazie per aver spiegato. Penso che per ora la nostra discussione qui sia abbastanza di un cartello per chiunque si confonda in questo caso.
Frank
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.