Denna anteckning kommer att vara intressant för dem som anvĂ€nder databehandlingsbiblioteket för R â data.table, och kanske kommer att glĂ€dja sig över flexibiliteten i dess tillĂ€mpning pĂ„ olika exempel.
Inspirerad av ett bra exempel , och hoppades att du redan har lÀst hans artikel, föreslÄr jag att vi grÀver djupare i optimering av kod och prestanda baserat pÄ data.table.
Inledning: VarifrÄn kommer data.table?
Det Àr bÀst att börja bekanta sig med biblioteket nÄgot pÄ avstÄnd, nÀmligen med datatyperna som kan generera ett data.table-objekt (hÀdanefter, DT).
Array
Kod
## arrays ---------
arrmatr <- array(1:20, c(4,5))
class(arrmatr)
typeof(arrmatr)
is.array(arrmatr)
is.matrix(arrmatr)
En sÄdan struktur Àr en array (?base::array). Precis som i andra sprÄk Àr arrayer hÀr flerdimensionella. Det intressanta Àr att till exempel en tvÄdimensionell array börjar Àrva egenskaper frÄn klassen matris (?base::matrix), medan en endimensionell array, vilket ocksÄ Àr viktigt, inte Àrver frÄn vektorn (?base::vector).
DĂ€rför Ă€r det viktigt att förstĂ„ att datatypen som finns i ett objekt bör kontrolleras med funktionen base::typeof, som returnerar en intern beskrivning av typen enligt R Internals â det allmĂ€nna protokollet för sprĂ„ket som Ă€r relaterat till den ursprungliga C.
En annan kommando för att bestÀmma klass av ett objekt, base::class, returnerar vid vektorer vektor-typen (den har ett annat namn Àn den interna, men gör det ocksÄ möjligt att förstÄ datatypen).
Lista
FrÄn en tvÄdimensionell array, vilken ocksÄ Àr en matris, kan man gÄ vidare till en lista (?base::list).
Kod
## lists ------------------
mylist <- as.list(arrmatr)
is.vector(mylist)
is.list(mylist)
Flertalet saker hÀnder samtidigt:
- Det andra mÄttet av matrisen komprimeras, vilket innebÀr att vi fÄr bÄde en lista och en vektor.
- Listan Àr sÄledesverksamm av dessa klasser. Det Àr viktigt att ha i Ätanke att ett list-element kommer att motsvara ett (skalar) vÀrde frÄn matrisens array-cell.
Tack vare att listan ocksÄ Àr en vektor kan vissa funktioner för vektorer tillÀmpas pÄ den.
Dataframe
FrÄn en lista, matris eller vektor kan man övergÄ till en dataframe (?base::data.frame).
Kod
## data.frames ------------
df <- as.data.frame(arrmatr)
df2 <- as.data.frame(mylist)
is.list(df)
df$V6 <- df$V1 + df$V2
Vad som Àr intressant i den: en dataframe Àrver frÄn en lista! Kolumnerna i dataframen Àr celler i listan. Detta kommer att vara viktigt senare nÀr vi anvÀnder funktioner som tillÀmpas pÄ listor.
data.table
En DT (?data.table::data.table) kan skapas frÄn dataframe, lista, vektor eller matris. Till exempel sÄ hÀr (in place).
Kod
## data.tables -----------------------
library(data.table)
data.table::setDT(df)
is.list(df)
is.data.frame(df)
is.data.table(df)
Det Àr anvÀndbart att, precis som en dataframe, DT Àrver egenskaper frÄn en lista.
DT och minne
Till skillnad frÄn alla andra objekt i R-bas, överförs DT via referens. Om du behöver göra en kopia i ett nytt minnesomrÄde krÀvs en funktion data.table::copy eller sÄ mÄste du göra ett urval frÄn det gamla objektet.
Kod
df2 <- df
df[V1 == 1, V2 := 999]
data.table::fsetdiff(df, df2)
df2 <- data.table::copy(df)
df[V1 == 2, V2 := 999]
data.table::fsetdiff(df, df2)
Med detta avslutas introduktionen. DT Àr en fortsÀttning pÄ utvecklingen av datatyper i R, som frÀmst sker genom att utöka och snabba upp operationer som utförs pÄ objekt av dataram-klass. Samtidigt bevaras Àrvandet frÄn andra primitiva typer.
NÄgra exempel pÄ anvÀndning av egenskaperna hos data.table
Som en listaâŠ
Att iterera över raderna i en dataram eller DT Àr inte den bÀsta idén, eftersom loopkoden pÄ sprÄket R Àr mycket lÄngsammare C, men att iterera över kolumner, som vanligtvis Àr betydligt fÀrre, gÄr bra. NÀr man gÄr igenom kolumnerna, kom ihÄg att varje kolumn Àr ett element i en lista som vanligtvis innehÄller en vektor. Och operationer pÄ vektorer Àr vÀl vektoriserade i grundlÀggande funktioner i sprÄket. Det gÄr ocksÄ att anvÀnda urvalsomoperatorer som Àr typiska för listor och vektorer: `[[`, `$`.
Kod
## operations on data.tables ------------
#using list properties
df$'V1'[1]
df[['V1']]
df[[1]][1]
sapply(df, class)
sapply(df, function(x) sum(is.na(x)))
Vektorisering
Om det finns ett behov av att gÄ igenom raderna i en stor DT, Àr den bÀsta lösningen att skriva en funktion med vektorisering. Men om detta inte Àr möjligt, kom ihÄg att loopen inuti DT fortfarande Àr snabbare Àn loopen i R, eftersom den körs pÄ C.
LĂ„t oss prova med ett större exempel med 100K rader. Vi kommer att extrahera den första bokstaven frĂ„n ord som ingĂ„r i vektor-kolumnen â kolumnvektorer, och $inline$(.)^H$inline$ Ă€r operationen.
Uppdaterad
Kod
library(magrittr)
library(microbenchmark)
## Större exempel ----
rown <- 100000
dt %
.[, d := 1 + b + c + rnorm(nrow(.))]
# vektorisering
microbenchmark({
dt[
, first_l := unlist(strsplit(w, split = ' ', fixed = T))[1]
, by = 1:nrow(dt)
]
})
# andra
first_l_f %
do.call(rbind, .) %>%
`[`(,1)
}
dt[, first_l := NULL]
microbenchmark({
dt[
, first_l := .(first_l_f(w))
]
})
# tredje
first_l_f2 %
unlist %>%
matrix(nrow = 3) %>%
`[`(1,)
}
dt[, first_l := NULL]
microbenchmark({
dt[
, first_l := .(first_l_f2(w))
]
})
Första körningen med iteration över rader:
Enhet: millisekunder
expr min
{ dt[, `:=`(first_l, unlist(strsplit(w, split = " ", fixed = T))[1]), by = 1:nrow(dt)] } 439.6217
lq mean median uq max neval
451.9998 460.1593 456.2505 460.9147 621.4042 100
Den andra genomkörningen, dÀr vektoriseringsprocessen sker genom att omvandla en lista till en matris och ta element vid skÀrningspunkten med index 1 (detta Àr sjÀlva vektoriseringen). Jag rÀttar mig: vektoriseringen pÄ funktionsnivÄ. strsplit, som kan ta en vektor som indata. Det visar sig att proceduren för att omvandla en lista till en matris Àr mycket tyngre Àn sjÀlva vektoriseringen, men i det hÀr fallet Àr det fortfarande mycket snabbare Àn den icke-vektoriserade varianten.
Enhet: millisekunder
expr min lq mean median uq max neval
{ dt[, `:=`(first_l, .(first_l_f(w)))] } 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100
Fördjupning baserad pÄ medianen i 3 gÄnger.
Den tredje genomkörningen, dÀr schemat för att omvandla till matris har Àndrats.
Enhet: millisekunder
expr min lq mean median uq max neval
{ dt[, `:=`(first_l, .(first_l_f2(w)))] } 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100
Fördjupning baserad pÄ medianen i 13 gÄnger.
Med detta mÄste man experimentera, ju mer ju bÀttre blir det.
Ytterligare ett exempel pÄ vektorisering, dÀr Àven text ingÄr, men det Àr nÀrmare verkliga förhÄllanden: olika lÀngder pÄ orden, olika antal ord. Det krÀvs att man hÀmtar de tre första orden. SÄ hÀr:

HÀr fungerar inte den tidigare funktionen lÀngre, eftersom vektorerna har olika lÀngder och vi angav storleken pÄ matrisen. Vi tar om detta genom att grÀva i internet.
Kod
# fourth
rown <- 100000
words <-
sapply(
seq_len(rown)
, function(x){
nwords <- rbinom(1, 10, 0.5)
paste(
sapply(
seq_len(nwords)
, function(x){
paste(sample(letters, rbinom(1, 10, 0.5), replace = T), collapse = '')
}
)
, collapse = ' '
)
}
)
dt <-
data.table(
w = words
, a = sample(letters, rown, replace = T)
, b = runif(rown, -3, 3)
, c = runif(rown, -3, 3)
, e = rnorm(rown)
) %>%
.[, d := 1 + b + c + rnorm(nrow(.))]
first_l_f3 <- function(sd, n)
{
l <- strsplit(sd, split = ' ', fixed = T)
maxl <- max(lengths(l))
sapply(l, "length<-", maxl) %>%
`[`(n,) %>%
as.character
}
microbenchmark({
dt[
, (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x))
]
})
dt[
, (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x))
]
Enhet: millisekunder
expr min lq mean median
{ dt[, `:=`((paste0(«w_», 1:3)), strsplit(w, split = " ", fixed = T))] } 851.7623 916.071 1054.5 1035.199
uq max neval
1178.738 1356.816 100
Skriptet kördes med en genomsnittlig hastighet pÄ 1 sekund. Inte dÄligt.
LĂ€nkade som en kedja...
Med DT-objekt kan man arbeta med hjÀlp av chaining. Det ser ut som om man fÀster klamrar pÄ höger sida, i princip en sockerbit.
Kod
# chaining
res1 <- dt[a == 'a'][sample(.N, 100)]
res2 <- dt[, .N, a][, N]
res3 <- dt[, coefficients(lm(e ~ d))[1], a][, .(letter = a, coef = V1)]
Rinner genom rören...
Samma operationer kan göras genom piping, ser liknande ut, men funktionellt rikare, eftersom man kan anvÀnda alla metoder, inte bara DT. Vi kommer att visa koefficienterna för logistisk regression för vÄra syntetiska data med ett antal filter pÄ DT.
Kod
# piping
samplpe_b <- dt[a %in% head(letters), sample(b, 1)]
res4 <-
dt %>%
.[a %in% head(letters)] %>%
.[,
{
dt0 <- .SD[1:100]
quants <-
dt0[, c] %>%
quantile(seq(0.1, 1, 0.1), na.rm = T)
.(q = quants)
}
, .(cond = b > samplpe_b)
] %>%
glm(
cond ~ q -1
, family = binomial(link = "logit")
, data = .
) %>%
summary %>%
.[[12]]
Statistik, maskininlÀrning och annat inom DT
Man kan anvĂ€nda lambda-funktioner, men ibland Ă€r det bĂ€ttre att skapa dem separat, skriva hela pipelinen för dataanalys, och sedan framĂ„t â de fungerar inom DT. Exemplet Ă€r berikat med alla ovan nĂ€mnda funktioner, plus nĂ„gra anvĂ€ndbara saker frĂ„n DT:s arsenal (sĂ„som att adressera DT inom DT via lĂ€nk, ibland inte i ordning, men för att det ska funka).
Kod
# function
rm(lm_preds)
lm_preds <- function(
sd, by, n
)
{
if(
n < 100 |
!by[['a']] %in% head(letters, 4)
)
{
res <-
list(
low = NA
, mean = NA
, high = NA
, coefs = NA
)
} else {
lmm <-
lm(
d ~ c + b
, data = sd
)
preds <-
stats::predict.lm(
lmm
, sd
, interval = "prediction"
)
res <-
list(
low = preds[, 2]
, mean = preds[, 1]
, high = preds[, 3]
, coefs = coefficients(lmm)
)
}
res
}
res5 <-
dt %>%
.[e < 0] %>%
.[.[, .I[b > 0]]] %>%
.[, `:=` (
low = as.numeric(lm_preds(.SD, .BY, .N)[[1]])
, mean = as.numeric(lm_preds(.SD, .BY, .N)[[2]])
, high = as.numeric(lm_preds(.SD, .BY, .N)[[3]])
, coef_c = as.numeric(lm_preds(.SD, .BY, .N)[[4]][1])
, coef_b = as.numeric(lm_preds(.SD, .BY, .N)[[4]][2])
, coef_int = as.numeric(lm_preds(.SD, .BY, .N)[[4]][3])
)
, a
] %>%
.[!is.na(mean), -'e', with = F]
# plot
plo <-
res5 %>%
ggplot +
facet_wrap(~ a) +
geom_ribbon(
aes(
x = c * coef_c + b * coef_b + coef_int
, ymin = low
, ymax = high
, fill = a
)
, size = 0.1
, alpha = 0.1
) +
geom_point(
aes(
x = c * coef_c + b * coef_b + coef_int
, y = mean
, color = a
)
, size = 1
) +
geom_point(
aes(
x = c * coef_c + b * coef_b + coef_int
, y = d
)
, size = 1
, color = 'black'
) +
theme_minimal()
print(plo)
Slutsats
Jag hoppas att jag har kunnat skapa en sammanhÀngande, men sjÀlvklart inte fullstÀndig, bild av ett objekt som data.table, frÄn dess egenskaper kopplade till arv frÄn R-klasser till dess egna funktioner och omgivning av tidyverse-element. Jag hoppas detta hjÀlper dig att förstÄ och anvÀnda detta bibliotek bÀttre för arbete och nöje.

