2014-09-20 3 views
8

La funzione set o l'espressione := all'interno di [.data.table significa che data.table viene aggiornato per riferimento. Quello che non capisco molto bene è come questo comportamento differisca dalla riassegnazione del risultato di un'operazione al data.frame originale."aggiornamento per riferimento" vs copia superficiale

keepcols<-function(DF,cols){ 
    eval.parent(substitute(DF<-DF[,cols,with=FALSE])) 
} 
keeprows<-function(DF,i){ 
    eval.parent(substitute(DF<-DF[i,])) 
} 

Perché il RHS nell'espressione <- è una copia dei riferimenti dataframe iniziale, nelle ultime versioni di R, queste funzioni sembrano abbastanza efficiente. In che modo questo metodo di base R è diverso dall'equivalente data.table? La differenza riguarda solo la velocità o anche l'utilizzo della memoria? Quando la differenza è più importante?

Alcuni parametri (di velocità). Sembra che la differenza di velocità sia trascurabile quando il set di dati ha solo due variabili e si ingrandisce con più variabili.

library(data.table) 

# Long dataset 
N=1e7; K=100 
DT <- data.table(
    id1 = sample(sprintf("id%03d",1:K), N, TRUE),  
    v1 = sample(5, N, TRUE)           
) 
system.time(DT[,a_inplace:=mean(v1)]) 
user system elapsed 
0.060 0.013 0.077 
system.time(DT[,a_inplace:=NULL]) 
user system elapsed 
0.044 0.010 0.060 


system.time(DT <- DT[,c(.SD,a_usual=mean(v1)),.SDcols=names(DT)]) 
user system elapsed 
0.132 0.025 0.161 
system.time(DT <- DT[,list(id1,v1)]) 
user system elapsed 
0.124 0.026 0.153 


# Wide dataset 
N=1e7; K=100 
DT <- data.table(
    id1 = sample(sprintf("id%03d",1:K), N, TRUE),  
    id2 = sample(sprintf("id%03d",1:K), N, TRUE),  
    id3 = sample(sprintf("id%010d",1:(N/K)), N, TRUE), 
    v1 = sample(5, N, TRUE),       
    v2 = sample(1e6, N, TRUE),       
    v3 = sample(round(runif(100,max=100),4), N, TRUE)      
) 
system.time(DT[,a_inplace:=mean(v1)]) 
user system elapsed 
    0.057 0.014 0.089 
system.time(DT[,a_inplace:=NULL]) 
user system elapsed 
    0.038 0.009 0.061 

system.time(DT <- DT[,c(.SD,a_usual=mean(v1)),.SDcols=names(DT)]) 
user system elapsed 
2.483 0.146 2.602 
system.time(DT <- DT[,list(id1,id2,id3,v1,v2,v3)]) 
user system elapsed 
1.143 0.088 1.220 

Ora capisco che setkey o X[Y,:=] non può essere espressa in termini di copie poco profonde - così mi sto davvero solo chiedendo di creazione/eliminazione di nuove colonne o righe.

risposta

12

In data.table, := e tuttiset* oggetti funzioni di aggiornamento di riferimento. Questo è stato introdotto intorno al 2012 IIRC. E in questo momento, la base R non ha la copia superficiale, ma profonda copiata. Shallow copia è stata introdotta dalla 3.1.0.


Si tratta di una risposta prolisso/lungo, ma penso che questo risponde alle vostre prime due domande:

Come è questo metodo R base diversa dal equivalente data.table? La differenza riguarda solo la velocità o anche l'utilizzo della memoria?

In base di R + v3.1.0 quando facciamo:

DF1 = data.frame(x=1:5, y=6:10, z=11:15) 
DF2 = DF1[, c("x", "y")] 
DF3 = transform(DF2, y = ifelse(y>=8L, 1L, y)) 
DF4 = transform(DF2, y = 2L) 
  1. Da DF1 a DF2, entrambe le colonne sono solo superficiale copiati.
  2. Da DF2 al DF3 colonna y solo doveva essere copiato/riassegnazione, ma x ottiene superficiale copiato nuovamente.
  3. Da DF2 a DF4, uguale a (2).

Ovvero, le colonne vengono copiate in modo superficiale finché la colonna rimane invariata - in un modo, la copia viene ritardata a meno che non sia assolutamente necessario.

In data.table, si modifica sul posto. Il significato anche durante la colonna DF3 e DF4 non viene copiato nella colonna .

DT2[y >= 8L, y := 1L] ## (a) 
DT2[, y := 2L] 

Qui, dal y è già una colonna integer, e ci sono modificandola da intero, per riferimento, non c'è nuovo allocazione di memoria fatta qui a tutti.

Questo è anche particolarmente utile quando si desidera assegnare sotto il numero di telefono per riferimento (contrassegnato come (a) sopra). Questa è una funzionalità utile che ci piace molto in data.table.

Un altro vantaggio che viene fornito gratuitamente (che sono venuto a sapere dai nostri interazioni) è, quando abbiamo, per esempio, convertire tutte le colonne di una data.table a un tipo numeric, da dire, character Tipo:

DT[, (cols) := lapply(.SD, as.numeric), .SDcols = cols] 

Qui, dal momento che stiamo aggiornando per riferimento, ogni colonna personaggio ottiene sostituito per riferimento con il suo omologo numerico. E dopo quella sostituzione, la precedente colonna di caratteri non è più richiesta ed è in palio per la garbage collection. Ma se si dovesse farlo usando di base R:

DF[] = lapply(DF, as.numeric) 

Tutte le colonne dovranno essere convertite a numerico, e che dovrete essere tenuto in un temporanea variabile, e poi finalmente verranno assegnati di nuovo a DF. Ciò significa che, se hai 10 colonne con 100 milioni di righe, ciascuna di tipo di carattere, allora il vostro DF prende uno spazio di:

10 * 100e6 * 4/1024^3 = ~ 3.7GB 

E poiché numeric tipo è due volte tanto in termini di dimensioni, avremo bisogno un totale di 7.4GB + 3.7GB di spazio per noi per fare la conversione con base di R.

ma nota che data.table copie durante DF1-DF2. Cioè:

DT2 = DT1[, c("x", "y"), with=FALSE] 

risultati in una copia, perché non possiamo sub-assegnare da riferimento un superficiale copia . Aggiornerà tutti i cloni.

Ciò che sarebbe bello se potessimo integrare senza problemi la funzione di copia superficiale, ma tenere traccia di se le colonne di un particolare oggetto ha più riferimenti e aggiornare per riferimento ove possibile. La funzione di conteggio dei riferimenti aggiornata di R potrebbe essere molto utile a questo riguardo. In ogni caso, ci stiamo adoperando.


Per la vostra ultima domanda: "Quando è la differenza più consistente"

  1. Non ci sono ancora persone che devono utilizzare le vecchie versioni di R, in cui le copie profonde non possono essere evitati.

  2. Dipende da quante colonne vengono copiate perché le operazioni eseguite su di esso. Lo scenario peggiore sarebbe che hai copiato tutte le colonne, ovviamente.

  3. Ci sono casi come this in cui la copia superficiale non ne trarrà beneficio.

  4. Quando si desidera aggiornare le colonne di un data.frame per ogni gruppo e ci sono troppi gruppi.

  5. Quando si desidera aggiornare una colonna di dire, data.table DT1 sulla base di un join con un altro data.table DT2 - questo può essere fatto come:

    DT1[DT2, col := i.val] 
    

    dove i. si riferisce al valore dalla colonna val di DT2 (argomento i) per le righe corrispondenti. Questa sintassi consente di eseguire questa operazione in modo molto efficiente, invece di dover prima aggiungere l'intero risultato e quindi aggiornare la colonna richiesta.

Tutto sommato, ci sono forti argomenti in cui aggiornamento con riferimento permetterebbe di risparmiare un sacco di tempo, e di essere veloce. Ma a volte le persone non amano aggiornare gli oggetti sul posto e sono disposti a sacrificare la velocità/la memoria per questo. Stiamo cercando di capire come offrire al meglio questa funzionalità, oltre all'aggiornamento già esistente per riferimento.

Spero che questo aiuti. Questa è già una risposta abbastanza lunga. Lascerò tutte le domande che potresti aver lasciato agli altri o per te a capire (a parte qualsiasi idea sbagliata in questa risposta).