Alrededor de data.table

Esta nota será interesante para aquellos que usan la biblioteca de procesamiento de datos de tablas para R - data.table y, tal vez, se alegrarán de ver la flexibilidad de su aplicación con varios ejemplos.

Inspirado en un buen ejemplo de un colega , y con la esperanza de que ya haya leído su artículo, sugiero profundizar en la optimización del código y el rendimiento basado en data.table .

Introducción: ¿de dónde viene data.table?


Es mejor comenzar a familiarizarse con la biblioteca un poco desde lejos, es decir, a partir de las estructuras de datos a partir de las cuales se puede obtener el objeto data.table (en adelante, DT).

Formación


El código
## arrays ---------

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

class(arrmatr)

typeof(arrmatr)

is.array(arrmatr)

is.matrix(arrmatr)



Una de estas estructuras es una matriz ( ? Base :: array ). Como en otros idiomas, las matrices son multidimensionales aquí. Sin embargo, es interesante que, por ejemplo, una matriz bidimensional comience a heredar propiedades de la clase de matriz (? Base :: matriz ), y una matriz unidimensional, que también es importante, no hereda del vector ( ? Base :: vector ).

Se debe entender que el tipo de datos contenidos en una instalación se debe comprobar la función de base :: el typeof , que devuelve la descripción interna del tipo de acuerdo con las partes internas R - protocolo de lenguaje común asociado con el primer nacido el C .

Otro comando para determinar la clase de un objeto es base :: class, devuelve en el caso de vectores un tipo de vector (difiere del nombre interno, pero también le permite comprender el tipo de datos).

Lista


Desde una matriz bidimensional, es una matriz, puede ir a la lista ( ? Base :: list ).

El código
## lists ------------------

mylist <- as.list(arrmatr)

is.vector(mylist)

is.list(mylist)


En este caso, suceden varias cosas a la vez:

  • La segunda dimensión de la matriz colapsa, es decir, obtenemos una lista y un vector.
  • La lista hereda así de estas clases. Debe tenerse en cuenta que un solo valor (escalar) de la celda de la matriz matriz corresponderá al elemento de la lista.

Debido al hecho de que la lista también es un vector, se le pueden aplicar algunas funciones para vectores.

Marco de datos


Desde una lista, matriz o vector, puede ir al marco de datos ( ? Base :: data.frame ).

El código
## data.frames ------------

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

is.list(df)

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


Lo interesante es que el marco de datos hereda de la lista. Las columnas del marco de datos son celdas de lista. Esto será importante en el futuro cuando usemos las funciones aplicadas a las listas.

tabla de datos


Puede obtener DT ( ? Data.table :: data.table ) de un marco de datos , lista, vector o matriz. Por ejemplo, así (en su lugar).

El código
## data.tables -----------------------
library(data.table)

data.table::setDT(df)

is.list(df)

is.data.frame(df)

is.data.table(df)


Es útil que, como el marco de datos, DT herede las propiedades de la lista.

DT y memoria


A diferencia de todos los demás objetos en la base R, los DT se pasan por referencia. Si necesita copiar en una nueva área de memoria, necesita la función data.table :: copy o debe hacer una selección del objeto antiguo.

El código
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)


En esta introducción llega a su fin. DT es una continuación del desarrollo de estructuras de datos en R, que ocurre principalmente debido a la expansión y aceleración de operaciones realizadas en objetos de la clase de marco de datos. En este caso, se preserva la herencia de otras primitivas.

Algunos ejemplos de uso de propiedades data.table


Como una lista ...


Iterar sobre las filas de un marco de datos o DT no es una buena idea, ya que el código de bucle en el lenguaje R es mucho más lento que C , y es bastante posible recorrer las columnas en columnas que generalmente son mucho menos. Al recorrer las columnas, recuerde que cada columna es un elemento de la lista, que generalmente contiene un vector. Y las operaciones en vectores están bien vectorizadas en las funciones básicas del lenguaje. También puede usar los operadores de selección específicos para listas y vectores: `[[`, `$` .

El código
## 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)))


Vectorización


Si es necesario pasar por las líneas de un DT grande, escribir una función con vectorización es la mejor solución. Pero si no lo hace el trabajo, se debe recordar que el ciclo dentro del DT todavía ciclo más rápido en el R , porque se ejecuta en el C .

Probemos con un ejemplo más grande con 100K líneas. Extraeremos la primera letra de las palabras incluidas en el vector de columna w .

Actualizado

El código
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))
		]
})


La primera ejecución itera a través de las líneas:

Unidad: milisegundos
expr min
{dt [, `: =` (first_l, unlist (strsplit (w, split = "", fixed = T)) [1]), by = 1: nrow (dt)]} 439.6217
lq media mediana uq max neval
451.9998 460.1593 456.2505 460.9147 621.4042 100

La segunda ejecución, donde la vectorización se realiza convirtiendo la lista en una matriz y tomando elementos en un segmento con el índice 1 (el último es en realidad vectorización) . Recuperaré: vectorización al nivel de la función strsplit , que puede tomar un vector como entrada. Resulta que el proceso de convertir la lista en una matriz es mucho más difícil que la vectorización en sí, pero en este caso es mucho más rápido que la versión no vectorizada.

Unidad: milisegundos
expr min lq media mediana uq max neval
{dt [, `: =` (first_l ,. (First_l_f (w)))]} 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100

La aceleración media es 3 veces .

La tercera ejecución, donde se cambia el esquema de transformación en una matriz.
Unidad: milisegundos
expr min lq media mediana uq max neval
{dt [, `: =` (first_l ,. (First_l_f2 (w)))]} 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100

La aceleración media es de 13 veces .

Tienes que experimentar con este asunto, cuanto más, mejor.

Otro ejemplo con vectorización, donde también hay texto, pero está cerca de condiciones reales: diferente longitud de palabras, diferente número de palabras. Se requiere para obtener las primeras 3 palabras. De esta manera:



Aquí la función anterior no funciona, ya que los vectores son de diferentes longitudes, y establecemos el tamaño de la matriz. Rehaga esto profundizando en Internet.

El código
# 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))
	]



Unidad: milisegundos
expr min lq mediana media
{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


El script se ejecutó a una velocidad promedio de 1 segundo. No está mal.

Otra forma más económica encontradakablag:

El código
# 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]])
		)
		]

})



Mediana 186, 5 veces más barata ...

Conectado por una cadena ...


Puede trabajar con objetos DT mediante encadenamiento. Parece como enganchar la sintaxis de paréntesis a la derecha, de hecho, azúcar.

El código
# 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)]



Fluyendo a través de tuberías ...


Las mismas operaciones se pueden realizar a través de tuberías, se ve similar, pero funcionalmente más rico, ya que puede usar cualquier método, no solo DT. Derivamos los coeficientes de regresión logística para nuestros datos sintéticos con varios filtros de combustible diesel.

El código
# 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]]


Estadísticas, aprendizaje automático, etc. dentro de DT


Puede usar funciones lambda, pero a veces es mejor crearlas por separado, registrar toda la canalización de análisis de datos y luego funcionan dentro del DT. El ejemplo se enriquece con todas las características anteriores, además de varias cosas útiles del arsenal de DT (como acceder al propio DT dentro del DT por referencia, a veces no insertado secuencialmente, pero para ser).

El código
# 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)



Conclusión


Espero haber podido crear una imagen completa, pero, por supuesto, no completa, de un objeto como data.table, comenzando por sus propiedades relacionadas con la herencia de las clases R y terminando con sus propios chips y alrededores de elementos tidyverse. Espero que esto te ayude a aprender y usar mejor esta biblioteca para trabajar y divertirte .



¡Gracias!

Código completo


El código
## 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