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


173

.SDparece útil pero realmente no sé qué estoy haciendo con él. Que significa? ¿Por qué hay un período anterior (punto final)? ¿Qué sucede cuando lo uso?

Leí: .SDes un que data.tablecontiene el subconjunto de xdatos de cada grupo, excluyendo las columnas del grupo. Se puede usar al agrupar por i, al agrupar por by, con clave byy _ad hoc_by

¿Eso significa que la hija data.tablese guarda en la memoria para la próxima operación?


30
?data.tablefue mejorado en v1.7.10, gracias a esta pregunta. Ahora explica el nombre .SDsegún la respuesta aceptada.
Matt Dowle

Respuestas:


205

.SDsignifica algo como " Subset of Data.table". La inicial no tiene importancia ".", excepto que hace que sea aún más improbable que haya un choque con un nombre de columna definido por el usuario.

Si este es su data.table:

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 ayudarlo a ver qué .SDes:

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

Básicamente, la by=ydeclaració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
# <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á operando en cualquiera de los dos, le permite referirse al subtítulo actual data.tableutilizando 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 comando trabajando con una sola tabla de datos llamada .SD... excepto que aquí, data.tablellevará a cabo esas operaciones en cada uno de los subdefinidos data.tablepor combinaciones de la clave, "pegarlas" de nuevo juntas y devolver los resultados en un solo data.table!


12
Sí exactamente. Otra forma de ver .SDes DT[,print(.SD),by=y].
Matt Dowle

66
@MatthewDowle - Mientras estás aquí, una pregunta para ti. Hacer DT[,print(.SD[,y]),by=y], indica que tengo acceso al valor de y, aunque no sea parte de .SD. ¿De dónde es el valor de yser explorado? ¿Está disponible b / c es el valor actual de by?
Josh O'Brien

8
@ Josh Nice. Si. El .SD[,y]es un data.tablesubconjunto regular, ya yque no se ve una columna .SDen el entorno que lo llamó, que en este caso es el jentorno (de la DTconsulta) donde byestán disponibles las variables. Si no se encuentra allí, se ve en el padre, y en su padre, etc., de la forma habitual de R. (Bueno, también a través del alcance heredado de unión, que no se usa en estos ejemplos porque no hay is).
Matt Dowle

1
@Josh FAQ 2.8 tiene más información sobre esto, podría necesitar mejoras (sugerencias bienvenidas).
Matt Dowle

3
@Josh Porque las variables de grupo también están disponibles en j, y son de longitud 1. by=list(x,y,z)significaría x, yy zestán disponibles para j. Para acceso genérico también están envueltos .BY. La pregunta frecuente 2.10 tiene algo de historia, pero podría agregarse algo de claridad ?data.table. Genial, la ayuda documental sería muy bienvenida. Aún mejor si desea unirse al proyecto y cambiar directamente.
Matt Dowle

98

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" - .SDes en su forma más básica un referencia reflexiva a data.tablesí 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 .SDes en sí undata.table (con la advertencia de que no permite la asignación con:= ).

El uso más simple de .SDes para subconjuntos de columnas (es decir, cuándo .SDcolsse 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 .SDen 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 Pitchingo 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 .SDes es limitar las columnas contenidas en el .SDuso del .SDcolsargumento 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, fwriteno se puede leer automáticamente Dateo POSIXctcolumnas , y las conversiones de ida y vuelta entre character/ factor/ numericson comunes. Podemos usar .SDy .SDcolsconvertir por lotes grupos de tales columnas.

Notamos que las siguientes columnas se almacenan como characteren el Teamsconjunto 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 sapplyaquí, 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 listdonde cada elemento es una columna; por lo tanto, sapply/ se lapplyaplica FUNa cada columna y devuelve el resultado como sapply/ lapplynormalmente lo haría (aquí, FUN == is.characterdevuelve un logicalde longitud 1, entoncessapply devuelve un vector).

La sintaxis para convertir estas columnas factores muy similar: simplemente agregue el :=operador de asignación

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

Tenga en cuenta que debemos incluir fktparé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 charactervector o un integervector 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 factorcolumnas 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 teamnuevo 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 Pitchingtabla. ¿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 .SDcual 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 (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.tableLa sintaxis es hermosa por su simplicidad y robustez. La sintaxis x[i]maneja de manera flexible dos enfoques comunes de subconjunto: cuando ies un logicalvector, x[i]devolverá esas filas xcorrespondientes a donde iestá TRUE; cuando ies otrodata.table , joinse realiza a (en forma simple, usando la keys de xy 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_performancea la Pitchingtabla 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 .SDestá a la derecha en Teams[.SD](ya que el RHS de :=en este caso requiere nrow(Pitching[rank_in_team == 1])valores.

.SDOperaciones 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.tableprocesos jes pensar data.tableque se divide en muchos subcomponentes data.table, cada uno de los cuales corresponde a un solo valor de su by(s) variable (s):

agrupación ilustrada

En este caso, .SDes 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 .SDun 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 .SDsí mismo es un data.table, y que se .Nrefiere 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 ( Rpor 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 .SDcolspara devolver solo partes de data.tablecada uno .SD(con la advertencia que .SDcolsdebe corregirse en los distintos subconjuntos)

NB : .SD[1L]actualmente está optimizado por GForce( vea también ), data.tablecomponentes internos que aceleran masivamente las operaciones agrupadas más comunes como sumo mean- vea ?GForcepara 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 ERAy 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*teamIDserá 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 concentración clara alrededor del valor general observado

¡Esperemos que esto haya aclarado el poder de .SDfacilitar un código hermoso y eficiente data.table!


55
Buena explicación. Solo un comentario: en lugar de Teams [, .SD [which.max (R)], by = teamID], puede explotar la función de orden rápida data.table: a través de Teams [order (teamID, -R), .SD [ 1L], keyby = teamID], esto debería ser más rápido.
bartleby

@bartleby gracias, de hecho, esto es correcto, pero el enfoque general sigue siendo útil para saber, por ejemplo, si necesitamos ordenar por una cantidad dependiente del grupo. Quizás cambie el ejemplo.
MichaelChirico

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.