Was ist der Zweck des Festlegens eines Schlüssels in data.table?


113

Ich verwende data.table und es gibt viele Funktionen, bei denen ich einen Schlüssel setzen muss (z X[Y]. B. ). Daher möchte ich verstehen, was ein Schlüssel tut, um Schlüssel in meinen Datentabellen richtig festzulegen.


Eine Quelle, die ich las, war ?setkey.

setkey()sortiert a data.tableund markiert es als sortiert. Die sortierten Spalten sind der Schlüssel. Der Schlüssel kann eine beliebige Spalte in beliebiger Reihenfolge sein. Die Spalten sind immer in aufsteigender Reihenfolge sortiert. Die Tabelle wird als Referenz geändert. Es wird überhaupt keine Kopie erstellt, außer dem temporären Arbeitsspeicher, der so groß wie eine Spalte ist.

Meine Erkenntnis hier ist, dass ein Schlüssel die data.table "sortieren" würde, was zu einem sehr ähnlichen Effekt führen würde wie order(). Es erklärt jedoch nicht den Zweck eines Schlüssels.


In den häufig gestellten Fragen 3.2 und 3.3 der Datentabelle wird Folgendes erläutert:

3.2 Ich habe keinen Schlüssel auf einem großen Tisch, aber die Gruppierung ist immer noch sehr schnell. Warum ist das so?

data.table verwendet die Radix-Sortierung. Dies ist deutlich schneller als andere Sortieralgorithmen. Radix ist speziell nur für ganze Zahlen, siehe ?base::sort.list(x,method="radix"). Dies ist auch ein Grund, warum setkey()es schnell geht. Wenn kein Schlüssel festgelegt ist oder wir in einer anderen Reihenfolge als der Schlüssel gruppieren, nennen wir ihn Ad-hoc von.

3.3 Warum ist die Gruppierung nach Spalten im Schlüssel schneller als eine Ad-hoc-Gruppierung?

Da jede Gruppe im RAM zusammenhängend ist, wodurch Seitenabrufe minimiert werden und der Speicher in großen Mengen ( memcpyin C) kopiert werden kann, anstatt in C eine Schleife zu bilden.

Von hier aus denke ich, dass das Setzen eines Schlüssels es R irgendwie ermöglicht, die "Radix-Sortierung" gegenüber anderen Algorithmen zu verwenden, und deshalb ist es schneller.


Die 10-minütige Kurzanleitung enthält auch eine Anleitung zu den Tasten.

  1. Schlüssel

Beginnen wir mit der Betrachtung von data.frame, insbesondere von Rownamen (oder in Englisch von Zeilennamen). Das heißt, die mehreren Namen, die zu einer einzelnen Zeile gehören. Die mehreren Namen, die zur einzelnen Zeile gehören? Das ist nicht das, was wir in einem data.frame gewohnt sind. Wir wissen, dass jede Zeile höchstens einen Namen hat. Eine Person hat mindestens zwei Namen, einen ersten und einen zweiten Namen. Dies ist nützlich, um beispielsweise ein Telefonverzeichnis zu organisieren, das nach Nachname und Vorname sortiert ist. Jede Zeile in einem data.frame kann jedoch nur einen Namen haben.

Ein Schlüssel besteht aus einer oder mehreren Spalten von Rownamen, die eine Ganzzahl, ein Faktor, ein Zeichen oder eine andere Klasse sein können, nicht nur ein Zeichen. Außerdem werden die Zeilen nach dem Schlüssel sortiert. Daher kann eine data.table höchstens einen Schlüssel haben, da sie nicht auf mehrere Arten sortiert werden kann.

Die Eindeutigkeit wird nicht erzwungen, dh doppelte Schlüsselwerte sind zulässig. Da die Zeilen nach dem Schlüssel sortiert sind, werden alle Duplikate im Schlüssel nacheinander angezeigt

Das Telefonverzeichnis war hilfreich, um zu verstehen, was ein Schlüssel ist, aber es scheint, dass ein Schlüssel nicht anders ist als eine Faktorspalte. Darüber hinaus wird nicht erklärt, warum ein Schlüssel benötigt wird (insbesondere um bestimmte Funktionen zu verwenden) und wie die Spalte ausgewählt wird, die als Schlüssel festgelegt werden soll. Außerdem scheint es, dass in einer data.table mit der Zeit als Spalte das Festlegen einer anderen Spalte als Schlüssel wahrscheinlich auch die Zeitspalte durcheinander bringt, was es noch verwirrender macht, da ich nicht weiß, ob ich eine andere Spalte als festlegen darf Schlüssel. Kann mich bitte jemand aufklären?


"Ich denke, dass das Setzen eines Schlüssels es R irgendwie ermöglicht," Radix-Sortierung "gegenüber anderen Algorithmen zu verwenden" - das bekomme ich überhaupt nicht von der Hilfe. Ich habe gelesen, dass das Setzen eines Schlüssels nach einem Schlüssel sortiert wird. Sie können "ad hoc" nach anderen Spalten als dem Schlüssel sortieren. Dies ist schnell, aber nicht so schnell, als ob Sie bereits sortiert hätten.
Ari B. Friedman

Ich denke, dass die binäre Suche bei der Auswahl von Zeilen schneller ist als die Vektorsuche. Ich bin kein Informatiker, also weiß ich nicht, was das eigentlich bedeutet. Neben den FAQ siehe die Einführung .
Frank

Antworten:


125

Kleinere Aktualisierung: Bitte beachten Sie auch die neuen HTML-Vignetten . In dieser Ausgabe werden die anderen Vignetten vorgestellt, die wir planen.


Ich habe diese Antwort (Februar 2016) angesichts der neuen on=Funktion, die auch Ad-hoc- Joins ermöglicht, erneut aktualisiert . Frühere (veraltete) Antworten finden Sie im Verlauf.

Was genau macht setkey(DT, a, b)das?

Es macht zwei Dinge:

  1. ordnet die Zeilen der Datentabelle DT nach den bereitgestellten Spalten neu an ( a , b ) als Referenz neu an , immer in aufsteigender Reihenfolge.
  2. markiert diese Spalten als Schlüsselspalten , indem ein Attribut festgelegt wird, sortedzu dem aufgerufen wird DT.

Die Neuordnung ist sowohl schnell (aufgrund der internen Radix-Sortierung von data.table ) als auch speichereffizient (nur eine zusätzliche Spalte vom Typ double wird zugewiesen).

Wann ist setkey()erforderlich?

Für Gruppierungsvorgänge setkey()war dies nie eine absolute Voraussetzung. Das heißt, wir können eine Erkältung durchführen oder By Ad-Hoc-By durchführen .

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

Vorher v1.9.6müssen jedoch Verknüpfungen des Formulars x[i]aktiviert keywerden x. Mit dem neuen on=Argument aus Version 1.9.6 + ist dies nicht mehr der Fall, und das Setzen von Schlüsseln ist daher auch hier keine absolute Voraussetzung.

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

Beachten Sie, dass das on=Argument auch für keyedJoins explizit angegeben werden kann .

Die einzige Operation, die keyunbedingt eingestellt werden muss, ist die Funktion foverlaps () . Wir arbeiten jedoch an einigen weiteren Funktionen, mit denen diese Anforderung beseitigt werden kann.

  • Was ist der Grund für die Implementierung von on=Argumenten?

    Es gibt einige Gründe.

    1. Es ermöglicht eine klare Unterscheidung der Operation als eine Operation mit zwei Datentabellen . Nur dies zu tun, X[Y]unterscheidet dies auch nicht, obwohl dies durch die entsprechende Benennung der Variablen deutlich werden könnte.

    2. Sie können auch die Spalten verstehen, für die der Join / die Teilmenge sofort ausgeführt wird, indem Sie sich diese Codezeile ansehen (und nicht zur entsprechenden setkey()Zeile zurückverfolgen müssen ).

    3. Bei Operationen, bei denen Spalten durch Referenz hinzugefügt oder aktualisiert werden , sind on=Operationen viel leistungsfähiger, da nicht die gesamte Datentabelle neu angeordnet werden muss, nur um Spalten hinzuzufügen / zu aktualisieren. Beispielsweise,

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

      Im zweiten Fall mussten wir nicht nachbestellen. Es geht nicht darum, die Reihenfolge zu berechnen, die zeitaufwändig ist, sondern die Datentabelle im RAM physisch neu zu ordnen. Indem wir sie vermeiden, behalten wir die ursprüngliche Reihenfolge bei und sie ist auch performant.

    4. Selbst ansonsten sollte es keinen merklichen Leistungsunterschied zwischen verschlüsselten und Ad-hoc- Verknüpfungen geben , es sei denn, Sie führen Joins wiederholt durch .

