Rund um data.table

Dieser Hinweis ist für diejenigen interessant, die die Tabellendatenverarbeitungsbibliothek für R - data.table verwenden, und freut sich möglicherweise über die Flexibilität ihrer Anwendung anhand verschiedener Beispiele.

Inspiriert von einem guten Beispiel eines Kollegen und in der Hoffnung, dass Sie seinen Artikel bereits gelesen haben, schlage ich vor, tiefer in die Optimierung von Code und Leistung basierend auf data.table einzusteigen .

Einführung: Woher kommt data.table?


Es ist am besten, die Bibliothek ein wenig von weitem kennenzulernen, nämlich von den Datenstrukturen, aus denen das Objekt data.table (im Folgenden DT) erhalten werden kann.

Array


Der Code
## arrays ---------

arrmatr <- array(1:20, c(4,5))

class(arrmatr)

typeof(arrmatr)

is.array(arrmatr)

is.matrix(arrmatr)



Eine dieser Strukturen ist ein Array ( ? Base :: array ). Wie in anderen Sprachen sind Arrays hier mehrdimensional. Es ist jedoch interessant, dass beispielsweise ein zweidimensionales Array beginnt, Eigenschaften von der Matrixklasse (? Base :: matrix ) zu erben , und ein eindimensionales Array, das ebenfalls wichtig ist, nicht vom Vektor ( ? Base :: vector ) erbt .

Es sollte klar sein, dass die Art der Daten , die in einer Einrichtung enthalten ist, sollte Funktion überprüft wird Basis :: die Typeof , die die interne Beschreibung des Typs liefert gemäß dem R - Interna - Common Language - Protokoll mit dem ersten Sohn assoziiert C .

Ein weiterer Befehl zum Bestimmen der Klasse eines Objekts ist base :: class, gibt bei Vektoren einen Vektortyp zurück (er unterscheidet sich vom internen Namen, ermöglicht Ihnen aber auch das Verständnis des Datentyps).

Liste


Aus einem zweidimensionalen Array, einer Matrix, können Sie zur Liste ( ? Base :: list ) wechseln .

Der Code
## lists ------------------

mylist <- as.list(arrmatr)

is.vector(mylist)

is.list(mylist)


In diesem Fall passieren mehrere Dinge gleichzeitig:

  • Die zweite Dimension der Matrix kollabiert, dh wir erhalten sowohl eine Liste als auch einen Vektor.
  • Die Liste erbt somit von diesen Klassen. Es ist zu beachten, dass ein einzelner (skalarer) Wert aus der Zelle des Matrixarrays dem Listenelement entspricht.

Aufgrund der Tatsache, dass die Liste auch ein Vektor ist, können einige Funktionen für Vektoren darauf angewendet werden.

Datenrahmen


Über eine Liste, Matrix oder einen Vektor können Sie zum Datenrahmen ( ? Base :: data.frame ) wechseln .

Der Code
## data.frames ------------

df <- as.data.frame(arrmatr)
df2 <- as.data.frame(mylist)

is.list(df)

df$V6 <- df$V1 + df$V2


Was daran interessant ist: Der Datenrahmen erbt von der Liste! Die Spalten des Datenrahmens sind Listenzellen. Dies wird in Zukunft wichtig sein, wenn wir die Funktionen verwenden, die auf Listen angewendet werden.

Datentabelle


Sie können erhalten DT ( ? Data.table :: data.table ) von einem Datenrahmen , Liste, Vektor oder eine Matrix. Zum Beispiel so (an Ort und Stelle).

Der Code
## data.tables -----------------------
library(data.table)

data.table::setDT(df)

is.list(df)

is.data.frame(df)

is.data.table(df)


Es ist nützlich, dass DT wie der Datenrahmen die Eigenschaften der Liste erbt.

DT und Speicher


Im Gegensatz zu allen anderen Objekten in der R-Basis werden DTs als Referenz übergeben. Wenn Sie eine Kopie in einen neuen Speicherbereich erstellen möchten , benötigen Sie die Funktion data.table :: copy , oder Sie müssen eine Auswahl aus dem alten Objekt treffen.

Der Code
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)


Mit dieser Einführung geht ein Ende. DT ist eine Fortsetzung der Entwicklung von Datenstrukturen in R, die hauptsächlich aufgrund der Erweiterung und Beschleunigung von Operationen an Objekten der Datenrahmenklasse erfolgt. In diesem Fall bleibt die Vererbung von anderen Grundelementen erhalten.

Einige Beispiele für die Verwendung von data.table-Eigenschaften


Wie eine Liste ...


Das Iterieren über die Zeilen eines Datenrahmens oder DT ist keine gute Idee, da der Schleifencode in der R- Sprache viel langsamer als C ist und es durchaus möglich ist, die Spalten in Spalten zu durchlaufen, die normalerweise viel kleiner sind. Denken Sie beim Durchlaufen der Spalten daran, dass jede Spalte ein Listenelement ist, das normalerweise einen Vektor enthält. Und Operationen an Vektoren sind in den Grundfunktionen der Sprache gut vektorisiert. Sie können auch die für Operatoren und Vektoren spezifischen Auswahloperatoren verwenden: `[[`, `$` .

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


