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” - .SD
jest w swej najbardziej podstawowej guise A refleksyjne odniesienie do data.table
samego 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 .SD
służy do ustawiania podzbiorów kolumn (tj. Kiedy .SDcols
jest określony); Myślę, że ta wersja jest dużo łatwiejsza do zrozumienia, więc najpierw omówimy to poniżej. Interpretacja .SD
w 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 Pitching
lub Pitching[]
:
identical(Pitching, Pitching[ , .SD])
# [1] TRUE
Jeśli chodzi o .SD
podzbiór , to wciąż jest podzbiorem danych, to tylko trywialny (sam zestaw).
Podział kolumn: .SDcols
Pierwszym sposobem na uderzenia, co .SD
to jest ograniczenie kolumn zawartych w .SD
użyciu .SDcols
argumentu [
:
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 fwrite
nie można automatycznie czytać Date
ani POSIXct
kolumn , a konwersje tam iz powrotem międzycharacter
/ factor
/ numeric
są powszechne. Możemy używać .SD
i .SDcols
konwertować grupy takich kolumn.
Zauważamy, że następujące kolumny są przechowywane jako character
w zestawie Teams
danych:
# 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, list
gdzie każdy element jest kolumną - w ten sposób sapply
/ lapply
stosuje się FUN
do każdej kolumny i zwraca wynik jako sapply
/lapply
zwykle (tutaj FUN == is.character
zwraca a logical
o długości 1, więc sapply
zwraca wektor).
Składnia konwersji tych kolumn factor
jest 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ę fkt
do RHS.
Elastyczność .SDcols
(a :=
) przyjęcia character
wektor lub e integer
wektor pozycji kolumna może również się przydać do konwersji w oparciu o wzorce nazw kolumn *. Moglibyśmy przekonwertować wszystkie factor
kolumny 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 team
powrotem 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 Pitching
tabeli. Jak wygląda (liniowa) zależność między plikamiW
(wygrywa) i ERA
różni się w zależności od innych zmiennych towarzyszących uwzględnionych w specyfikacji?
Oto krótki skrypt wykorzystujący moc, .SD
któ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)
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.table
skł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 i
jest logical
wektorem, x[i]
zwróci te wiersze x
odpowiadające gdzie i
jest TRUE
; kiedy i
jest innydata.table
, join
wykonuje się a (w zwykłej formie, używając key
s z x
ii
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_performance
do Pitching
tabeli, 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 .SD
jest po prawej stronie Teams[.SD]
(ponieważ prawa strona :=
w tym przypadku wymaga nrow(Pitching[rank_in_team == 1])
wartości.
.SD
Operacje 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.table
procesy, j
polega na myśleniu, że data.table
jesteś podzielony na wiele pod-podrzędnych data.table
, z których każda odpowiada pojedynczej wartości by
zmiennej (-ych):
W tym przypadku .SD
ma charakter wieloraki - odnosi się do każdego z tych pod- data.table
, pojedynczo (nieco dokładniej, zakres .SD
jest 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 .SD
sobie data.table
, że to samo jest a i .N
odnosi 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 .SDcols
zwracaniem tylko części data.table
dla każdego .SD
(z zastrzeżeniem, które .SDcols
powinno zostać naprawione w różnych podzbiorach)
Uwaga : .SD[1L]
jest obecnie zoptymalizowany przez GForce
( zobacz także ) data.table
wewnętrzne elementy, które znacznie przyspieszają najczęściej wykonywane operacje grupowe, takie jak sum
lub mean
- zobacz ?GForce
wię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 ERA
i 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*teamID
bę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')]
Chociaż istnieje spora różnorodność, istnieje wyraźna koncentracja wokół obserwowanej wartości ogólnej
Miejmy nadzieję, że to wyjaśniło moc .SD
ułatwienia tworzenia pięknego, wydajnego kodu data.table
!
?data.table
został ulepszony w wersji 1.7.10, dzięki temu pytaniu. Teraz wyjaśnia nazwę.SD
zgodnie z zaakceptowaną odpowiedzią.