¿Cuál es el propósito de establecer una clave en data.table?

113

Estoy usando data.table y hay muchas funciones que requieren que establezca una clave (por ejemplo X[Y]). Como tal, deseo comprender qué hace una clave para configurar correctamente las claves en mis tablas de datos.


Una fuente que leí fue ?setkey.

setkey()ordena un data.tabley lo marca como ordenado. Las columnas ordenadas son la clave. La clave puede ser cualquier columna en cualquier orden. Las columnas se ordenan siempre en orden ascendente. La tabla se cambia por referencia. No se realiza ninguna copia, salvo la memoria de trabajo temporal de una columna.

Mi conclusión aquí es que una clave "ordenaría" la tabla de datos, lo que resultaría en un efecto muy similar a order(). Sin embargo, no explica el propósito de tener una clave.


Las preguntas frecuentes 3.2 y 3.3 de data.table explica:

3.2 No tengo una clave en una mesa grande, pero la agrupación es muy rápida. ¿Porqué es eso?

data.table utiliza la clasificación de radix. Esto es significativamente más rápido que otros algoritmos de clasificación. Radix es específicamente solo para enteros, consulte ?base::sort.list(x,method="radix"). Esta es también una de las razones por las que setkey()es rápido. Cuando no se establece ninguna clave, o lo agrupamos en un orden diferente al de la clave, lo llamamos ad hoc by.

3.3 ¿Por qué el agrupamiento por columnas en la clave es más rápido que un ad hoc por?

Debido a que cada grupo es contiguo en la RAM, lo que minimiza las búsquedas de páginas, y la memoria se puede copiar a granel ( memcpyen C) en lugar de realizar un bucle en C.

A partir de aquí, supongo que establecer una clave de alguna manera le permite a R usar "ordenación de radix" sobre otros algoritmos, y por eso es más rápido.


La guía de inicio rápido de 10 minutos también tiene una guía sobre las teclas.

  1. Llaves

Comencemos considerando data.frame, específicamente nombres de filas (o en inglés, nombres de filas). Es decir, los múltiples nombres que pertenecen a una sola fila. ¿Los múltiples nombres que pertenecen a una sola fila? Eso no es a lo que estamos acostumbrados en un data.frame. Sabemos que cada fila tiene como máximo un nombre. Una persona tiene al menos dos nombres, un primer nombre y un segundo nombre. Esto es útil para organizar un directorio telefónico, por ejemplo, que se ordena por apellido y luego por nombre. Sin embargo, cada fila de un data.frame solo puede tener un nombre.

Una clave consta de una o más columnas de nombres de filas, que pueden ser números enteros, factores, caracteres o alguna otra clase, no simplemente caracteres. Además, las filas están ordenadas por clave. Por lo tanto, una tabla de datos puede tener como máximo una clave, porque no se puede ordenar de más de una forma.

La unicidad no se aplica, es decir, se permiten valores de clave duplicados. Dado que las filas están ordenadas por clave, cualquier duplicado en la clave aparecerá consecutivamente

El directorio telefónico fue útil para comprender qué es una clave, pero parece que una clave no es diferente en comparación con tener una columna de factores. Además, no explica por qué se necesita una clave (especialmente para usar ciertas funciones) y cómo elegir la columna para establecer como clave. Además, parece que en una tabla de datos con el tiempo como columna, establecer cualquier otra columna como clave probablemente también estropearía la columna de tiempo, lo que lo hace aún más confuso ya que no sé si se me permite establecer cualquier otra columna como llave. Alguien puede iluminarme, por favor?

Pies mojados
fuente
"Supongo que establecer una clave de alguna manera le permite a R usar la" ordenación de radix "sobre otros algoritmos", no lo obtengo de la ayuda en absoluto. Mi lectura es que establecer una clave ordena por clave. Puede realizar una clasificación "ad hoc" por otras columnas además de la clave, y es rápido, pero no tan rápido como si ya lo hubiera ordenado.
Ari B. Friedman
Creo que la búsqueda binaria es más rápida que el escaneo vectorial al seleccionar filas. No soy un científico informático, así que no sé qué significa eso en realidad. Además de las preguntas frecuentes, consulte la introducción .
Frank

Respuestas:

125

Actualización menor: consulte también las nuevas viñetas HTML . Este número destaca las otras viñetas que planeamos.


Actualicé esta respuesta nuevamente (febrero de 2016) a la luz de la nueva on=función que también permite uniones ad-hoc . Consulte el historial para obtener respuestas anteriores (desactualizadas).

¿Qué hace exactamente setkey(DT, a, b)?

Hace dos cosas:

  1. reordena las filas de la tabla DT de datos por la (s) columna (s) proporcionada ( a , b ) por referencia , siempre en orden creciente .
  2. marca esas columnas como columnas clave estableciendo un atributo llamado sorteda DT.

