Editar:
Dada a resposta recebida, eu a converti em uma vinheta de pacote agora disponível aqui
Dada a frequência com que isso ocorre, acho que isso merece um pouco mais de exposição, além da resposta útil dada por Josh O'Brien acima.
Além do S ubset da D ata acrónimo geralmente citada / criado por Josh, eu acho que também é útil considerar o "S" para representar "selfsame" ou "auto-referência" - .SD
é em sua maioria disfarce um básica referência reflexiva a data.table
si mesma - como veremos nos exemplos abaixo, isso é particularmente útil para encadear "consultas" (extrações / subconjuntos / etc usando [
). Em particular, isso também significa que ele próprio.SD
é umdata.table
(com a ressalva de que não permite a atribuição de:=
).
O uso mais simples de .SD
é para subconjunto de colunas (ou seja, quando .SDcols
é especificado); Eu acho que esta versão é muito mais simples de entender, então abordaremos isso primeiro abaixo. A interpretação de, .SD
em seu segundo uso, cenários de agrupamento (ou seja, quando by =
ou keyby =
é especificado), é um pouco diferente, conceitualmente (embora no núcleo seja a mesma, pois, afinal, uma operação não agrupada é um caso extremo de agrupamento com apenas Um grupo).
Aqui estão alguns exemplos ilustrativos e outros exemplos de usos que eu mesmo implemento com frequência:
Carregando dados da Lahman
Para dar uma sensação mais real, em vez de criar dados, vamos carregar alguns conjuntos de dados sobre beisebol 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
Nu .SD
Para ilustrar o que quero dizer sobre a natureza reflexiva de .SD
, considere seu uso mais 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
Ou seja, acabamos de retornar Pitching
, ou seja, essa era uma maneira muito detalhada de escrever Pitching
ou Pitching[]
:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
Em termos de .SD
subconjunto , ainda é um subconjunto dos dados, é apenas trivial (o próprio conjunto).
Subconjunto de colunas: .SDcols
A primeira maneira de impactar o que .SD
é é limitar as colunas contidas no .SD
uso do .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
Isso é apenas para ilustração e foi bem chato. Mas mesmo esse simples uso se presta a uma ampla variedade de operações de manipulação de dados altamente benéficas / ubíquas:
Conversão de tipo de coluna
A conversão do tipo de coluna é um fato vital para a troca de dados - até o momento, fwrite
não é possível ler Date
ou POSIXct
colunas automaticamente , e as conversões entre character
/ factor
/ numeric
são comuns. Podemos usar .SD
e .SDcols
converter em lote grupos dessas colunas.
Observamos que as seguintes colunas são armazenadas como character
no Teams
conjunto de dados:
# 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
Se você está confuso com o uso de sapply
aqui, observe que é o mesmo da 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
A chave para entender essa sintaxe é lembrar que a data.table
(e também a data.frame
) pode ser considerado um local list
onde cada elemento é uma coluna - portanto, sapply
/ lapply
aplica FUN
- se a cada coluna e retorna o resultado como sapply
/ lapply
normalmente faria (aqui, FUN == is.character
retorna um logical
de comprimento 1, entãosapply
retorna um vetor).
A sintaxe para a qual converter essas colunas factor
é muito semelhante - basta adicionar o :=
operador de atribuição
Teams[ , (fkt) := lapply(.SD, factor), .SDcols = fkt]
Observe que devemos colocar fkt
parênteses ()
para forçar R a interpretar isso como nomes de colunas, em vez de tentar atribuir o nomefkt
ao RHS.
A flexibilidade de .SDcols
(e :=
) para aceitar um character
vetor ou um integer
vetor de posições de coluna também pode ser útil para a conversão baseada em padrões de nomes de colunas *. Podemos converter todas as factor
colunas para character
:
fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]
E então converta todas as colunas que contêm team
novamente para factor
:
team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]
** Usando explicitamente os números das colunas (comoDT[ , (1) := rnorm(.N)]
) é uma prática ruim e pode levar a códigos silenciosamente corrompidos ao longo do tempo, se as posições das colunas mudarem. Mesmo o uso implícito de números pode ser perigoso se não mantivermos controle inteligente / estrito sobre a ordem de quando criamos o índice numerado e quando o usamos.
Controlando o RHS de um modelo
A especificação variável do modelo é um recurso essencial da análise estatística robusta. Vamos tentar prever a ERA de um arremessador (média de desempenho ganho, uma medida de desempenho) usando o pequeno conjunto de covariáveis disponíveis na Pitching
tabela. Como a relação (linear) entre W
(vitórias) eERA
varia dependendo de quais outras covariáveis estão incluídas na especificação?
Aqui está um pequeno script que aproveita o poder de .SD
explorar esta questão:
# 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)
O coeficiente sempre tem o sinal esperado (arremessadores melhores tendem a ter mais vitórias e menos rodadas permitidas), mas a magnitude pode variar substancialmente, dependendo do que mais controlamos.
Junções Condicionais
data.table
sintaxe é bonita por sua simplicidade e robustez. A sintaxe x[i]
lida de maneira flexível com duas abordagens comuns para o subconjunto - quando i
é um logical
vetor, x[i]
retornará as linhas x
correspondentes a onde i
está TRUE
; quando i
é outradata.table
, a join
é executada (na forma simples, usando os key
de x
e i
, caso contrário, quandoon =
é especificado, usando correspondências dessas colunas).
Isso é ótimo em geral, mas fica aquém quando queremos realizar uma junção condicional , em que a natureza exata do relacionamento entre tabelas depende de algumas características das linhas em uma ou mais colunas.
Este exemplo é um pouco artificial, mas ilustra a idéia; veja aqui ( 1 , 2 ) para mais.
O objetivo é adicionar uma coluna team_performance
à Pitching
tabela que registra o desempenho (classificação) do time do melhor arremessador de cada time (conforme medido pela ERA mais baixa, entre arremessadores com pelo menos 6 jogos gravados).
# 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)]]
Observe que a x[y]
sintaxe retorna nrow(y)
valores, e é por isso que .SD
está à direita Teams[.SD]
(já que o RHS :=
nesse caso requer nrow(Pitching[rank_in_team == 1])
valores.
.SD
Operações agrupadas
Muitas vezes, gostaríamos de executar alguma operação em nossos dados no nível do grupo . Quando especificamos by =
(ou keyby =
), o modelo mental para o que acontece quando os data.table
processos j
é pensar que você data.table
está dividido em muitos sub-componentes data.table
, cada um dos quais corresponde a um único valor de sua by
(s) variável (s):
Nesse caso, .SD
é de natureza múltipla - refere-se a cada um desses sub- data.table
s, um de cada vez (um pouco mais precisamente, o escopo de .SD
é um único sub- data.table
). Isso nos permite expressar concisa uma operação que gostaríamos de realizar em cada sub-data.table
antes que o resultado remontado seja devolvido a nós.
Isso é útil em uma variedade de configurações, as mais comuns são apresentadas aqui:
Sub-definição de grupo
Vamos obter a temporada de dados mais recente de cada equipe nos dados de Lahman. Isso pode ser feito simplesmente com:
# 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]
Lembre-se de que .SD
é ele próprio a data.table
e .N
se refere ao número total de linhas em um grupo (é igual a nrow(.SD)
dentro de cada grupo); portanto, .SD[.N]
retorna a totalidade da.SD
linha final associada a cada um teamID
.
Outra versão comum disso é usar .SD[1L]
para obter a primeira observação para cada grupo.
Grupo Optima
Suponha que desejássemos retornar o melhor ano para cada equipe, conforme medido pelo número total de execuções pontuadas ( R
; poderíamos facilmente ajustar isso para se referir a outras métricas, é claro). Em vez de pegar um elemento fixo de cada sub- data.table
, agora definimos o índice desejado dinamicamente da seguinte maneira:
Teams[ , .SD[which.max(R)], by = teamID]
Observe que é claro que essa abordagem pode ser combinada .SDcols
para retornar apenas partes do item data.table
para cada uma .SD
(com a ressalva que .SDcols
deve ser corrigida nos vários subconjuntos)
NB : .SD[1L]
atualmente é otimizado por GForce
( veja também ), data.table
internos que aceleram massivamente as operações agrupadas mais comuns, como sum
ou mean
- veja ?GForce
para mais detalhes e fique de olho no suporte por voz para pedidos de aprimoramento de recursos para atualizações nesta frente: 1 , 2 , 3 , 4 , 5 , 6
Regressão agrupada
Voltando à pergunta acima sobre o relacionamento entre ERA
e W
, suponha que esperamos que esse relacionamento seja diferente por equipe (ou seja, há uma inclinação diferente para cada equipe). Podemos facilmente reexecutar essa regressão para explorar a heterogeneidade nesse relacionamento da seguinte maneira (observando que os erros padrão dessa abordagem geralmente estão incorretos - a especificação ERA ~ W*teamID
será melhor - essa abordagem é mais fácil de ler e os coeficientes estão OK) :
# 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')]
Embora exista uma quantidade razoável de heterogeneidade, há uma concentração distinta em torno do valor geral observado
Espero que isso tenha esclarecido o poder de .SD
facilitar o código bonito e eficiente data.table
!
?data.table
foi aprimorado na v1.7.10, graças a esta pergunta. Agora, ele explica o nome de.SD
acordo com a resposta aceita.