Co oznacza .SD w data.table w R

173

.SDwygląda na przydatne, ale tak naprawdę nie wiem, co z nim robię. Co to oznacza? Dlaczego występuje poprzednia kropka (kropka). Co się dzieje, gdy go używam?

Czytałem: .SDto zbiór data.tablezawierający podzbiór xdanych dla każdej grupy, z wyłączeniem kolumn grup. Może być używany podczas grupowania według i, podczas grupowania według by, klucza byi _ad hoc_by

Czy to oznacza, że ​​córka data.tablezostaje zachowana w pamięci do następnej operacji?

Farrel
źródło
30
?data.tablezostał ulepszony w wersji 1.7.10, dzięki temu pytaniu. Teraz wyjaśnia nazwę .SDzgodnie z zaakceptowaną odpowiedzią.
Matt Dowle,

Odpowiedzi:

205

.SDoznacza coś w rodzaju „ Subset of Data.table”. Początek nie ma żadnego znaczenia ".", poza tym, że jest jeszcze mniej prawdopodobne, że dojdzie do konfliktu z nazwą kolumny zdefiniowaną przez użytkownika.

Jeśli to są Twoje dane. Tabela:

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

Może to pomóc Ci zobaczyć, co .SDto jest:

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

Zasadniczo by=yinstrukcja dzieli pierwotną tabelę data.table na te dwie pod-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

i działa na nich po kolei.

Podczas gdy działa na którymkolwiek z nich, pozwala ci odwołać się do aktualnego podrzędnego data.tableprzy użyciu pseudonimu / uchwytu / symbolu .SD. Jest to bardzo przydatne, ponieważ możesz uzyskać dostęp i operować na kolumnach tak, jakbyś siedział w wierszu poleceń, pracując z pojedynczą tabelą danych o nazwie .SD... z wyjątkiem tego, że tutaj wykonasz data.tablete operacje na każdym data.tablezdefiniowanym przez kombinacje klucza, „wklejanie” ich z powrotem i zwracanie wyników w jednym data.table!

Josh O'Brien
źródło
12
Tak, dokładnie. Innym sposobem, aby zobaczyć .SDto DT[,print(.SD),by=y].
Matt Dowle,
6
@MatthewDowle - Skoro już tu jesteś, pytanie do Ciebie. Wykonanie DT[,print(.SD[,y]),by=y]oznacza, że ​​mam dostęp do wartości y, mimo że nie jest ona częścią .SD. Skąd bierze się wartość yposiadania zakresu? Czy jest dostępny b / c to jest aktualna wartość by?
Josh O'Brien,
8
@Josh Nice. Tak. .SD[,y]Jest zwykłym data.tablepodzbiorem tak, ponieważ ynie jest to kolumna .SDwygląda w środowisku, który wywołał to, co w tym przypadku, to jśrodowisko (na DTzapytanie), gdzie bydostępne są zmienne. Jeśli nie zostanie tam znaleziony, wygląda w rodzicu, jego rodzicu i tak dalej w zwykły sposób R. (Cóż, również przez dziedziczony zakres złączenia, który nie jest używany w tych przykładach, ponieważ nie ma żadnych is).
Matt Dowle,
1
@Josh FAQ 2.8 zawiera więcej informacji na ten temat, może wymagać ulepszeń (mile widziane sugestie).
Matt Dowle,
3
@Josh Ponieważ zmienne grupy są dostępne w j, też, i to długość 1. by=list(x,y,z)oznaczałoby x, yi zsą dostępne j. Aby uzyskać ogólny dostęp, są one również zapakowane .BY. W FAQ 2.10 jest trochę historii, ale można by dodać do tego trochę jasności ?data.table. Świetnie, pomoc dokumentacyjna byłaby bardzo mile widziana. Jeszcze lepiej, jeśli chcesz dołączyć do projektu i bezpośrednio zmienić.
Matt Dowle,
98