El reordenamiento es rápido (debido a la ordenación de base interna de data.table ) y eficiente en memoria (solo se asigna una columna adicional de tipo double ).

¿Cuándo se setkey()requiere?

Para las operaciones de agrupación, setkey()nunca fue un requisito absoluto. Es decir, podemos realizar un cold-by o 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

Sin embargo, antes de v1.9.6, las uniones del formulario x[i]deben keyconfigurarse x. Con el nuevo on=argumento de v1.9.6 + , esto ya no es cierto y, por lo tanto, la configuración de claves tampoco es un requisito absoluto aquí.

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

Tenga en cuenta que el on=argumento también se puede especificar explícitamente incluso para las keyedcombinaciones.

La única operación que keydebe configurarse absolutamente es la función foverlaps () . Pero estamos trabajando en algunas funciones más que, una vez terminadas, eliminarían este requisito.

  • Entonces, ¿cuál es la razón para implementar el on=argumento?

    Hay bastantes razones.

    1. Permite distinguir claramente la operación como una operación que involucra dos tablas de datos . El solo hecho X[Y]no distingue esto también, aunque podría quedar claro al nombrar las variables de manera apropiada.

    2. También permite comprender las columnas en las que se está realizando la combinación / subconjunto inmediatamente al mirar esa línea de código (y no tener que rastrear hasta la setkey()línea correspondiente ).

    3. En las operaciones en las que las columnas se agregan o actualizan por referencia , las on=operaciones son mucho más eficaces, ya que no es necesario reordenar toda la tabla de datos solo para agregar / actualizar columnas. Por ejemplo,

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

      En el segundo caso, no tuvimos que reordenar. No es calcular el orden lo que lleva mucho tiempo, sino reordenar físicamente la tabla de datos en la RAM y, al evitarlo, mantenemos el orden original y también funciona.

    4. Incluso de lo contrario, a menos que esté realizando uniones repetitivamente, no debería haber una diferencia de rendimiento notable entre uniones con clave y ad-hoc .

Esto lleva a la pregunta, ¿qué ventaja tiene ya la introducción de datos en una tabla de datos ?

  • ¿Hay alguna ventaja en teclear una tabla de datos?

    Al escribir una tabla de datos, se reordena físicamente en función de esas columnas en la RAM. Calcular el pedido no suele ser la parte que requiere mucho tiempo, sino el reordenamiento en sí. Sin embargo, una vez que tenemos los datos ordenados en la RAM, las filas que pertenecen al mismo grupo son todas contiguas en la RAM y, por lo tanto, es muy eficiente en caché. Es la ordenación lo que acelera las operaciones en tablas de datos con clave.

    Por lo tanto, es esencial averiguar si el tiempo dedicado a reordenar toda la tabla de datos vale la pena para realizar una combinación / agregación eficiente en caché. Por lo general, a menos que se realicen operaciones repetitivas de agrupación / unión en la misma tabla de datos con clave, no debería haber una diferencia notable.

En la mayoría de los casos, por lo tanto, ya no debería ser necesario configurar claves. Recomendamos usarlo on=siempre que sea posible, a menos que la clave de configuración tenga una mejora dramática en el rendimiento que le gustaría explotar.

Pregunta: ¿Cómo crees que sería el rendimiento en comparación con una combinación con clave , si utilizas setorder()para reordenar el data.table y el uso on=? Si lo ha seguido hasta ahora, debería poder averiguarlo :-).

Arun
fuente
3
¡Genial, gracias! Hasta ahora, no había pensado en lo que realmente significaba "búsqueda binaria", ni había entendido realmente la razón por la que se usaba en lugar de un hash.
Frank
@Arun, ¿es DT[J(1e4:1e5)]realmente equivalente a DF[DF$x > 1e4 & DF$x < 1e5, ]? ¿Podrías indicarme qué Jsignifica? Además, esa búsqueda no devolvería ninguna fila, ya sample(1e4, 1e7, TRUE)que no incluye números superiores a 1e4.
Pecera
@fishtank, en este caso, debería ser >=y <=- arreglado. J(y .) son alias de list(es decir, son equivalentes). Internamente, cuando ies una lista, se convierte en una tabla de datos después de la cual se usa la búsqueda binaria para calcular índices de fila. Fijo 1e4para 1e5evitar confusiones. Gracias por avistar. Tenga en cuenta que ahora podemos usar el on=argumento directamente para realizar subconjuntos binarios en lugar de configurar la clave. Lea más sobre las nuevas viñetas HTML . Y esté atento a esa página para ver las viñetas de las uniones.
Arun
¿Quizás esto podría ser una actualización más completa? la sección "cuando sea necesario" parece obsoleta, por ejemplo
MichaelChirico
¿Qué función le dice la clave que se está utilizando?
skan
20

