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

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


Una de las fonts que leí fue ?setkey .

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

Mi punto es que una clave “ordenaría” la tabla de datos, dando como resultado 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 explican:

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

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

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

Debido a que cada grupo es contiguo en la RAM, lo que minimiza las capturas de página, y la memoria se puede copiar a granel ( memcpy en C) en lugar de bucle en C.

A partir de aquí, supongo que al establecer una clave, de alguna manera, R puede usar “ordenamiento de radix” sobre otros algoritmos, y es por eso que es más rápido.


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

  1. Llaves

Comencemos por considerar data.frame, específicamente rownames (o en inglés, nombres de fila). Es decir, los múltiples nombres que pertenecen a una sola fila. Los múltiples nombres que pertenecen a la fila única? 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. Eso es útil para organizar un directorio telefónico, por ejemplo, que está ordenado por apellido, luego primer nombre. Sin embargo, cada fila en un data.frame solo puede tener un nombre.

Una clave consta de una o más columnas de rownames, que pueden ser enteros, factor, carácter o alguna otra clase, no simplemente carácter. Además, las filas están ordenadas por la 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 manera.

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

El directorio telefónico fue útil para entender qué es una clave, pero parece que una clave no es diferente cuando se compara con 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 establecerla como clave. Además, parece que en una tabla de datos con tiempo como columna, configurar cualquier otra columna como clave probablemente también arruinaría la columna de tiempo, lo que lo hace aún más confuso ya que no sé si estoy autorizado a configurar cualquier otra columna como llave. Alguien puede iluminarme, por favor?

Actualización menor: consulte las nuevas viñetas HTML también. Este problema resalta las otras viñetas que tenemos planeado.


He actualizado esta respuesta nuevamente (Feb 2016) a la luz de la nueva función on= que también permite combinaciones ad-hoc . Vea el historial de respuestas anteriores (desactualizadas).

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

Hace dos cosas:

  1. reordena las filas del DT datos.tabla por la (s) columna (s) provistas ( a , b ) por referencia , siempre en orden creciente .
  2. marca esas columnas como columnas clave al establecer un atributo llamado sorted a DT .

El reordenamiento es rápido (debido a la clasificación de radix interna de data.table ) y eficiente desde la memoria (solo se asigna una columna extra de tipo double ).

¿Cuándo se setkey() ?

Para operaciones de agrupación, setkey() nunca fue un requisito absoluto. Es decir, podemos hacer 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 de la forma x[i] requerían que la key se estableciera en x . Con el nuevo argumento on= desde v1.9.6 + , esto ya no es cierto, y establecer claves no 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 on= cuenta que on= argumento se puede especificar explícitamente incluso para combinaciones con keyed .

La única operación que requiere que la key esté configurada por completo es la función foverlaps () . Pero estamos trabajando en algunas características más que, cuando terminen, eliminarán este requisito.

  • Entonces, ¿cuál es el motivo para implementar on= argument?

    Hay bastantes razones.

    1. Permite distinguir claramente la operación como una operación que involucra dos data.tables . El simple hecho de X[Y] tampoco lo distingue, aunque podría estar claro al nombrar las variables de manera apropiada.

    2. También permite comprender las columnas sobre las que se realiza el join / subconjunto de forma inmediata mirando esa línea de código (y sin tener que volver a la línea setkey() correspondiente).

    3. En las operaciones donde las columnas se agregan o actualizan por referencia , on= operaciones son mucho más efectivas ya que no es necesario que todo el data.table se reordene solo para agregar / actualizar columna (s). 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 se trata de calcular el orden que lleva mucho tiempo, sino de reordenar físicamente la tabla de datos en la memoria RAM y, al evitarla, conservamos el orden original y también es un rendimiento.

    4. Incluso de lo contrario, a menos que esté realizando uniones repetitivas, no debería haber una diferencia de rendimiento notable entre uniones con y sin adjetivo .

Esto nos lleva a la pregunta: ¿qué ventaja tiene tener una clave de data.table ?

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

    Al escribir una tabla de datos, se vuelve a ordenar físicamente en función de esas columna (s) en la RAM. Computar el orden no suele ser la parte que consume mucho tiempo, sino más bien el reordenamiento . Sin embargo, una vez que tenemos los datos ordenados en RAM, las filas que pertenecen al mismo grupo son todas contiguas en la RAM y, por lo tanto, son muy eficientes en la caché. Es la clasificación lo que acelera las operaciones en data.tables con clave.

    Por lo tanto, es esencial averiguar si el tiempo dedicado a reordenar todo el data.table vale la pena el tiempo para hacer un join / aggregation 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 haber necesidad de establecer claves. Recomendamos usar on= siempre que sea posible, a menos que la tecla de configuración tenga una mejora dramática en el rendimiento que le gustaría explotar.

Pregunta: ¿Cuál cree que sería el rendimiento en comparación con una combinación con clave , si usa setorder() para reordenar data.table y usar on= ? Si has seguido hasta ahora, deberías ser capaz de resolverlo :-).

Una clave es básicamente un índice en un conjunto de datos, que permite operaciones de ordenació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 syntax para usar tablas de datos también es mucho más amigable para el usuario, pero eso no tiene nada que ver con las claves).

Si no entiende los índices, considere esto: una guía telefónica está “indexada” por su 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 particular). A menos que pueda “volver a indexar” la guía telefónica por número de teléfono, llevará mucho tiempo.

Considere el siguiente ejemplo: supongamos que tengo una tabla, ZIP, de todos los códigos postales de 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) .

Otro beneficio tiene que ver con las uniones. Supongamos que tenemos una lista de personas y sus códigos postales en una tabla de datos (llámala “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 "unión" en el sentido de que estoy uniendo la información de 2 tablas basadas en un campo común (código postal). Se une así en tablas muy grandes que 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 tablas de datos, el script duró unos 20 min. correr. Ni siquiera lo intenté con marcos de datos porque me habría llevado más de 2 semanas.

En mi humilde opinión, no solo debe leer, sino estudiar las preguntas frecuentes y el material de introducción. Es más fácil comprender si tiene un problema real para aplicar esto.

[Respuesta al comentario de @ Frank]

Re: ordenamiento vs. indización - En función de la respuesta a esta pregunta , parece que setkey(...) reorganiza las columnas en la tabla (por ejemplo, una clasificación física) y no crea un índice en la base de datos sentido. 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 de clave, data.table simplemente declara que la tabla ya no está ordenada (por desactivar el atributo sorted ); no vuelve a indexarse ​​dinámicamente para mantener el orden correcto (como ocurriría en una base de datos). Además, "quitar la clave" usando setky(DT,NULL) no restaura la tabla a su orden original sin ordenar.

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