Edytować:

Biorąc pod uwagę, jak dobrze przyjęto tę odpowiedź, przekształciłem ją w winietę pakietową, która jest teraz dostępna tutaj


Biorąc pod uwagę, jak często się to pojawia, myślę, że uzasadnia to nieco szersze wyjaśnienie, poza pomocną odpowiedzią udzieloną przez Josha O'Briena powyżej.

Oprócz S ubset części D ata skrót najczęściej cytowanym / stworzony przez Josha, myślę, że to również pomocne do rozważenia „S” stanąć na „ten sam” lub „self-reference” - .SDjest w swej najbardziej podstawowej guise A refleksyjne odniesienie do data.tablesamego siebie - jak zobaczymy w poniższych przykładach, jest to szczególnie przydatne przy łączeniu w łańcuch "zapytań" (używanie ekstrakcji / podzbiorów / itp [.). W szczególności oznacza to również, że samo.SD jest adata.table (z zastrzeżeniem, że nie pozwala na przypisanie :=).

Prostsze użycie .SDsłuży do ustawiania podzbiorów kolumn (tj. Kiedy .SDcolsjest określony); Myślę, że ta wersja jest dużo łatwiejsza do zrozumienia, więc najpierw omówimy to poniżej. Interpretacja .SDw drugim zastosowaniu scenariuszy grupowania (tj. Kiedy by =lub keyby =jest określona) jest koncepcyjnie nieco inna (chociaż w istocie jest taka sama, ponieważ w końcu operacja niezgrupowana jest skrajnym przypadkiem grupowania z tylko jedna grupa).


Oto kilka przykładów ilustracyjnych i kilka innych przykładów zastosowań, które sam często stosuję:

Ładowanie danych Lahmana

Aby nadać temu bardziej realistyczny charakter, zamiast wymyślać dane, załadujmy kilka zestawów danych o baseballu z 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

Nagi .SD

Aby zilustrować, co mam na myśli, mówiąc o refleksyjnej naturze .SD, rozważmy jego najbardziej banalne użycie:

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

Oznacza to, że właśnie wróciliśmy Pitching, tj. Był to zbyt rozwlekły sposób pisania Pitchinglub Pitching[]:

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

Jeśli chodzi o .SDpodzbiór , to wciąż jest podzbiorem danych, to tylko trywialny (sam zestaw).

Podział kolumn: .SDcols

Pierwszym sposobem na uderzenia, co .SDto jest ograniczenie kolumn zawartych w .SDużyciu .SDcolsargumentu [:

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

To jest tylko dla ilustracji i było dość nudne. Ale nawet to proste użycie nadaje się do szerokiej gamy bardzo korzystnych / wszechobecnych operacji manipulacji danymi:

Konwersja typu kolumny

Konwersja typu kolumny jest faktem w przypadku łączenia danych - w chwili pisania tego tekstu fwritenie można automatycznie czytać Dateani POSIXctkolumn , a konwersje tam iz powrotem międzycharacter / factor/ numericsą powszechne. Możemy używać .SDi .SDcolskonwertować grupy takich kolumn.

Zauważamy, że następujące kolumny są przechowywane jako character w zestawie Teamsdanych:

# 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 

Jeśli jesteś zdezorientowany przez użycie sapply tutaj, zwróć uwagę, że jest to to samo, co w przypadku podstawy 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

Kluczem do zrozumienia tej składni jest przypomnienie, że a data.table(jak również a data.frame) można uznać za a, listgdzie każdy element jest kolumną - w ten sposób sapply/ lapplystosuje się FUNdo każdej kolumny i zwraca wynik jako sapply/lapply zwykle (tutaj FUN == is.characterzwraca a logicalo długości 1, więc sapplyzwraca wektor).

Składnia konwersji tych kolumn factorjest bardzo podobna - po prostu dodaj:= operator przypisania

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

Zauważ, że musimy zawinąć fkt w nawiasy, ()aby zmusić R do interpretacji tego jako nazw kolumn, zamiast próbować przypisać nazwę fktdo RHS.

Elastyczność .SDcols(a :=) przyjęcia characterwektor lub e integerwektor pozycji kolumna może również się przydać do konwersji w oparciu o wzorce nazw kolumn *. Moglibyśmy przekonwertować wszystkie factorkolumny na character:

fkt_idx = which(sapply(Teams, is.factor))
Teams[ , (fkt_idx) := lapply(.SD, as.character), .SDcols = fkt_idx]

Następnie przekonwertuj wszystkie kolumny zawierające z teampowrotem na factor:

team_idx = grep('team', names(Teams), value = TRUE)
Teams[ , (team_idx) := lapply(.SD, factor), .SDcols = team_idx]

** Jawne używanie numerów kolumn (takich jak DT[ , (1) := rnorm(.N)]) jest złą praktyką i może z czasem prowadzić do dyskretnego uszkodzenia kodu, jeśli zmieniają się pozycje kolumn. Nawet niejawne używanie liczb może być niebezpieczne, jeśli nie zachowujemy inteligentnej / ścisłej kontroli nad kolejnością, kiedy tworzymy numerowany indeks i kiedy go używamy.

Kontrolowanie RHS Modela

Zmienna specyfikacja modelu jest podstawową cechą solidnej analizy statystycznej. Spróbujmy przewidzieć ERA miotacza (średnia zdobytych przebiegów, miara wydajności) przy użyciu małego zestawu zmiennych towarzyszących dostępnych w Pitchingtabeli. Jak wygląda (liniowa) zależność między plikamiW (wygrywa) i ERAróżni się w zależności od innych zmiennych towarzyszących uwzględnionych w specyfikacji?

Oto krótki skrypt wykorzystujący moc, .SDktóry bada to pytanie:

# 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)

dopasować współczynnik OLS do W, różne specyfikacje

Współczynnik zawsze ma oczekiwany znak (lepsi miotacze mają zwykle więcej wygranych i mniej dozwolonych przebiegów), ale wielkość może się znacznie różnić w zależności od tego, co jeszcze kontrolujemy.

Połączenia warunkowe

data.tableskładnia jest piękna ze względu na swoją prostotę i solidność. Składnia x[i]elastycznie obsługuje dwa typowe podejścia do podzbioru - kiedy ijest logicalwektorem, x[i]zwróci te wiersze xodpowiadające gdzie ijest TRUE; kiedy ijest innydata.table , joinwykonuje się a (w zwykłej formie, używając keys z xii przeciwnym razie, gdy on =jest określone, przy użyciu dopasowań tych kolumn).

Ogólnie jest to świetne, ale nie wystarczy, gdy chcemy wykonać sprzężenie warunkowe , w którym dokładny charakter relacji między tabelami zależy od niektórych cech wierszy w jednej lub większej liczbie kolumn.

Ten przykład jest nieco wymyślony, ale ilustruje pomysł; więcej tutaj ( 1 , 2 ).

Celem jest dodanie kolumny team_performancedo Pitchingtabeli, która rejestruje wyniki zespołu (rangę) najlepszego miotacza w każdej drużynie (mierzoną przez najniższą ERA, wśród miotaczy z co najmniej 6 zarejestrowanymi meczami).

# 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)]]

Zwróć uwagę, że x[y]składnia zwraca nrow(y)wartości, dlatego .SDjest po prawej stronie Teams[.SD](ponieważ prawa strona :=w tym przypadku wymaga nrow(Pitching[rank_in_team == 1])wartości.

.SDOperacje zgrupowane

Często chcielibyśmy wykonać pewne operacje na naszych danych na poziomie grupy . Kiedy określamy by =(lub keyby =), mentalny model tego, co dzieje się, gdy data.tableprocesy, jpolega na myśleniu, że data.tablejesteś podzielony na wiele pod-podrzędnych data.table, z których każda odpowiada pojedynczej wartości byzmiennej (-ych):

zilustrowane grupowanie

W tym przypadku .SDma charakter wieloraki - odnosi się do każdego z tych pod- data.table, pojedynczo (nieco dokładniej, zakres .SDjest jednym pod- data.table). To pozwala nam zwięźle wyrazić operację, którą chcielibyśmy wykonać na każdymdata.table składniku, zanim ponownie złożony wynik zostanie nam zwrócony.

Jest to przydatne w różnych ustawieniach, z których najczęstsze są przedstawione tutaj:

Podzbiór grupowy

Uzyskajmy dane z ostatniego sezonu dla każdej drużyny w danych Lahmana. Można to zrobić po prostu za pomocą:

# 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]

Przypomnij .SDsobie data.table, że to samo jest a i .Nodnosi się do całkowitej liczby wierszy w grupie (jest równa nrow(.SD)w każdej grupie), więc .SD[.N]zwraca całość.SD dla ostatniego wiersza skojarzonego z każdymteamID .

Inną popularną wersją jest użycie .SD[1L]zamiast tego pierwszego obserwacji dla każdej grupy.

Grupa Optima

Załóżmy, że chcemy zwrócić najlepszy rok dla każdego zespołu, mierzony całkowitą liczbą zdobytych runów ( R; oczywiście możemy łatwo dostosować to, aby odnosić się do innych wskaźników). Zamiast pobierać stały element z każdego pod- data.table, teraz dynamicznie definiujemy żądany indeks w następujący sposób:

Teams[ , .SD[which.max(R)], by = teamID]

Zauważ, że to podejście można oczywiście łączyć z .SDcolszwracaniem tylko części data.tabledla każdego .SD(z zastrzeżeniem, które .SDcolspowinno zostać naprawione w różnych podzbiorach)

Uwaga : .SD[1L]jest obecnie zoptymalizowany przez GForce( zobacz także ) data.tablewewnętrzne elementy, które znacznie przyspieszają najczęściej wykonywane operacje grupowe, takie jak sumlub mean- zobacz ?GForcewięcej szczegółów i miej oko na / głosową obsługę próśb o ulepszenie funkcji dla aktualizacji na tym froncie: 1 , 2 , 3 , 4 , 5 , 6

Regresja grupowa

Wracając do powyższego pytania dotyczącego relacji między ERAi W, przypuśćmy, że spodziewamy się, że ta relacja będzie się różnić w zależności od zespołu (tj. Dla każdego zespołu istnieje inne nachylenie). Możemy łatwo powtórzyć tę regresję, aby zbadać niejednorodność tej relacji w następujący sposób (zauważając, że standardowe błędy z tego podejścia są generalnie niepoprawne - specyfikacja ERA ~ W*teamIDbędzie lepsza - to podejście jest łatwiejsze do odczytania, a współczynniki są w porządku) :

# 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')]

rozkład dopasowanych współczynników

Chociaż istnieje spora różnorodność, istnieje wyraźna koncentracja wokół obserwowanej wartości ogólnej

Miejmy nadzieję, że to wyjaśniło moc .SDułatwienia tworzenia pięknego, wydajnego kodu data.table!

MichaelChirico
źródło
5
Dobre wytłumaczenie. Tylko jeden komentarz: Zamiast Teams [, .SD [which.max (R)], by = teamID], możesz wykorzystać funkcję szybkiego zamawiania danych.table: przez Teams [order (teamID, -R), .SD [ 1L], keyby = teamID], powinno być szybsze.
Bartleby
@bartleby dzięki, wprawdzie jest to poprawne, ale ogólne podejście jest nadal przydatne, aby wiedzieć - np. jeśli potrzebujemy zamówić według ilości zależnej od grupy. może zmienię przykład.
MichaelChirico