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




(2)

Sto usando data.table e ci sono molte funzioni che mi richiedono di impostare una chiave (ad es. X[Y] ). In quanto 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.table e lo contrassegna come ordinato. Le colonne ordinate sono la chiave. La chiave può essere qualsiasi colonna in qualsiasi ordine. Le colonne sono ordinate sempre in ordine crescente. La tabella è cambiata per riferimento. Nessuna copia viene creata, a parte una memoria di lavoro temporanea grande quanto una colonna.

Il mio takeaway è che una chiave "ordina" il data.table, risultando in un effetto molto simile a order() . Tuttavia, non spiega lo scopo di avere una chiave.

Le FAQ 3.2 e 3.3 di data.table spiegano:

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

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

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

Poiché ogni gruppo è contiguo nella RAM, riducendo così al minimo i recuperi di pagine e la memoria può essere copiata in memcpy ( memcpy in C) anziché eseguire il ciclo in C.

Da qui, suppongo che l'impostazione di una chiave in qualche modo consente a R di utilizzare "ordinamento digitale" su altri algoritmi, ed è per questo che è più veloce.

La guida di avvio rapido di 10 minuti ha anche una guida sui tasti.

  1. chiavi

Iniziamo considerando data.frame, in modo specifico i nomi dei giocatori (o in inglese, i nomi delle righe). Cioè, i nomi multipli appartenenti a una singola riga. I nomi multipli appartenenti 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. Questo è utile per organizzare una rubrica telefonica, ad esempio, che è ordinata per cognome, quindi per nome. Tuttavia, ogni riga in un data.frame può avere solo un nome.

Una chiave è costituita da una o più colonne di nomi di ballo, che possono essere un numero intero, un fattore, un carattere o qualche altra classe, non semplicemente un carattere. Inoltre, le righe sono ordinate per 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 per chiave, eventuali duplicati nella chiave appariranno consecutivamente

L'elenco telefonico è stato utile per capire che 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 una colonna, l'impostazione di qualsiasi altra colonna come chiave probabilmente farebbe anche pasticciare la colonna del tempo, il che rende ancora più confusa dato che non so se posso impostare qualsiasi altra colonna come chiave. Qualcuno può illuminarmi per favore?


Una chiave è fondamentalmente un indice in un set di dati, che consente operazioni di ordinamento, filtro e join molto veloci ed efficienti. Questi sono probabilmente i migliori motivi per utilizzare le tabelle di dati anziché i frame di dati (la sintassi per l'utilizzo delle tabelle di dati è anche molto più user friendly, ma ciò 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, è piuttosto semplice. Ma supponiamo di voler effettuare una ricerca per numero di telefono (ad esempio, cercare chi ha un numero di telefono specifico)? A meno che non riesca a "indicizzare" 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 uno specifico codice postale, la ricerca (filtro) è circa 1000 volte più veloce se prima ho setkey(ZIP,zipcode) .

Un altro vantaggio ha a che fare con i join. Supponiamo di avere una lista di persone e dei loro codici postali in una tabella di dati (chiamiamola "PPL"), e voglio aggiungere informazioni dalla tabella ZIP (es. Città, stato e così via). Il seguente codice 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 (zipcode). Unioni come questa su tabelle molto grandi sono estremamente lente con i frame di dati e estremamente veloci con le tabelle di dati. In un esempio di vita reale dovevo fare più di 20.000 join come questo su una tabella completa di codici postali. Con le tabelle di dati lo script ha impiegato circa 20 minuti. correre. Non l'ho nemmeno provato con i frame di dati perché ci sarebbero voluti 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 per applicarlo.

[Risposta al commento di @ Frank]

Ri: sorting vs. indexing - In base alla risposta a questa domanda , sembra che setkey(...) in effetti riorganizzi le colonne nella tabella (ad es. Un ordinamento fisico) e non crei un indice nel database senso. Ciò ha alcune implicazioni pratiche: per prima cosa se si imposta la chiave in una tabella con setkey(...) e quindi si modifica uno qualsiasi dei valori nella colonna chiave, data.table dichiara semplicemente che la tabella non deve più essere ordinata (per disattivare l'attributo sorted ); non reindicizza dinamicamente per mantenere l'ordine corretto (come accadrebbe in un database). Inoltre, "rimuovere la chiave" usando setky(DT,NULL) non ripristina la tabella al suo ordine originale, non ordinato.

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


Aggiornamento minore: si prega di fare riferimento anche alle nuove vignette HTML . Questo problema evidenzia le altre vignette che abbiamo in programma.

Ho aggiornato di nuovo questa risposta (febbraio 2016) alla luce della nuova funzione on= che consente anche join ad-hoc . Vedi cronologia per risposte precedenti (obsolete).

Che cosa fa esattamente setkey(DT, a, b) ?

Fa due cose:

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

Il riordino è veloce (grazie all'ordinamento interno dei dati di data.table ) ed efficiente in termini di memoria (viene allocata solo una colonna aggiuntiva di tipo double ).

Quando è richiesto setkey() ?

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

## "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 della v1.9.6 , i join della forma x[i] key richiesta devono essere impostati su x . Con il nuovo argomento on= dalla v1.9.6 + , questo non è più vero, e anche qui le chiavi di impostazione non sono un requisito assoluto.

## 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")]

Si noti che on= argomento può essere specificato esplicitamente anche per i join con keyed .

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

  • Quindi qual è la ragione per implementare on= argomento?

    Ci sono parecchi motivi.

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

    2. Permette anche di capire le colonne su cui viene eseguito immediatamente il join / sottogruppo guardando quella riga di codice (e non dovendo eseguire il traceback alla corrispondente linea setkey() ).

    3. Nelle operazioni in cui le colonne vengono aggiunte o aggiornate per riferimento , on= operazioni sono molto più performanti in quanto non ha bisogno dell'intero data.table da riordinare 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 sta calcolando l'ordine che richiede molto tempo, ma riordinando fisicamente il data.table in RAM, e evitandolo, manteniamo l'ordine originale ed è anche performante.

    4. Anche in caso contrario, a meno che non si stiano eseguendo i join ripetutamente, non ci dovrebbero essere differenze di prestazioni notevoli tra join uniti e ad hoc .

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

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

    La digitazione di un data.table la riordina fisicamente in base a quelle colonne nella RAM. Calcolare l'ordine non è solitamente la parte che richiede tempo, 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 in termini di cache. È l'ordinamento che accelera le operazioni su dati key.tables.

    È quindi essenziale capire se il tempo impiegato per riordinare l'intero data.table vale il tempo di fare un join / aggregazione efficiente in cache. Di solito, a meno che non ci siano operazioni ripetitive di raggruppamento / join eseguite sullo stesso file data.table, non ci dovrebbe essere una differenza evidente.

Nella maggior parte dei casi, quindi, non dovrebbe essere più necessario impostare le chiavi. Ti consigliamo di utilizzare on= ove possibile, a meno che la chiave di impostazione non abbia un notevole miglioramento delle prestazioni che desideri sfruttare.

Domanda: Quale pensi che sarebbe la performance rispetto a un join con chiave , se usi setorder() per riordinare data.table e usare on= ? Se hai seguito fino ad ora, dovresti riuscire a capirlo :-).





data.table