Dies führt zu der Frage, welchen Vorteil das Eingeben einer data.table mehr hat.

  • Gibt es einen Vorteil beim Eingeben einer Datentabelle?

    Wenn Sie eine data.table eingeben, wird sie basierend auf den Spalten im RAM physisch neu angeordnet. Die Berechnung der Bestellung ist normalerweise nicht der zeitaufwändige Teil, sondern die Neuordnung selbst. Sobald wir die Daten im RAM sortiert haben, sind die Zeilen, die zu derselben Gruppe gehören, alle im RAM zusammenhängend und daher sehr cacheeffizient. Es ist die Sortierung, die Operationen an verschlüsselten Datentabellen beschleunigt.

    Es ist daher wichtig herauszufinden, ob die Zeit, die für die Neuordnung der gesamten Daten aufgewendet wird, die Zeit für eine cacheeffiziente Verknüpfung / Aggregation wert ist. Normalerweise, es sei denn es sind sich wiederholende Gruppierung / Join - Operationen auf demselben ausgeführt werden verkeilten data.table, sollte es nicht einen spürbaren Unterschied.

In den meisten Fällen sollte es daher nicht mehr erforderlich sein, Schlüssel festzulegen. Wir empfehlen, on=wo immer möglich zu verwenden, es sei denn, das Setzen des Schlüssels führt zu einer dramatischen Leistungsverbesserung, die Sie ausnutzen möchten.

Frage: Wie würde Ihrer Meinung nach die Leistung im Vergleich zu einem verschlüsselten Join aussehen , wenn Sie setorder()die data.table neu verwenden und verwenden on=? Wenn Sie bisher gefolgt sind, sollten Sie es herausfinden können :-).


3
Cool, danke! Bis jetzt hatte ich weder darüber nachgedacht, was "binäre Suche" eigentlich bedeutet, noch den Grund verstanden, warum sie anstelle eines Hashs verwendet wurde.
Frank

@Arun, ist DT[J(1e4:1e5)]wirklich gleichbedeutend mit DF[DF$x > 1e4 & DF$x < 1e5, ]? Könnten Sie mich darauf hinweisen, was Jbedeutet? Außerdem würde diese Suche keine Zeilen zurückgeben, da sample(1e4, 1e7, TRUE)sie keine Zahlen über 1e4 enthält.
Fischtank

@fishtank, in diesem Fall sollte es >=und <=- behoben sein. J(und .) sind Aliase zu list(dh sie sind äquivalent). Wenn ies sich um eine Liste handelt, wird sie intern in eine data.table konvertiert, nach der die binäre Suche zum Berechnen von Zeilenindizes verwendet wird. Feste 1e4auf , 1e5um Verwechslungen zu vermeiden. Danke fürs Erkennen. Beachten Sie, dass wir on=jetzt direkt Argumente verwenden können, um binäre Teilmengen auszuführen, anstatt den Schlüssel zu setzen. Lesen Sie mehr aus den neuen HTML-Vignetten . Und behalten Sie diese Seite im Auge, um Vignetten für Verknüpfungen zu erhalten.
Arun

Vielleicht könnte dies für ein gründlicheres Update gehen? Der Abschnitt "wenn erforderlich" scheint veraltet zu sein, zB
MichaelChirico

Welche Funktion sagt Ihnen, welche Taste verwendet wird?
Skan

20

Ein Schlüssel ist im Grunde ein Index in einem Dataset, der sehr schnelle und effiziente Sortier-, Filter- und Verknüpfungsvorgänge ermöglicht. Dies sind wahrscheinlich die besten Gründe, Datentabellen anstelle von Datenrahmen zu verwenden (die Syntax für die Verwendung von Datentabellen ist auch viel benutzerfreundlicher, hat aber nichts mit Schlüsseln zu tun).

Wenn Sie Indizes nicht verstehen, beachten Sie Folgendes: Ein Telefonbuch wird nach Namen "indiziert". Wenn ich also die Telefonnummer von jemandem nachschlagen möchte, ist das ziemlich einfach. Angenommen, ich möchte nach Telefonnummer suchen (z. B. nachschlagen, wer eine bestimmte Telefonnummer hat). Wenn ich das Telefonbuch nicht nach Telefonnummer "neu indizieren" kann, dauert es sehr lange.

Betrachten Sie das folgende Beispiel: Angenommen, ich habe eine Tabelle mit der Postleitzahl aller Postleitzahlen in den USA (> 33.000) sowie die zugehörigen Informationen (Stadt, Bundesland, Bevölkerung, Durchschnittseinkommen usw.). Wenn ich die Informationen für eine bestimmte Postleitzahl nachschlagen möchte, ist die Suche (Filter) etwa 1000-mal schneller, wenn ich sie setkey(ZIP,zipcode)zuerst mache .

