Qual è lo scopo di impostare una chiave in data.table?


113

Sto usando data.table e ci sono molte funzioni che richiedono di impostare una chiave (ad esempio X[Y]). Come tale, desidero capire cosa fa una chiave per impostare correttamente le chiavi nelle mie tabelle di dati.


Una fonte che ho letto è stata ?setkey.

setkey()ordina un data.tablee lo contrassegna come ordinato. Le colonne ordinate sono la chiave. La chiave può essere qualsiasi colonna in qualsiasi ordine. Le colonne sono sempre ordinate in ordine crescente. La tabella viene modificata per riferimento. Non viene eseguita alcuna copia, a parte la memoria di lavoro temporanea grande quanto una colonna.

La mia conclusione qui è che una chiave "ordinerebbe" data.table, ottenendo un effetto molto simile a order(). Tuttavia, non spiega lo scopo di avere una chiave.


Le FAQ di data.table 3.2 e 3.3 spiegano:

3.2 Non ho una chiave su un tavolo grande, ma il raggruppamento è comunque molto veloce. Perché?

data.table utilizza l'ordinamento digitale. Questo è notevolmente più veloce di altri algoritmi di ordinamento. Radix è specificatamente solo per interi, vedi ?base::sort.list(x,method="radix"). Questo è anche uno dei motivi per cui setkey()è veloce. Quando non è impostata alcuna chiave, o raggruppiamo in un ordine diverso da quello della chiave, la chiamiamo ad hoc by.

3.3 Perché il raggruppamento per colonne nella chiave è più veloce di un raggruppamento ad hoc per?

Poiché ogni gruppo è contiguo nella RAM, riducendo così al minimo i recuperi di pagina, e la memoria può essere copiata in blocco ( memcpyin C) anziché in loop in C.

Da qui, immagino che l'impostazione di una chiave in qualche modo consenta a R di usare "radix sorting" rispetto ad altri algoritmi, ed è per questo che è più veloce.


La guida rapida di 10 minuti ha anche una guida sui tasti.

  1. chiavi

Cominciamo considerando data.frame, specically rownames (o in inglese, nomi di riga). Cioè, i nomi multipli che appartengono a una singola riga. I nomi multipli che appartengono alla singola riga? Non è quello a cui siamo abituati in un data.frame. Sappiamo che ogni riga ha al massimo un nome. Una persona ha almeno due nomi, un primo nome e un secondo nome. Ciò è utile per organizzare un elenco telefonico, ad esempio, ordinato per cognome, quindi per primo. Tuttavia, ogni riga in un data.frame può avere un solo nome.

Una chiave è costituita da una o più colonne di nomi utente, che possono essere interi, fattori, caratteri o qualche altra classe, non semplicemente caratteri. Inoltre, le righe vengono ordinate in base alla chiave. Pertanto, un data.table può avere al massimo una chiave, perché non può essere ordinato in più di un modo.

L'unicità non viene applicata, ovvero sono consentiti valori chiave duplicati. Poiché le righe sono ordinate in base alla chiave, eventuali duplicati nella chiave appariranno consecutivamente

L'elenco telefonico è stato utile per capire cos'è una chiave, ma sembra che una chiave non sia diversa rispetto alla colonna dei fattori. Inoltre, non spiega perché è necessaria una chiave (soprattutto per utilizzare determinate funzioni) e come scegliere la colonna da impostare come chiave. Inoltre, sembra che in un data.table con il tempo come colonna, l'impostazione di qualsiasi altra colonna come chiave probabilmente rovinerebbe anche la colonna del tempo, il che la rende ancora più confusa poiché non so se mi è permesso impostare qualsiasi altra colonna come chiave. Qualcuno può illuminarmi per favore?


