O que .SD representa em data.table em R


173

.SDparece útil, mas eu realmente não sei o que estou fazendo com isso. O que significa? Por que existe um período anterior (ponto final). O que está acontecendo quando eu o uso?

Eu li: .SDé um data.tablecontendo o subconjunto de xdados de cada grupo, excluindo as colunas do grupo. Pode ser usado ao agrupar por i, ao agrupar por by, keyed bye _ad hoc_by

Isso significa que a filha data.tables está na memória para a próxima operação?


30
?data.tablefoi aprimorado na v1.7.10, graças a esta pergunta. Agora, ele explica o nome de .SDacordo com a resposta aceita.
precisa saber é o seguinte

Respostas:


205

.SDsignifica algo como " Subset of Data.table". Não há significado para a inicial ".", exceto que torna ainda mais improvável que ocorra um conflito com um nome de coluna definido pelo usuário.

Se este é o seu 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

Fazer isso pode ajudá-lo a ver o que .SDé:

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

Basicamente, a by=ydeclaração divide os dados originais. Tabela nesses dois 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

e opera neles por sua vez.

Enquanto estiver operando em um dos dois, permite que você se refira ao sub- atual data.tableusando o apelido / identificador / símbolo .SD. Isso é muito útil, pois você pode acessar e operar as colunas como se estivesse sentado na linha de comando trabalhando com uma única tabela de dados chamada .SD... exceto que aqui, data.tableas operações serão executadas em todas as sub- data.tabledefinidas por combinações de teclas, "colando" elas novamente e retornando os resultados em uma única data.table!


12
Sim, exatamente. Outra maneira de ver .SDé DT[,print(.SD),by=y].
precisa

6
@ MatthewDowle - Enquanto você estiver aqui, uma pergunta para você. Fazendo DT[,print(.SD[,y]),by=y], indica que eu tenho acesso ao valor de y, mesmo que não faça parte dele .SD. De onde está o valor do yescopo? Está disponível porque é o valor atual de by?
Josh O'Brien

8
@ Josh Nice. Sim. O .SD[,y]é um data.tablesubconjunto regular, portanto, como ynão é uma coluna, .SDele procura o ambiente que o chamou, que nesse caso é o jambiente (da DTconsulta) em que as byvariáveis ​​estão disponíveis. Se não for encontrado lá, ele aparecerá no pai e no pai e assim por diante da maneira R usual. (Bem, também via escopo herdado de junção, que não está sendo usado nesses exemplos porque não há is).
precisa

1
O @ Josh FAQ 2.8 tem mais sobre isso, pode precisar de melhorias (sugestões são bem-vindas).
quer

3
@ Josh Porque as variáveis do grupo estão disponíveis em J, também, e são de comprimento 1. by=list(x,y,z)significaria x, ye zestão disponíveis para j. Para acesso genérico, eles também estão envolvidos .BY. A FAQ 2.10 tem um histórico, mas pode ser adicionada alguma clareza ?data.table. Grande ajuda docu seria muito bem-vinda. Ainda melhor se você gostaria de ingressar no projeto e mudar diretamente.
quer

98

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.tablesi 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, .SDem 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 Pitchingou Pitching[]:

identical(Pitching, Pitching[ , .SD])
# [1] TRUE

Em termos de .SDsubconjunto , 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 .SDuso do .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

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, fwritenão é possível ler Dateou POSIXctcolunas automaticamente , e as conversões entre character/ factor/ numericsão comuns. Podemos usar .SDe .SDcolsconverter em lote grupos dessas colunas.

Observamos que as seguintes colunas são armazenadas como characterno Teamsconjunto 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 sapplyaqui, 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 listonde cada elemento é uma coluna - portanto, sapply/ lapplyaplica FUN- se a cada coluna e retorna o resultado como sapply/ lapplynormalmente faria (aqui, FUN == is.characterretorna um logicalde 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 fktparê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 charactervetor ou um integervetor 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 factorcolunas 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 teamnovamente 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 Pitchingtabela. 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 .SDexplorar 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)

ajuste o coeficiente OLS em W, várias especificações

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.tablesintaxe é 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 logicalvetor, x[i]retornará as linhas xcorrespondentes a onde iestá TRUE; quando ié outradata.table , a joiné executada (na forma simples, usando os keyde xe 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à Pitchingtabela 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 .SDestá à direita Teams[.SD](já que o RHS :=nesse caso requer nrow(Pitching[rank_in_team == 1])valores.

.SDOperaçõ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.tableprocessos jé pensar que você data.tableestá dividido em muitos sub-componentes data.table, cada um dos quais corresponde a um único valor de sua by(s) variável (s):

agrupamento ilustrado

Nesse caso, .SDé de natureza múltipla - refere-se a cada um desses sub- data.tables, 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.tablee .Nse 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 .SDcolspara retornar apenas partes do item data.tablepara cada uma .SD(com a ressalva que .SDcolsdeve ser corrigida nos vários subconjuntos)

NB : .SD[1L]atualmente é otimizado por GForce( veja também ), data.tableinternos que aceleram massivamente as operações agrupadas mais comuns, como sumou mean- veja ?GForcepara 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 ERAe 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*teamIDserá 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')]

distribuição dos coeficientes ajustados

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 .SDfacilitar o código bonito e eficiente data.table!


5
Boa explicação. Apenas um comentário: em vez de equipes [, .SD [which.max (R)], por = teamID], você pode explorar a função de ordem rápida data.table: via equipes [order (teamID, -R), .SD [ 1L], keyby = teamID], isso deve ser mais rápido.
bartleby

@ Bartleby obrigado, na verdade isso está correto, mas a abordagem geral ainda é útil para saber - por exemplo, se precisamos pedir por uma quantidade dependente do grupo. talvez eu mude o exemplo.
MichaelChirico

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.