Qué significa .SD en data.table en R

.SD parece útil, pero realmente no sé lo que estoy haciendo con él. Que significa? ¿Por qué hay un período anterior (punto)? ¿Qué está pasando cuando lo uso?

Leí: .SD es una data.table contiene el subconjunto de datos de x para cada grupo, excluyendo la (s) columna (s) de grupo. Se puede usar cuando se agrupa por i , cuando se agrupa por by , keyed by y _ad hoc_

¿ data.table significa que la hija data.table s se data.table en la memoria para la siguiente operación?

.SD significa algo así como ” S ubset of D ata.table”. No hay importancia para la inicial "." , excepto que hace aún más improbable que haya un choque con un nombre de columna definido por el usuario.

Si esta es su tabla de datos:

 DT = data.table(x=rep(c("a","b","c"),each=2), y=c(1,3), v=1:6) setkey(DT, y) DT # xyv # 1: a 1 1 # 2: b 1 3 # 3: c 1 5 # 4: a 3 2 # 5: b 3 4 # 6: c 3 6 

Hacer esto puede ayudarte a ver qué es .SD :

 DT[ , .SD[ , paste(x, v, sep="", collapse="_")], by=y] # y V1 # 1: 1 a1_b3_c5 # 2: 3 a2_b4_c6 

Básicamente, la sentencia by=y divide la tabla de datos original en estos dos data.tables

 DT[ , print(.SD), by=y] # <1st sub-data.table, called '.SD' while it's being operated on> # xv # 1: a 1 # 2: b 3 # 3: c 5 # <2nd sub-data.table, ALSO called '.SD' while it's being operated on> # xv # 1: a 2 # 2: b 4 # 3: c 6 #  # Empty data.table (0 rows) of 1 col: y 

y opera en ellos a su vez.

Mientras está funcionando en cualquiera de los dos, le permite referirse a la data.table de data.table actual utilizando el nick-name / handle / symbol .SD . Eso es muy útil, ya que puede acceder y operar en las columnas como si estuviera sentado en la línea de comando trabajando con una única tabla de datos llamada .SD … excepto que aquí, data.table realizará esas operaciones en cada una única data.table definida por combinaciones de la clave, “pegándolas” juntas y devolviendo los resultados en una única data.table .

Teniendo en cuenta la frecuencia con la que esto sucede, creo que esto justifica un poco más de exposición, más allá de la respuesta útil dada por Josh O’Brien anteriormente.

