Editar:
Dado lo bien recibida que fue esta respuesta, la he convertido en una viñeta de paquete ahora disponible aquí
Dada la frecuencia con la que esto ocurre, creo que esto justifica un poco más de exposición, más allá de la útil respuesta dada por Josh O'Brien arriba.
Además del S ubset del acrónimo D ata generalmente citado / creado por Josh, creo que también es útil considerar que la "S" significa "Selfsame" o "Self-reference" - .SD
es en su forma más básica un referencia reflexiva a data.table
sí mismo: 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í undata.table
(con la advertencia de que no permite la asignación con:=
).
El uso más simple de .SD
es para subconjuntos de columnas (es decir, cuándo .SDcols
se especifica); Creo que esta versión es mucho más sencilla de entender, por lo que cubriremos eso primero a continuación. La interpretación de .SD
en su segundo uso, los escenarios de agrupación (es decir, cuándo by =
o keyby =
se especifica), es ligeramente diferente, conceptualmente (aunque en el fondo es lo mismo, ya que, después de todo, una operación no agrupada es un caso extremo 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 a esto una sensación más real, en lugar de inventar datos, carguemos algunos conjuntos de datos sobre el 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
Desnudo .SD
Para ilustrar lo que quiero decir sobre la naturaleza reflexiva de .SD
, considere su uso más banal:
Pitching[ , .SD]
# playerID yearID teamID W L G 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 solo trivial (el conjunto en sí).
Subconjunto de columnas: .SDcols
La primera forma de afectar lo que .SD
es es limitar las columnas contenidas en el .SD
uso del .SDcols
argumento para [
:
Pitching[ , .SD, .SDcols = c('W', 'L', 'G')]
# W L G
# 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 del tipo de columna es una realidad para la mezcla de datos: a partir de este escrito, fwrite
no se puede leer automáticamente Date
o POSIXct
columnas , y las conversiones de ida y vuelta entre character
/ factor
/ numeric
son comunes. Podemos usar .SD
y .SDcols
convertir por lotes grupos de tales columnas.
Notamos que las siguientes columnas se almacenan como character
en el Teams
conjunto de datos:
# 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 lo mismo que para la base R data.frames
:
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 comprender esta sintaxis es recordar que a data.table
(así como a data.frame
) se puede considerar como un lugar list
donde cada elemento es una columna; por lo tanto, sapply
/ se lapply
aplica FUN
a cada columna y devuelve el resultado como sapply
/ lapply
normalmente lo haría (aquí, FUN == is.character
devuelve un logical
de longitud 1, entoncessapply
devuelve un vector).
La sintaxis para convertir estas columnas factor
es muy similar: simplemente agregue el :=
operador de asignación
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]
Tenga en cuenta que debemos incluir fkt
paréntesis ()
para forzar a R a interpretar esto como nombres de columna, en lugar de intentar asignar el nombrefkt
al RHS.
La flexibilidad de .SDcols
(y :=
) para aceptar un character
vector o un integer
vector 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 factor
columnas 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 de team
nuevo a factor
:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** El uso explícito de números de columna (como DT[ , (1) := rnorm(.N)]
) es una mala práctica y puede conducir a un código silenciosamente dañado con el tiempo si cambian las posiciones de las columnas. 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.
Control de RHS de un modelo
La especificación del modelo variable es una característica central del análisis estadístico robusto. Tratemos de predecir la ERA de un lanzador (Promedio de carreras ganadas, una medida de rendimiento) usando el pequeño conjunto de covariables disponibles en la Pitching
tabla. ¿Cómo funciona la relación (lineal) entre W
(victorias) yERA
dependiendo de qué otras covariables se incluyen en la especificación?
Aquí hay un breve guión que aprovecha el poder del .SD
cual 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)
El coeficiente siempre tiene el signo esperado (los 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
La sintaxis es hermosa por su simplicidad y robustez. La sintaxis x[i]
maneja de manera flexible dos enfoques comunes de subconjunto: cuando i
es un logical
vector, x[i]
devolverá esas filas x
correspondientes a donde i
está TRUE
; cuando i
es otrodata.table
, join
se realiza a (en forma simple, usando la key
s de x
y i
, de lo contrario, cuandoon =
se especifica, usando coincidencias de esas columnas).
Esto es genial en general, pero se queda corto cuando deseamos realizar una unión condicional , en donde la naturaleza exacta de la relación entre 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; ver aquí ( 1 , 2 ) para más.
El objetivo es agregar una columna team_performance
a la Pitching
tabla que registre el desempeño del equipo (rango) del mejor lanzador en cada equipo (medido por la efectividad más baja, 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 x[y]
sintaxis devuelve nrow(y)
valores, razón por la cual .SD
está a la derecha en Teams[.SD]
(ya que el RHS de :=
en este caso requiere nrow(Pitching[rank_in_team == 1])
valores.
.SD
Operaciones agrupadas
A menudo, nos gustaría realizar alguna operación en nuestros datos a nivel de grupo . Cuando especificamos by =
(o keyby =
), el modelo mental de lo que sucede cuando los data.table
procesos j
es pensar data.table
que se divide en muchos subcomponentes data.table
, cada uno de los cuales corresponde a un solo valor de su by
(s) variable (s):
En este caso, .SD
es de naturaleza múltiple: se refiere a cada uno de estos sub data.table
, uno a la vez (un poco más exactamente, el alcance de .SD
un sub- data.table
). Esto nos permite expresar de manera concisa una operación que nos gustaría realizar en cada sub-data.table
antes de que el resultado reensamblado nos sea devuelto.
Esto es útil en una variedad de configuraciones, las más comunes de las cuales se presentan aquí:
Subconjunto de grupo
Obtengamos la temporada de datos más reciente para cada equipo en los datos de Lahman. Esto se puede hacer 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 en .SD
sí mismo es un data.table
, y que se .N
refiere al número total de filas en un grupo (es igual a nrow(.SD)
dentro de cada grupo), por lo que .SD[.N]
devuelve la totalidad de.SD
la fila final asociada a cada uno teamID
.
Otra versión común de esto es usar .SD[1L]
en su 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 su número total de carreras anotadas ( R
por supuesto, podríamos ajustar esto fácilmente para referirnos a otras métricas). En lugar de tomar un elemento fijo de cada sub- data.table
, ahora definimos el índice deseado dinámicamente de la siguiente manera:
Teams[ , .SD[which.max(R)], by = teamID]
Tenga en cuenta que, por supuesto, este enfoque se puede combinar .SDcols
para devolver solo partes de data.table
cada uno .SD
(con la advertencia que .SDcols
debe corregirse en los distintos subconjuntos)
NB : .SD[1L]
actualmente está optimizado por GForce
( vea también ), data.table
componentes internos 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 consulta 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 (observando 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')]
Si bien existe una buena cantidad de heterogeneidad, existe una concentración clara alrededor del valor general observado
¡Esperemos que esto haya aclarado el poder de .SD
facilitar un código hermoso y eficiente data.table
!
?data.table
fue mejorado en v1.7.10, gracias a esta pregunta. Ahora explica el nombre.SD
según la respuesta aceptada.