"Immagino che impostare una chiave in qualche modo consenta a R di usare" radix sorting "su altri algoritmi" - Non lo ottengo affatto dall'help. La mia lettura è che l'impostazione di una chiave ordina in base a una chiave. Puoi eseguire l'ordinamento "ad hoc" in base a colonne diverse dalla chiave, ed è veloce, ma non così veloce come se avessi già ordinato.
Ari B. Friedman

Penso che la ricerca binaria sia più veloce della scansione vettoriale quando si selezionano le righe. Non sono uno scienziato informatico, quindi non so cosa significhi effettivamente. Oltre alle FAQ, vedi l'introduzione .
Frank

Risposte:


125

Aggiornamento minore: fai riferimento anche alle nuove vignette HTML . Questo numero mette in evidenza le altre vignette che abbiamo in programma.


Ho aggiornato nuovamente questa risposta (febbraio 2016) alla luce della nuova on=funzionalità che consente anche i join ad-hoc . Visualizza la cronologia per le risposte precedenti (obsolete).

Cosa fa esattamente setkey(DT, a, b)?

Fa due cose:

  1. riordina le righe di data.table DT dalle colonne fornite ( a , b ) per riferimento , sempre in ordine crescente .
  2. contrassegna quelle colonne come colonne chiave impostando un attributo chiamato sorteda DT.

Il riordino è sia veloce (grazie all'ordinamento digitale interno di data.table ) che efficiente in termini di memoria ( viene allocata solo una colonna extra di tipo double ).

Quando è setkey()richiesto?

Per le operazioni di raggruppamento, setkey()non è mai stato un requisito assoluto. Cioè, possiamo eseguire un cold-by o un adhoc-by .

## "cold" by
require(data.table)
DT <- data.table(x=rep(1:5, each=2), y=1:10)
DT[, mean(y), by=x] # no key is set, order of groups preserved in result

Tuttavia, prima v1.9.6, i join del modulo x[i]devono keyessere impostatix . Con il nuovo on=argomento da v1.9.6 + , questo non è più vero, e impostando le chiavi quindi, è non è un requisito assoluto anche qui.

## joins using < v1.9.6 
setkey(X, a) # absolutely required
setkey(Y, a) # not absolutely required as long as 'a' is the first column
X[Y]

## joins using v1.9.6+
X[Y, on="a"]
# or if the column names are x_a and y_a respectively
X[Y, on=c("x_a" = "y_a")]

Nota che l' on=argomento può essere specificato esplicitamente anche per i keyedjoin.

L'unica operazione che richiede key di essere impostata in modo assoluto è la funzione foverlaps () . Ma stiamo lavorando su alcune altre funzionalità che una volta terminate eliminerebbero questo requisito.

  • Allora qual è il motivo per implementare l' on=argomento?

    Ci sono diversi motivi.

    1. Permette di distinguere chiaramente l'operazione come un'operazione che coinvolge due data.tables . Anche il semplice fatto X[Y]non distingue questo, anche se potrebbe essere chiaro nominando le variabili in modo appropriato.

    2. Consente inoltre di comprendere le colonne su cui si trova il join / sottoinsieme viene eseguito immediatamente guardando quella riga di codice (e non dovendo risalire alla setkey()riga corrispondente ).

    3. Nelle operazioni in cui le colonne vengono aggiunte o aggiornate per riferimento ,on= operazioni sono molto più performanti in quanto non è necessario riordinare l'intero data.table solo per aggiungere / aggiornare le colonne. Per esempio,

      ## compare 
      setkey(X, a, b) # why physically reorder X to just add/update a column?
      X[Y, col := i.val]
      
      ## to
      X[Y, col := i.val, on=c("a", "b")]

      Nel secondo caso, non abbiamo dovuto riordinare. Non è il calcolo dell'ordine che richiede tempo, ma il riordino fisico della data.table nella RAM e, evitandolo, conserviamo l'ordine originale ed è anche performante.

    4. Anche in caso contrario, a meno che non si eseguano join ripetutamente, non dovrebbero esserci differenze di prestazioni evidenti tra join con chiave e join ad-hoc .

Questo porta alla domanda, quale vantaggio ha più la codifica di un data.table ?

  • C'è un vantaggio nel digitare un data.table?

    La digitazione di un data.table lo riordina fisicamente in base a quelle colonne nella RAM. Il calcolo dell'ordine di solito non è la parte che richiede tempo, ma piuttosto il riordino stesso. Tuttavia, una volta ordinati i dati nella RAM, le righe appartenenti allo stesso gruppo sono tutte contigue nella RAM ed è quindi molto efficiente nella cache. È l'ordinamento che accelera le operazioni su data.tables con chiave.

    È quindi essenziale capire se il tempo speso per riordinare l'intero data.table vale il tempo per fare un join / aggregazione efficiente nella cache. Di solito, a meno che non vengano eseguite operazioni ripetitive di raggruppamento / unione sulla stessa tabella data.table con chiave , non dovrebbe esserci una differenza evidente.

Nella maggior parte dei casi, quindi, non dovrebbe essere più necessario impostare le chiavi. Si consiglia di utilizzare on=ove possibile, a meno che l'impostazione della chiave non abbia un notevole miglioramento delle prestazioni che si desidera sfruttare.

Domanda: quale sarebbe la performance rispetto a un join con chiave , se si utilizza setorder()per riordinare data.table e utilizzare on=? Se l'hai seguito fino ad ora, dovresti essere in grado di capirlo :-).


3
Figo, grazie! Fino ad ora, non avevo pensato a cosa significasse effettivamente "ricerca binaria", né veramente capito il motivo per cui è stato utilizzato al posto di un hash.
Frank

@ Arun, è DT[J(1e4:1e5)]davvero equivalente a DF[DF$x > 1e4 & DF$x < 1e5, ]? Potresti indicarmi cosa Jsignifica? Inoltre, la ricerca non restituirà alcuna riga poiché sample(1e4, 1e7, TRUE)non include numeri superiori a 1e4.
acquario

@fishtank, in questo caso, dovrebbe essere >=e <=- corretto. J(e .) sono alias di list(cioè sono equivalenti). Internamente, quando iè un elenco, viene convertito in data.table a seguito del quale la ricerca binaria viene utilizzata per calcolare gli indici di riga. Risolto 1e4per 1e5evitare confusione. Grazie per aver notato. Nota che ora possiamo usare direttamente l' on=argomento per eseguire sottoinsiemi binari piuttosto che impostare la chiave. Leggi di più dalle nuove vignette HTML . E tieni d'occhio quella pagina per le vignette per i join.
Arun

forse questo potrebbe andare per un aggiornamento più approfondito? la sezione "quando richiesto" sembra obsoleta, ad esempio
MichaelChirico

Quale funzione ti dice la chiave utilizzata?
skan

20

Una chiave è fondamentalmente un indice in un set di dati, che consente operazioni di ordinamento, filtro e unione molto veloci ed efficienti. Questi sono probabilmente i migliori motivi per usare le tabelle di dati invece dei frame di dati (la sintassi per usare le tabelle di dati è anche molto più facile da usare, ma non ha nulla a che fare con le chiavi).

Se non capisci gli indici, considera questo: una rubrica è "indicizzata" per nome. Quindi, se voglio cercare il numero di telefono di qualcuno, è abbastanza semplice. Ma supponiamo che io voglia effettuare una ricerca per numero di telefono (ad esempio, cercare chi ha un determinato numero di telefono)? A meno che non riesca a "reindicizzare" la rubrica per numero di telefono, ci vorrà molto tempo.

Considera il seguente esempio: supponiamo di avere una tabella, ZIP, di tutti i codici postali negli Stati Uniti (> 33.000) insieme alle informazioni associate (città, stato, popolazione, reddito medio, ecc.). Se voglio cercare le informazioni per un codice postale specifico, la ricerca (filtro) è circa 1000 volte più veloce se lo faccio setkey(ZIP,zipcode)prima.

Un altro vantaggio ha a che fare con i join. Supponiamo di avere un elenco di persone e dei loro codici postali in una tabella di dati (chiamiamola "PPL"), e voglio aggiungere informazioni dalla tabella ZIP (ad esempio città, stato e così via). Il codice seguente lo farà:

setkey(ZIP,zipcode)
setkey(PPL,zipcode)
full.info <- PPL[ZIP, nomatch=F]

Questo è un "join" nel senso che sto unendo le informazioni da 2 tabelle basate su un campo comune (codice postale). I join come questo su tabelle molto grandi sono estremamente lenti con i frame di dati ed estremamente veloci con le tabelle di dati. In un esempio di vita reale ho dovuto fare più di 20.000 join come questo su una tabella completa di codici postali. Con le tabelle di dati lo script ha richiesto circa 20 minuti. correre. Non l'ho nemmeno provato con i frame di dati perché ci sarebbero volute più di 2 settimane.

IMHO non dovresti solo leggere ma studiare le FAQ e il materiale introduttivo. È più facile capire se hai un problema reale a cui applicarlo.

[Risposta al commento di @ Frank]

Ri: ordinamento e indicizzazione - In base alla risposta a questa domanda , sembra che setkey(...)in realtà riorganizzi le colonne nella tabella (ad esempio, un ordinamento fisico) e non crei un indice nel senso del database. Ciò ha alcune implicazioni pratiche: per prima cosa se si imposta la chiave in una tabella con setkey(...)e poi si modifica uno qualsiasi dei valori nella colonna chiave, data.table dichiara semplicemente che la tabella non è più ordinata (disattivando l' sortedattributo); non si reindicizza dinamicamente per mantenere il corretto ordinamento (come accadrebbe in un database). Inoltre, "rimuovendo il tasto" utilizzando setky(DT,NULL)non non ripristinare il tavolo ad esso è originale, l'ordine non differenziati.

Ri: filtro e join : la differenza pratica è che il filtro estrae un sottoinsieme da un singolo set di dati, mentre il join combina i dati da due set di dati in base a un campo comune. Esistono molti tipi diversi di join (interno, esterno, sinistro). L'esempio sopra è un join interno (vengono restituiti solo i record con chiavi comuni a entrambe le tabelle) e questo ha molte somiglianze con il filtro.


1
+1. Per quanto riguarda la tua prima frase ... è già ordinata, giusto? E un join non è un caso speciale di filtro (o un'operazione che prende il filtro come primo passaggio)? Sembra che il "filtraggio migliore" riassuma l'intero vantaggio.
Frank

1
O meglio la scansione suppongo.
Piedi bagnati

1
@jlhoward Grazie. La mia convinzione precedente era che l'ordinamento non fosse tra i vantaggi dell'impostazione della chiave (poiché se vuoi ordinare, dovresti semplicemente ordinare), e anche questo in setkeyrealtà riordina le righe in modo irreversibile. Se è solo a scopo di visualizzazione, come faccio a stampare le prime dieci righe secondo l'ordine "vero" (che avrei visto prima di setkey)? Sono abbastanza sicuro che setkey(DT,NULL)non lo faccia ... (cont.)
Frank

... (cont.) Inoltre, non ho esaminato il codice per il pacchetto, ma per unirti X[Y,...], devi "filtrare" le righe di X usando la chiave. Certo, altre cose accadono dopo (le colonne di Y sono rese disponibili e c'è un implicito by-without-by), ma ancora non lo vedo come un vantaggio concettualmente distinto. Immagino che la tua risposta sia espressa in termini di operazioni che potresti voler fare, tuttavia, in cui la distinzione potrebbe essere utile.
Frank

1
@Frank - Quindi setkey(DT,NULL)rimuove la chiave ma non influisce sull'ordinamento. Ho posto una domanda su questo qui . Vediamo.
jlhoward
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.