Además de la extensión del acrónimo D ata usualmente citado / creado por Josh, creo que también es útil considerar la “S” para representar “Selfsame” o “Self-reference” – .SD está en su forma más básica representar una referencia reflexiva a la data.table sí, como veremos en los ejemplos a continuación, esto es particularmente útil para encadenar “consultas” (extracciones / subconjuntos / etc usando [ ). En particular, esto también significa que .SD es en sí mismo una data.table (con la advertencia de que no permite la asignación con := ).

El uso más simple de .SD es para subconjuntos de columna (es decir, cuando se especifica .SDcols ); Creo que esta versión es mucho más sencilla de entender, por lo que la abordaremos a continuación. La interpretación de .SD en su segundo uso, agrupando escenarios (es decir, cuando se especifica by = o keyby = ), es ligeramente diferente, conceptualmente (aunque en el núcleo es el mismo, ya que, después de todo, una operación no agrupada es una borde caso de agrupación con solo un grupo).


Aquí hay algunos ejemplos ilustrativos y algunos otros ejemplos de usos que yo mismo implemento a menudo:

Cargando datos de Lahman

Para darle una sensación más real, en lugar de inventar datos, carguemos algunos conjuntos de datos sobre béisbol de Lahman :

 library(data.table) library(magrittr) # some piping can be beautiful library(Lahman) Teams = as.data.table(Teams) # *I'm selectively suppressing the printed output of tables here* Teams Pitching = as.data.table(Pitching) # subset for conciseness Pitching = Pitching[ , .(playerID, yearID, teamID, W, L, G, ERA)] Pitching 

Naked .SD

Para ilustrar lo que quiero decir sobre la naturaleza reflexiva de .SD , considere su uso más banal:

 Pitching[ , .SD] # playerID yearID teamID WLG ERA # 1: bechtge01 1871 PH1 1 2 3 7.96 # 2: brainas01 1871 WS3 12 15 30 4.50 # 3: fergubo01 1871 NY2 0 0 1 27.00 # 4: fishech01 1871 RC1 4 16 24 4.35 # 5: fleetfr01 1871 NY2 0 1 1 10.00 # --- # 44959: zastrro01 2016 CHN 1 0 8 1.13 # 44960: zieglbr01 2016 ARI 2 3 36 2.82 # 44961: zieglbr01 2016 BOS 2 4 33 1.52 # 44962: zimmejo02 2016 DET 9 7 19 4.87 # 44963: zychto01 2016 SEA 1 0 12 3.29 

Es decir, acabamos de regresar Pitching , es decir, esta era una forma excesivamente detallada de escribir Pitching o Pitching[] :

 identical(Pitching, Pitching[ , .SD]) # [1] TRUE 

En términos de subconjunto, .SD sigue siendo un subconjunto de los datos, es simplemente trivial (el conjunto mismo).

Subconjunto de columnas: .SDcols

La primera forma de impactar lo que es .SD es limitar las columnas contenidas en .SD usando el argumento .SDcols a [ :

 Pitching[ , .SD, .SDcols = c('W', 'L', 'G')] # WLG # 1: 1 2 3 # 2: 12 15 30 # 3: 0 0 1 # 4: 4 16 24 # 5: 0 1 1 # --- # 44959: 1 0 8 # 44960: 2 3 36 # 44961: 2 4 33 # 44962: 9 7 19 # 44963: 1 0 12 

Esto es solo para ilustración y fue bastante aburrido. Pero incluso este simple uso se presta a una amplia variedad de operaciones de manipulación de datos altamente beneficiosas / ubicuas:

Conversión de tipo de columna

La conversión de tipo de columna es una realidad para el borrado de datos: al escribir esto, fwrite no puede leer automáticamente las columnas de Date o POSIXct , y las conversiones entre character / factor / numeric son comunes. Podemos usar .SD y .SDcols para convertir por lotes grupos de tales columnas.

Observamos que las siguientes columnas se almacenan como character en el conjunto de datos de Teams :

 # see ?Teams for explanation; these are various IDs # used to identify the multitude of teams from # across the long history of baseball fkt = c('teamIDBR', 'teamIDlahman45', 'teamIDretro') # confirm that they're stored as `character` Teams[ , sapply(.SD, is.character), .SDcols = fkt] # teamIDBR teamIDlahman45 teamIDretro # TRUE TRUE TRUE 

Si está confundido por el uso de sapply aquí, tenga en cuenta que es el mismo que para los data.frames base R:

 setDF(Teams) # convert to data.frame for illustration sapply(Teams[ , fkt], is.character) # teamIDBR teamIDlahman45 teamIDretro # TRUE TRUE TRUE setDT(Teams) # convert back to data.table 

La clave para entender esta syntax es recordar que un data.table (así como un data.frame ) se puede considerar como una list donde cada elemento es una columna, por lo tanto, sapply / lapply aplica FUN a cada columna y devuelve el el resultado como sapply / lapply generalmente sería (aquí, FUN == is.character devuelve un valor logical de longitud 1, por sapply devuelve un vector).

La syntax para convertir estas columnas en factor es muy similar: simplemente agregue el operador := asignación

 Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt] 

Tenga en cuenta que debemos envolver fkt entre paréntesis () para forzar a R a interpretar esto como nombres de columna, en lugar de intentar asignar el nombre fkt al RHS.

La flexibilidad de .SDcols (y := ) para aceptar un vector de character o un vector integer de posiciones de columna también puede ser útil para la conversión basada en patrones de nombres de columna *. Podríamos convertir todas las columnas de factor a character :

 fkt_idx = which(sapply(Teams, is.factor)) Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx] 