Vektorisierung


Wenn Sie die Zeilen eines großen DT durchgehen müssen, ist das Schreiben einer Funktion mit Vektorisierung die beste Lösung. Wenn es jedoch nicht funktioniert, sollte beachtet werden, dass der Zyklus innerhalb des DT im R immer noch schneller läuft, da er auf dem C ausgeführt wird .

Versuchen wir ein größeres Beispiel mit 100K-Zeilen. Wir werden den ersten Buchstaben aus den Wörtern ziehen, die im Spaltenvektor w enthalten sind .

Aktualisiert

Der Code
library(magrittr)
library(microbenchmark)

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

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


Der erste Lauf durchläuft die Zeilen:

Einheit: Millisekunden
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

Der zweite Lauf, bei dem die Vektorisierung durchgeführt wird, indem die Liste in eine Matrix umgewandelt und Elemente auf einem Slice mit Index 1 aufgenommen werden (letzteres ist tatsächlich eine Vektorisierung) . Ich werde mich erholen: Vektorisierung auf der Ebene der strsplit- Funktion , die einen Vektor als Eingabe verwenden kann. Es stellt sich heraus, dass das Umwandeln der Liste in eine Matrix viel schwieriger ist als die Vektorisierung selbst, in diesem Fall jedoch viel schneller als die nicht vektorisierte Version.

Einheit: Millisekunden
expr min lq mittlerer Median uq max neval
{dt [, `: =` (first_l ,. (First_l_f (w)))]} 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100

Die mittlere Beschleunigung beträgt das Dreifache .

Der dritte Lauf, bei dem das Schema der Transformation in eine Matrix geändert wird.
Einheit: Millisekunden
expr min lq mittlerer Median uq max neval
{dt [, `: =` (first_l ,. (First_l_f2 (w)))]} 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100

Die mittlere Beschleunigung beträgt das 13-fache .

Man muss mit dieser Angelegenheit experimentieren, je mehr - desto besser.

Ein weiteres Beispiel für die Vektorisierung, bei der es auch Text gibt, der jedoch den realen Bedingungen nahe kommt: unterschiedliche Länge von Wörtern, unterschiedliche Anzahl von Wörtern. Es ist erforderlich, die ersten 3 Wörter zu erhalten. So:



Hier funktioniert die vorherige Funktion nicht, da die Vektoren unterschiedlich lang sind und wir die Größe der Matrix festlegen. Wiederholen Sie dies, indem Sie in das Internet eintauchen.

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



Einheit: Millisekunden
expr min lq mittlerer 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


Das Skript lief mit einer Durchschnittsgeschwindigkeit von 1 Sekunde. Nicht schlecht.

Ein anderer, wirtschaftlicherer Weg gefundenkablag::

Der Code
# fifth

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(.))]

microbenchmark({
	
	w_split <- dt[
		, data.table::tstrsplit(w, split = ' ', fixed = T, keep = 1L:3L)
		]
	
	dt[
		, `:=` (
			w_1 = as.character(w_split[[1]])
			, w_2 = as.character(w_split[[2]])
			, w_3 = as.character(w_split[[3]])
		)
		]

})



Median 186, 5 mal billiger ...

Verbunden durch eine Kette ...


Sie können mit DT-Objekten mithilfe der Verkettung arbeiten. Es sieht so aus, als würde man die Klammer-Syntax rechts einfügen, tatsächlich Zucker.

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



Durch Rohre fließen ...


Die gleichen Vorgänge können über Rohrleitungen ausgeführt werden. Sie sehen ähnlich aus, sind jedoch funktional umfangreicher, da Sie alle Methoden verwenden können, nicht nur DT. Wir leiten die logistischen Regressionskoeffizienten für unsere synthetischen Daten mit einer Reihe von Filtern für Dieselkraftstoff ab.

Der Code
# 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, maschinelles Lernen usw. in DT


Sie können Lambda-Funktionen verwenden, aber manchmal ist es besser, sie separat zu erstellen, die gesamte Datenanalyse-Pipeline zu registrieren und sie dann im DT zu arbeiten. Das Beispiel ist mit allen oben genannten Funktionen sowie einigen nützlichen Dingen aus dem DT-Arsenal angereichert (z. B. Zugriff auf die DT selbst innerhalb der DT durch Referenz, manchmal nicht nacheinander eingefügt, sondern zu sein).

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



Fazit


Ich hoffe, dass ich in der Lage war, ein vollständiges, aber natürlich nicht vollständiges Bild eines Objekts wie data.table zu erstellen, beginnend mit seinen Eigenschaften in Bezug auf die Vererbung von R-Klassen und endend mit seinen eigenen Chips und seiner Umgebung aus aufgeräumten Elementen. Ich hoffe, dies hilft Ihnen dabei, diese Bibliothek besser zu lernen und für Arbeit und Spaß zu nutzen .



Danke!

Vollständiger Code


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


All Articles