Hola, tenemos la respuesta a tu interrogante, has scroll y la verás aquí.
Solución:
.SD
representa algo como “S
conjunto de D
ata.table “. No hay ningún significado para la inicial "."
, excepto que hace que sea aún más improbable que haya un conflicto 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
# x y v
# 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 ver qué .SD
es:
DT[ , .SD[ , paste(x, v, sep="", collapse="_")], by=y]
# y V1
# 1: 1 a1_b3_c5
# 2: 3 a2_b4_c6
Básicamente, el by=y
declaración divide la tabla de datos original en estos dos sub-data.tables
DT[ , print(.SD), by=y]
# <1st sub-data.table, called '.SD' while it's being operated on>
# x v
# 1: a 1
# 2: b 3
# 3: c 5
# <2nd sub-data.table, ALSO called '.SD' while it's being operated on>
# x v
# 1: a 2
# 2: b 4
# 3: c 6
#
# Empty data.table (0 rows) of 1 col: y
y opera sobre ellos a su vez.
Mientras está operando en cualquiera de ellos, le permite referirse al sub-data.table
usando el apodo / identificador / símbolo .SD
. Eso es muy útil, ya que puede acceder y operar en las columnas como si estuviera sentado en la línea de comandos trabajando con una única tabla de datos llamada .SD
… excepto que aquí, data.table
llevará a cabo esas operaciones en cada uno de los sub-data.table
definido por combinaciones de los key, “pegándolos” de nuevo y devolviendo los resultados en una sola data.table
!
Editar:
Dado lo bien recibida que fue esta respuesta, la convertí en una viñeta de paquete que ahora está disponible aquí
Dada la frecuencia con la que surge esto, creo que esto merece un poco más de exposición, más allá de la útil respuesta dada por Josh O’Brien anteriormente.
Además de Subset del Dcomo un acrónimo que usualmente cita / crea 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 al data.table
en sí mismo, como veremos en los ejemplos a continuación, esto es particularmente útil para encadenar “consultas” (extracciones / subconjuntos / etc. [
). In particular, this also means that .SD
is itself a data.table
(with the caveat that it does not allow assignment with :=
).
The simpler usage of .SD
is for column subsetting (i.e., when .SDcols
is specified); I think this version is much more straightforward to understand, so we’ll cover that first below. The interpretation of .SD
in its second usage, grouping scenarios (i.e., when by =
or keyby =
is specified), is slightly different, conceptually (though at core it’s the same, since, after all, a non-grouped operation is an edge case of grouping with just one group).
Here are some illustrative examples and some other examples of usages that I myself implement often:
Loading Lahman Data
To give this a more real-world feel, rather than making up data, let’s load some data sets about baseball from 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)]
Cabeceo
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 demasiado 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 uno trivial (el conjunto en sí).
Subconjunto de columnas: .SDcols
La primera forma de impactar lo que .SD
es limitar el columnas contenida en .SD
utilizando el .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 manipulación de datos: al momento de escribir este artículo, fwrite
no puede leer automáticamente Date
o POSIXct
columnas y conversiones de ida y vuelta entre character
/factor
/numeric
son comunes. Nosotros podemos usar .SD
y .SDcols
para convertir por lotes grupos de dichas columnas.
Observamos 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
los key comprender esta sintaxis es recordar que un data.table
(así como un data.frame
) puede considerarse como un list
donde cada elemento es una columna, por lo tanto, sapply
/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, entonces sapply
devuelve un vector).
La sintaxis para convertir estas columnas a factor
es muy similar, simplemente agregue el :=
operador de 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
a la derecha.
La flexibilidad de .SDcols
(y :=
) para aceptar un character
vector o un integer
El vector de posiciones de columna también puede resultar útil para la conversión basada en patrones de nombres de columna *. Podríamos convertir todo 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 team
de regreso factor
:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** Explícitamente usando números de columna (como DT[ , (1) := rnorm(.N)]
) es una mala práctica y puede provocar que el código se corrompa silenciosamente 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.
Controlar el RHS de un modelo
La especificación del modelo variable es una característica fundamental de un análisis estadístico sólido. Intentemos predecir la efectividad de un lanzador (promedio de carreras ganadas, una medida de desempeño) usando el pequeño conjunto de covariables disponibles en el Pitching
mesa. ¿Cómo funciona la relación (lineal) entre W
(gana) y ERA
varían dependiendo de qué otras covariables se incluyen en la especificación?
Aquí hay un breve guión 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)
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 para subconjuntos: cuando i
es un logical
vector, x[i]
devolverá esas filas de x
correspondiente a donde i
es TRUE
; cuando i
es otro data.table
, a join
se realiza (en la forma simple, utilizando el key
s de x
y i
, de lo contrario, cuando on =
se especifica, utilizando coincidencias de esas columnas).
Esto es genial en general, pero se queda corto cuando deseamos realizar un unión condicional, 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; consulte aquí (1, 2) para obtener más información.
El objetivo es agregar una columna team_performance
al Pitching
tabla que registra el desempeño del equipo (rango) del mejor lanzador de cada equipo (medido por la efectividad más baja, entre 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 el x[y]
devoluciones de sintaxis nrow(y)
valores, por eso .SD
está a la derecha en Teams[.SD]
(desde el RHS de :=
en este caso requiere nrow(Pitching[rank_in_team == 1])
valores.
Agrupados .SD
operaciones
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 data.table
procesos j
es pensar en tu data.table
como dividido en muchos subcomponentesdata.table
s, cada uno de los cuales corresponde a un único valor de su by
variable (s):
En este caso, .SD
es de naturaleza múltiple – se refiere a cada uno de estos sub-data.table
s, uno a la vez (un poco más exactamente, el alcance de .SD
es un solo sub-data.table
). Esto nos permite expresar de forma concisa una operación que nos gustaría realizar en cada subdata.table
antes de que se nos devuelva el resultado reensamblado.
Esto es útil en una variedad de configuraciones, las más comunes de las cuales se presentan aquí:
Subconjunto de grupo
Obtengamos los datos de la temporada más reciente para cada equipo en los datos de Lahman. Esto se puede hacer de manera bastante simple 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]
Recordar que .SD
es en sí mismo un data.table
, y eso .N
se 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 el totalidad de .SD
para la última fila asociada con cada teamID
.
Otra versión común de esto es usar .SD[1L]
en lugar de conseguir el primero observación para cada grupo.
Grupo Optima
Supongamos que quisiéramos devolver el mejor año para cada equipo, medido por el número total de carreras anotadas (R
; podríamos ajustar fácilmente esto para hacer referencia a otras métricas, por supuesto). En lugar de tomar un reparado elemento de cada sub-data.table
, ahora definimos el índice deseado dinamicamente como sigue:
Teams[ , .SD[which.max(R)], by = teamID]
Tenga en cuenta que, por supuesto, este enfoque se puede combinar con .SDcols
para devolver solo porciones del data.table
para cada .SD
(con la salvedad de que .SDcols
debe fijarse en los distintos subconjuntos)
nótese bien: .SD[1L]
actualmente está optimizado por GForce
(ver también), data.table
internos que aceleran enormemente las operaciones agrupadas más comunes como sum
o mean
— ver ?GForce
para obtener más detalles y estar atento a / soporte de voz para solicitudes de mejora de funciones 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 por 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 el 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 distinta en torno al valor general observado
Con suerte, esto ha aclarado el poder de .SD
para facilitar un código hermoso y eficiente en data.table
!
Hice un video sobre esto después de hablar con Matt Dowle sobre .SD, puedes verlo en YouTube: https://www.youtube.com/watch?v=DwEzQuYfMsI
Si piensas que ha resultado provechoso este post, nos gustaría que lo compartas con otros juniors de esta forma nos ayudas a extender nuestra información.