Una clave es básicamente un índice en un conjunto de datos, que permite operaciones de clasificación, filtrado y unión muy rápidas y eficientes. Estas son probablemente las mejores razones para usar tablas de datos en lugar de marcos de datos (la sintaxis para usar tablas de datos también es mucho más fácil de usar, pero eso no tiene nada que ver con las claves).

Si no comprende los índices, considere lo siguiente: una guía telefónica se "indexa" por nombre. Entonces, si quiero buscar el número de teléfono de alguien, es bastante sencillo. Pero supongamos que quiero buscar por número de teléfono (por ejemplo, buscar quién tiene un número de teléfono en particular). A menos que pueda "volver a indexar" la guía telefónica por número de teléfono, tomará mucho tiempo.

Considere el siguiente ejemplo: suponga que tengo una tabla, ZIP, de todos los códigos postales en los EE. UU. (> 33,000) junto con la información asociada (ciudad, estado, población, ingreso medio, etc.). Si quiero buscar la información de un código postal específico, la búsqueda (filtro) es aproximadamente 1000 veces más rápida si setkey(ZIP,zipcode)primero.

Otro beneficio tiene que ver con las combinaciones. Suponga que tiene una lista de personas y sus códigos postales en una tabla de datos (llámelo "PPL"), y quiero agregar información de la tabla ZIP (por ejemplo, ciudad, estado, etc.). El siguiente código lo hará:

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

Esta es una "combinación" en el sentido de que estoy uniendo la información de 2 tablas basadas en un campo común (código postal). Uniones como esta en tablas muy grandes son extremadamente lentas con marcos de datos y extremadamente rápidas con tablas de datos. En un ejemplo de la vida real, tuve que hacer más de 20.000 uniones como esta en una tabla completa de códigos postales. Con las tablas de datos, el guión tardó unos 20 minutos. correr. Ni siquiera lo probé con marcos de datos porque me habría llevado más de 2 semanas.

En mi humilde opinión, no solo deberías leer sino estudiar las preguntas frecuentes y el material de introducción. Es más fácil de entender si tiene un problema real al que aplicar esto.

[Respuesta al comentario de @ Frank]

Re: clasificación vs. indexación - Basado en la respuesta a esta pregunta , parece que setkey(...)de hecho reordena las columnas en la tabla (por ejemplo, una clasificación física) y no crea un índice en el sentido de la base de datos. Esto tiene algunas implicaciones prácticas: por un lado, si configura la clave en una tabla con setkey(...)y luego cambia cualquiera de los valores en la columna clave, data.table simplemente declara que la tabla ya no está ordenada (desactivando el sortedatributo); lo hace no dinámicamente re-índice para mantener el orden adecuado (como sucedería en una base de datos). Además, "la eliminación de la clave" utilizando setky(DT,NULL)no no restaurar la tabla a la misma del orden original, sin clasificar.

Re: filtrar frente a unir : la diferencia práctica es que el filtrado extrae un subconjunto de un solo conjunto de datos, mientras que unir combina datos de dos conjuntos de datos basados ​​en un campo común. Hay muchos tipos diferentes de combinación (interna, externa, izquierda). El ejemplo anterior es una combinación interna (solo se devuelven los registros con claves comunes a ambas tablas), y esto tiene muchas similitudes con el filtrado.

jlhoward
fuente
1
+1. Respecto a tu primera oración ... ya está ordenada ¿no? ¿Y no es una combinación un caso especial de filtro (o una operación que toma el filtrado como primer paso)? Parece que un "mejor filtrado" resume todo el beneficio.
Frank
1
O mejor escaneando, supongo.
Wet Feet
1
@jlhoward Gracias. Mi creencia anterior era que ordenar no estaba entre los beneficios de establecer la clave (ya que si desea ordenar, simplemente debe ordenar), y también que en setkeyrealidad reordena las filas de manera irreversible. Si es solo para fines de visualización, ¿cómo imprimo las primeras diez filas de acuerdo con el orden "verdadero" (que habría visto antes de setkey)? Estoy bastante seguro de setkey(DT,NULL)que no hace esto ... (cont.)
Frank
... (cont.) Además, no he mirado el código del paquete, pero para unirse X[Y,...], necesita "filtrar" las filas de X usando la clave. Por supuesto, otras cosas suceden después de eso (las columnas de Y están disponibles y hay un by-without-by implícito), pero todavía no veo eso como un beneficio conceptualmente distinto. Sin embargo, supongo que su respuesta se expresa en términos de operaciones que podría querer hacer, donde la distinción puede ser útil.
Frank
1
@Frank: setkey(DT,NULL)elimina la clave pero no afecta el orden de clasificación. Hizo una pregunta sobre esto aquí . Veamos.
jlhoward