Saltar al contenido

¿Qué significa .SD en data.table en R

Hola, tenemos la respuesta a tu interrogante, has scroll y la verás aquí.

Solución:

.SD representa algo como “Sconjunto de Data.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)

ajuste el coeficiente OLS en W, varias especificaciones, representadas como barras con distintos colores.

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 keys 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.tables, cada uno de los cuales corresponde a un único valor de su by variable (s):

Una descripción visual de cómo funciona la agrupación.  a la izquierda hay una cuadrícula.  La primera columna se titula

En este caso, .SD es de naturaleza múltiple – se refiere a cada uno de estos sub-data.tables, 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')]

Un histograma que muestra la distribución de coeficientes ajustados.  Tiene una vaga forma de campana y se concentra alrededor de -2.

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.

¡Haz clic para puntuar esta entrada!
(Votos: 0 Promedio: 0)



Utiliza Nuestro Buscador

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *