Em torno de data.table

Esta nota será interessante para aqueles que usam a biblioteca de processamento de dados de tabela para R - data.table e, talvez, fique feliz em ver a flexibilidade de sua aplicação com vários exemplos.

Inspirado em um bom exemplo de um colega , e esperando que você já leia o artigo dele, sugiro aprofundar a otimização do código e do desempenho com base em data.table .

Introdução: de onde vem o data.table?


É melhor começar a familiarizar-se um pouco com a biblioteca, a partir das estruturas de dados a partir das quais o objeto data.table (daqui em diante, DT) pode ser obtido.

Matriz


O código
## arrays ---------

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

class(arrmatr)

typeof(arrmatr)

is.array(arrmatr)

is.matrix(arrmatr)



Uma dessas estruturas é uma matriz ( ? Base :: array ). Como em outros idiomas, as matrizes são multidimensionais aqui. No entanto, é interessante que, por exemplo, uma matriz bidimensional comece a herdar propriedades da classe matrix (? Base :: matrix ) e uma matriz unidimensional, que também é importante, não herda do vetor ( ? Base :: vector ).

Deve ser entendido que o tipo de dados contidos em uma instalação deve ser verificado função de base :: o typeof , que retorna a descrição interna do tipo de acordo com os R Internas - protocolo de linguagem comum associado com o primeiro-nascido a C .

Outro comando para determinar a classe de um objeto é base :: class, retorna no caso de vetores um tipo de vetor (difere do nome interno, mas também permite entender o tipo de dados).

Lista


A partir de uma matriz bidimensional, é uma matriz, você pode ir para a lista ( ? Base :: list ).

O código
## lists ------------------

mylist <- as.list(arrmatr)

is.vector(mylist)

is.list(mylist)


Nesse caso, várias coisas acontecem ao mesmo tempo:

  • A segunda dimensão da matriz entra em colapso, ou seja, obtemos uma lista e um vetor.
  • A lista, portanto, herda dessas classes. Deve-se ter em mente que um único valor (escalar) da célula da matriz matriz corresponderá ao item da lista.

Devido ao fato de a lista também ser um vetor, algumas funções para vetores podem ser aplicadas a ela.

Quadro de dados


A partir de uma lista, matriz ou vetor, você pode ir para o quadro de dados ( ? Base :: data.frame ).

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

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

is.list(df)

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


O que é interessante: o quadro de dados herda da lista! As colunas do quadro de dados são células da lista. Isso será importante no futuro quando usarmos as funções aplicadas às listas.

Tabela de dados


Você pode obter a DT ( ? Data.table :: data.table ) a partir de um quadro de dados , lista, vetor ou matriz. Por exemplo, assim (no local).

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

data.table::setDT(df)

is.list(df)

is.data.frame(df)

is.data.table(df)


É útil que, como o quadro de dados, a DT herda as propriedades da lista.

TD e memória


Diferentemente de todos os outros objetos na base R, as DTs são passadas por referência. Se você precisar copiar para uma nova área de memória, precisará da função data.table :: copy ou precisará fazer uma seleção do objeto antigo.

O 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)


Nesta introdução, chega ao fim. DT é uma continuação do desenvolvimento de estruturas de dados em R, o que ocorre principalmente devido à expansão e aceleração das operações executadas em objetos da classe de quadro de dados. Nesse caso, a herança de outras primitivas é preservada.

Alguns exemplos de uso das propriedades data.table


Como uma lista ...


A iteração nas linhas de um quadro de dados ou DT não é uma boa ideia, pois o código de loop na linguagem R é muito mais lento que C e é bem possível percorrer as colunas em colunas que geralmente são muito menores. Percorrendo as colunas, lembre-se de que cada coluna é um item da lista, que geralmente contém um vetor. E operações em vetores são bem vetorizadas nas funções básicas da linguagem. Você também pode usar os operadores de seleção específicos para listas e vetores: `[[`, `$` .

O 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)))


Vetorização


Se houver necessidade de percorrer as linhas de uma DT grande, escrever uma função com vetorização é a melhor solução. Mas se isso não funcionar, deve-se lembrar que o ciclo dentro do DT ciclo ainda mais rápido em R , porque ele é executado em a C .

Vamos tentar um exemplo maior com 100 mil linhas. Retiraremos a primeira letra das palavras incluídas na coluna vetor w .

Atualizada

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


A primeira execução percorre as linhas:

Unidade: milissegundos
expr min
{dt [, `: =` (first_l, unlist (strsplit (w, split = "", fixado = T)) [1]), por = 1: nrow (dt)]} 439,6217 lq
mean mediana uq max neval
451,9998 460,1593 456,2505 460,9147 621,4042 100

A segunda execução, onde a vetorização é feita, transformando a lista em uma matriz e colocando elementos em uma fatia com o índice 1 (o último é na verdade vetorização) . Recuperarei: vetorização no nível da função strsplit , que pode receber um vetor como entrada. Acontece que o processo de transformar a lista em uma matriz é muito mais difícil que a própria vetorização, mas, neste caso, é muito mais rápido que a versão não vetorizada.

Unidade: milissegundos
expr min lq média mediana uq max neval
{dt [, `: =` (first_l ,. (First_l_f (w)))]} 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100

A aceleração mediana é 3 vezes .

A terceira execução, onde o esquema de transformação em uma matriz é alterado.
Unidade: milissegundos
expr min lq média mediana uq max neval
{dt [, `: =` (first_l ,. (First_l_f2 (w)))]} 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100

A aceleração mediana é 13 vezes .

Você precisa experimentar esse assunto, quanto mais - melhor.

Outro exemplo com vetorização, onde também há texto, mas está próximo de condições reais: comprimento diferente de palavras, número diferente de palavras. É necessário obter as 3 primeiras palavras. Assim:



Aqui, a função anterior não funciona, pois os vetores são de comprimentos diferentes e definimos o tamanho da matriz. Refaça isso pesquisando na Internet.

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



Unidade: milissegundos
expr min lq média mediana
{dt [, `: =` ((colar0 ("w_", 1: 3)), strsplit (w, split = "", fixo = T))]} 851.7623 916.071 1054.5 1035.199
uq max neval
1178.738 1356.816 100


O script foi executado a uma velocidade média de 1 segundo. Não é ruim.

Outra maneira mais econômica encontradakablag:

O 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 vezes mais barata ...

Conectado por uma corrente ...


Você pode trabalhar com objetos DT usando encadeamento. Parece enganchar a sintaxe entre parênteses à direita, de fato, açúcar.

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



Fluindo através de tubos ...


As mesmas operações podem ser feitas por meio de tubulação, parece semelhante, mas funcionalmente mais rica, pois você pode usar qualquer método, não apenas o DT. Obtemos os coeficientes de regressão logística para nossos dados sintéticos com vários filtros no combustível diesel.

O 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]]


Estatísticas, aprendizado de máquina etc. dentro da DT


Você pode usar funções lambda, mas às vezes é melhor criá-las separadamente, registrar todo o pipeline de análise de dados e, em seguida, elas funcionam na DT. O exemplo é enriquecido com todos os recursos acima, além de várias coisas úteis do arsenal da TD (como acessar a própria TD dentro da TD por referência, às vezes não inserida sequencialmente, mas sim).

O 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)



Conclusão


Espero ter conseguido criar uma imagem completa, mas, é claro, não completa, de um objeto como data.table, começando pelas propriedades relacionadas à herança das classes R e terminando com seus próprios chips e o ambiente a partir de elementos arrumados. Espero que isso ajude você a aprender e usar melhor esta biblioteca para trabalhar e se divertir .



Obrigado!

Código completo


O 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