Ein weiterer Vorteil betrifft Joins. Angenommen, Sie haben eine Liste mit Personen und deren Postleitzahlen in einer Datentabelle (nennen Sie sie "PPL"), und ich möchte Informationen aus der Postleitzahlentabelle anhängen (z. B. Stadt, Bundesland usw.). Der folgende Code wird es tun:

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

Dies ist ein "Join" in dem Sinne, dass ich die Informationen aus 2 Tabellen in einem gemeinsamen Feld (Postleitzahl) verbinde. Solche Verknüpfungen in sehr großen Tabellen sind bei Datenrahmen extrem langsam und bei Datentabellen extrem schnell. In einem Beispiel aus dem wirklichen Leben musste ich mehr als 20.000 Verknüpfungen wie diese auf einer vollständigen Tabelle mit Postleitzahlen durchführen. Bei Datentabellen dauerte das Skript ca. 20 Minuten. laufen. Ich habe es nicht einmal mit Datenrahmen versucht, da es mehr als 2 Wochen gedauert hätte.

IMHO sollten Sie nicht nur die FAQ und das Intro-Material lesen, sondern auch studieren . Es ist einfacher zu verstehen, ob Sie ein tatsächliches Problem haben, auf das Sie dies anwenden können.

[Antwort auf @ Franks Kommentar]

Betreff: Sortieren vs. Indizieren - Basierend auf der Antwort auf diese Frage scheint es, dass setkey(...)die Spalten in der Tabelle tatsächlich neu angeordnet werden (z. B. eine physische Sortierung) und kein Index im Sinne der Datenbank erstellt wird. Dies hat einige praktische Auswirkungen: Zum einen, wenn Sie den Schlüssel in einer Tabelle mit festlegensetkey(...) und dann einen der Werte in der Schlüsselspalte ändern, deklariert data.table lediglich, dass die Tabelle nicht mehr sortiert ist (indem Sie das sortedAttribut deaktivieren ). Es wird nicht dynamisch neu indiziert, um die richtige Sortierreihenfolge beizubehalten (wie dies in einer Datenbank der Fall wäre). Auch „Entfernen Sie den Schlüssel“ verwendet setky(DT,NULL)wird nicht in der Tabelle wieder her , um es den ursprünglichen, unsortiert.

Betreff: Filter vs. Join - Der praktische Unterschied besteht darin, dass beim Filtern eine Teilmenge aus einem einzelnen Dataset extrahiert wird, während beim Join Daten aus zwei Datasets basierend auf einem gemeinsamen Feld kombiniert werden. Es gibt viele verschiedene Arten von Verknüpfungen (innen, außen, links). Das obige Beispiel ist eine innere Verknüpfung (es werden nur Datensätze mit Schlüsseln zurückgegeben, die beiden Tabellen gemeinsam sind), und dies hat viele Ähnlichkeiten mit der Filterung.


1
+1. In Bezug auf Ihren ersten Satz ... ist er schon richtig sortiert? Und ist ein Join nicht ein Sonderfall eines Filters (oder eine Operation, bei der die Filterung der erste Schritt ist)? Scheint, als würde "bessere Filterung" den gesamten Nutzen zusammenfassen.
Frank

1
Oder besser scannen, nehme ich an.
Wet Feet

1
@ jlhoward Danke. Mein früherer Glaube war, dass das Sortieren nicht zu den Vorteilen des Festlegens des Schlüssels gehört (denn wenn Sie sortieren möchten, sollten Sie nur sortieren), und dass setkeydie Zeilen tatsächlich irreversibel neu angeordnet werden. Wenn es nur zu Anzeigezwecken dient, wie drucke ich dann die ersten zehn Zeilen gemäß der "wahren" Reihenfolge (die ich vor setkey gesehen hätte)? Ich bin mir ziemlich sicher, setkey(DT,NULL)dass ich das nicht tue ... (Forts.)
Frank

... (Forts.) Außerdem habe ich mir den Code für das Paket nicht angesehen, aber um beizutreten X[Y,...], müssen Sie die Zeilen von X mit dem Schlüssel "filtern". Zugegeben, danach passieren andere Dinge (Ys Spalten werden zur Verfügung gestellt, und es gibt ein implizites By-Without-By), aber ich sehe das immer noch nicht als konzeptionell eindeutigen Vorteil. Ich denke, Ihre Antwort bezieht sich auf Operationen, die Sie möglicherweise ausführen möchten, wobei die Unterscheidung hilfreich sein kann.
Frank

1
@Frank - Entfernt also setkey(DT,NULL)den Schlüssel, hat jedoch keinen Einfluss auf die Sortierreihenfolge. Gestellt eine Frage zu diesem hier . Mal schauen.
Jlhoward
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.