Runt data.table

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 frÄn kollegan, 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:

Runt data.table

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.

Runt data.table

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

Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar đŸ”„ Köp pĂ„litlig webbhotell med DDoS-skydd, VPS VDS-servrar | ProHoster