Y luego convierta todas las columnas que contienen team a factor :

 team_idx = grep('team', names(Teams), value = TRUE) Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx] 

** La utilización explícita de los números de columna (como DT[ , (1) := rnorm(.N)] ) es una mala práctica y puede conducir a un código DT[ , (1) := rnorm(.N)] en el tiempo si las posiciones de las columnas cambian. Incluso el uso implícito de números puede ser peligroso si no mantenemos un control inteligente / estricto sobre el orden de cuándo creamos el índice numerado y cuándo lo usamos.

Controlando el RHS de un modelo

La especificación variable del modelo es una característica central del análisis estadístico robusto. Probemos y predijamos el ERA de un lanzador (Promedio de carreras ganadas, una medida del rendimiento) usando el conjunto pequeño de covariables disponibles en la tabla de Pitching . ¿Cómo varía la relación (lineal) entre W (victorias) y ERA dependiendo de qué otras covariables se incluyen en la especificación?

Aquí hay un pequeño script que aprovecha el poder de .SD que explora esta pregunta:

 # this generates a list of the 2^k possible extra variables # for models of the form ERA ~ G + (...) extra_var = c('yearID', 'teamID', 'G', 'L') models = lapply(0L:length(extra_var), combn, x = extra_var, simplify = FALSE) %>% unlist(recursive = FALSE) # here are 16 visually distinct colors, taken from the list of 20 here: # https://sashat.me/2017/01/11/list-of-20-simple-distinct-colors/ col16 = c('#e6194b', '#3cb44b', '#ffe119', '#0082c8', '#f58231', '#911eb4', '#46f0f0', '#f032e6', '#d2f53c', '#fabebe', '#008080', '#e6beff', '#aa6e28', '#fffac8', '#800000', '#aaffc3') par(oma = c(2, 0, 0, 0)) sapply(models, function(rhs) { # using ERA ~ . and data = .SD, then varying which # columns are included in .SD allows us to perform this # iteration over 16 models succinctly. # coef(.)['W'] extracts the W coefficient from each model fit Pitching[ , coef(lm(ERA ~ ., data = .SD))['W'], .SDcols = c('W', rhs)] }) %>% barplot(names.arg = sapply(models, paste, collapse = '/'), main = 'Wins Coefficient with Various Covariates', col = col16, las = 2L, cex.names = .8) 

ajuste el coeficiente OLS en W, varias especificaciones

El coeficiente siempre tiene el signo esperado (mejores lanzadores tienden a tener más victorias y menos carreras permitidas), pero la magnitud puede variar sustancialmente dependiendo de qué más controlemos.

Uniones condicionales

data.table syntax data.table es hermosa por su simplicidad y robustez. La syntax x[i] maneja flexiblemente dos enfoques comunes para subconjunto: cuando i es un vector logical , x[i] devolverá las filas de x correspondientes a donde i es TRUE ; cuando i data.table otra data.table , se realiza una join (en la forma simple, usando las key s de x , de lo contrario, cuando está especificado on = , se usan coincidencias de esas columnas).

Esto es genial en general, pero se queda corto cuando deseamos realizar una unión condicional , donde la naturaleza exacta de la relación entre las tablas depende de algunas características de las filas en una o más columnas.

Este ejemplo es un poco artificial, pero ilustra la idea; mira aquí ( 1 , 2 ) para más.

El objective es agregar una columna team_performance a la tabla de Pitching que registra el rendimiento (rango) del equipo del mejor lanzador de cada equipo (medido por el ERA más bajo, entre los lanzadores con al menos 6 juegos registrados).

 # to exclude pitchers with exceptional performance in a few games, # subset first; then define rank of pitchers within their team each year # (in general, we should put more care into the 'ties.method' Pitching[G > 5, rank_in_team := frank(ERA), by = .(teamID, yearID)] Pitching[rank_in_team == 1, team_performance := # this should work without needing copy(); # that it doesn't appears to be a bug: # https://github.com/Rdatatable/data.table/issues/1926 Teams[copy(.SD), Rank, .(teamID, yearID)]] 