Tack!
FullstÀndig kod
Kod
## load libs ----------------
library(data.table)
library(ggplot2)
library(magrittr)
library(microbenchmark)
## arrays ---------
arrmatr <- array(1:20, c(4,5))
class(arrmatr)
typeof(arrmatr)
is.array(arrmatr)
is.matrix(arrmatr)
## lists ------------------
mylist <- as.list(arrmatr)
is.vector(mylist)
is.list(mylist)
## data.frames ------------
df <- as.data.frame(arrmatr)
is.list(df)
df$V6 <- df$V1 + df$V2
## data.tables -----------------------
data.table::setDT(df)
is.list(df)
is.data.frame(df)
is.data.table(df)
df2 <- df
df[V1 == 1, V2 := 999]
data.table::fsetdiff(df, df2)
df2 <- data.table::copy(df)
df[V1 == 2, V2 := 999]
data.table::fsetdiff(df, df2)
## operations on data.tables ------------
#using list properties
df$'V1'[1]
df[['V1']]
df[[1]][1]
sapply(df, class)
sapply(df, function(x) sum(is.na(x)))
## Bigger example ----
rown <- 100000
dt <-
data.table(
w = sapply(seq_len(rown), function(x) paste(sample(letters, 3, replace = T), collapse = ' '))
, a = sample(letters, rown, replace = T)
, b = runif(rown, -3, 3)
, c = runif(rown, -3, 3)
, e = rnorm(rown)
) %>%
.[, d := 1 + b + c + rnorm(nrow(.))]
# vectorization
# zero - for loop
microbenchmark({
for(i in 1:nrow(dt))
{
dt[
i
, first_l := unlist(strsplit(w, split = ' ', fixed = T))[1]
]
}
})
# first
microbenchmark({
dt[
, first_l := unlist(strsplit(w, split = ' ', fixed = T))[1]
, by = 1:nrow(dt)
]
})
# second
first_l_f <- function(sd)
{
strsplit(sd, split = ' ', fixed = T) %>%
do.call(rbind, .) %>%
`[`(,1)
}
dt[, first_l := NULL]
microbenchmark({
dt[
, first_l := .(first_l_f(w))
]
})
# third
first_l_f2 <- function(sd)
{
strsplit(sd, split = ' ', fixed = T) %>%
unlist %>%
matrix(nrow = 3) %>%
`[`(1,)
}
dt[, first_l := NULL]
microbenchmark({
dt[
, first_l := .(first_l_f2(w))
]
})
# fourth
rown <- 100000
words <-
sapply(
seq_len(rown)
, function(x){
nwords <- rbinom(1, 10, 0.5)
paste(
sapply(
seq_len(nwords)
, function(x){
paste(sample(letters, rbinom(1, 10, 0.5), replace = T), collapse = '')
}
)
, collapse = ' '
)
}
)
dt <-
data.table(
w = words
, a = sample(letters, rown, replace = T)
, b = runif(rown, -3, 3)
, c = runif(rown, -3, 3)
, e = rnorm(rown)
) %>%
.[, d := 1 + b + c + rnorm(nrow(.))]
first_l_f3 <- function(sd, n)
{
l <- strsplit(sd, split = ' ', fixed = T)
maxl <- max(lengths(l))
sapply(l, "length<-", maxl) %>%
`[`(n,) %>%
as.character
}
microbenchmark({
dt[
, (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x))
]
})
dt[
, (paste0('w_', 1:3)) := lapply(1:3, function(x) first_l_f3(w, x))
]
# chaining
res1 <- dt[a == 'a'][sample(.N, 100)]
res2 <- dt[, .N, a][, N]
res3 <- dt[, coefficients(lm(e ~ d))[1], a][, .(letter = a, coef = V1)]
# piping
samplpe_b <- dt[a %in% head(letters), sample(b, 1)]
res4 <-
dt %>%
.[a %in% head(letters)] %>%
.[,
{
dt0 <- .SD[1:100]
quants <-
dt0[, c] %>%
quantile(seq(0.1, 1, 0.1), na.rm = T)
.(q = quants)
}
, .(cond = b > samplpe_b)
] %>%
glm(
cond ~ q -1
, family = binomial(link = "logit")
, data = .
) %>%
summary %>%
.[[12]]
# function
rm(lm_preds)
lm_preds <- function(
sd, by, n
)
{
if(
n < 100 |
!by[['a']] %in% head(letters, 4)
)
{
res <-
list(
low = NA
, mean = NA
, high = NA
, coefs = NA
)
} else {
lmm <-
lm(
d ~ c + b
, data = sd
)
preds <-
stats::predict.lm(
lmm
, sd
, interval = "prediction"
)
res <-
list(
low = preds[, 2]
, mean = preds[, 1]
, high = preds[, 3]
, coefs = coefficients(lmm)
)
}
res
}
res5 <-
dt %>%
.[e < 0] %>%
.[.[, .I[b > 0]]] %>%
.[, `:=` (
low = as.numeric(lm_preds(.SD, .BY, .N)[[1]])
, mean = as.numeric(lm_preds(.SD, .BY, .N)[[2]])
, high = as.numeric(lm_preds(.SD, .BY, .N)[[3]])
, coef_c = as.numeric(lm_preds(.SD, .BY, .N)[[4]][1])
, coef_b = as.numeric(lm_preds(.SD, .BY, .N)[[4]][2])
, coef_int = as.numeric(lm_preds(.SD, .BY, .N)[[4]][3])
)
, a
] %>%
.[!is.na(mean), -'e', with = F]
# plot
plo <-
res5 %>%
ggplot +
facet_wrap(~ a) +
geom_ribbon(
aes(
x = c * coef_c + b * coef_b + coef_int
, ymin = low
, ymax = high
, fill = a
)
, size = 0.1
, alpha = 0.1
) +
geom_point(
aes(
x = c * coef_c + b * coef_b + coef_int
, y = mean
, color = a
)
, size = 1
) +
geom_point(
aes(
x = c * coef_c + b * coef_b + coef_int
, y = d
)
, size = 1
, color = 'black'
) +
theme_minimal()
print(plo)
KĂ€lla: habr.com
