studio - key data table r
Qué significa.SD en data.table en R (2)
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 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 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'')]
# 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 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 sintaxis 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 sintaxis 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)
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
sintaxis data.table
es hermosa por su simplicidad y robustez. La sintaxis 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 objetivo 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 sintaxis 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):
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 alcance de .SD
es un solo data.table
) Esto nos permite expresar 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'')]
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
!
.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
# 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 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>
# 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
# <final output, since print() doesn''t return anything>
# 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
.