Tenga en cuenta que la syntax x[y] devuelve nrow(y) , por lo que .SD está a la derecha en Teams[.SD] (ya que la RHS de := en este caso requiere nrow(Pitching[rank_in_team == 1]) valores.

.SD agrupadas .SD

A menudo, nos gustaría realizar alguna operación en nuestros datos a nivel grupal . Cuando especificamos by = (o keyby = ), el modelo mental de lo que ocurre cuando data.table procesa j es pensar que su data.table está dividida en muchas sub- data.table componentes, cada una de las cuales corresponde a un valor único de su by variable (s):

agrupamiento ilustrado

En este caso, .SD es de naturaleza múltiple; se refiere a cada una de estas data.table , una a la vez (un poco más exactamente, el scope de .SD es un solo data.table ) Esto nos permite express concisamente una operación que nos gustaría realizar en cada data.table antes de que el resultado data.table se nos devuelva.

Esto es útil en una variedad de configuraciones, las más comunes de las cuales se presentan aquí:

Subconjunto grupal

Consigamos la temporada más reciente de datos para cada equipo en los datos de Lahman. Esto puede hacerse simplemente con:

 # the data is already sorted by year; if it weren't # we could do Teams[order(yearID), .SD[.N], by = teamID] Teams[ , .SD[.N], by = teamID] 

Recuerde que .SD es en sí misma una data.table , y que .N refiere al número total de filas en un grupo (es igual a nrow(.SD) dentro de cada grupo), entonces .SD[.N] devuelve la totalidad de .SD para la última fila asociada a cada teamID .

Otra versión común de esto es usar .SD[1L] lugar para obtener la primera observación para cada grupo.

Grupo Optima

Supongamos que queremos devolver el mejor año para cada equipo, medido por el número total de carreras anotadas ( R ; podríamos ajustarlo fácilmente para referirnos a otras métricas, por supuesto). En lugar de tomar un elemento fijo de cada data.table , ahora definimos el índice deseado dinámicamente de la siguiente manera:

 Teams[ , .SD[which.max(R)], by = teamID] 

Tenga en cuenta que este enfoque, por supuesto, se puede combinar con .SDcols para devolver solo partes de la data.table de data.table para cada .SD (con la advertencia de que .SDcols debe ser reparado en los diversos subconjuntos)

NB : .SD[1L] está actualmente optimizado por GForce ( ver también ), data.table internos de data.table que aceleran masivamente las operaciones agrupadas más comunes como sum o mean – vea ?GForce para más detalles y esté atento / soporte de voz para solicitudes de mejoras de características para actualizaciones en este frente: 1 , 2 , 3 , 4 , 5 , 6

Regresión agrupada

Volviendo a la pregunta anterior sobre la relación entre ERA y W , supongamos que esperamos que esta relación difiera según el equipo (es decir, hay una pendiente diferente para cada equipo). Podemos volver a ejecutar fácilmente esta regresión para explorar la heterogeneidad en esta relación de la siguiente manera (teniendo en cuenta que los errores estándar de este enfoque son generalmente incorrectos; la especificación ERA ~ W*teamID será mejor; este enfoque es más fácil de leer y los coeficientes están bien):

 # use the .N > 20 filter to exclude teams with few observations Pitching[ , if (.N > 20) .(w_coef = coef(lm(ERA ~ W))['W']), by = teamID ][ , hist(w_coef, 20, xlab = 'Fitted Coefficient on W', ylab = 'Number of Teams', col = 'darkgreen', main = 'Distribution of Team-Level Win Coefficients on ERA')] 

distribución de coeficientes ajustados

Si bien existe una buena cantidad de heterogeneidad, existe una clara concentración en torno al valor total observado

Esperemos que esto haya elucidado el poder de .SD para facilitar código hermoso y eficiente en data.table !