Autour de data.table

Cette note sera intéressante pour ceux qui utilisent la bibliothèque de traitement de données de table pour R-data.table, et, peut-être, sera heureuse de voir la flexibilité de son application avec divers exemples.

Inspiré par un bon exemple de collègue , et en espérant que vous ayez déjà lu son article, je vous suggère de creuser davantage pour optimiser le code et les performances en fonction de data.table .

Introduction: d'où vient data.table?


Il est préférable de commencer à se familiariser un peu avec la bibliothèque, à savoir à partir des structures de données à partir desquelles l'objet data.table (ci-après, DT) peut être obtenu.

Array


Le code
## arrays ---------

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

class(arrmatr)

typeof(arrmatr)

is.array(arrmatr)

is.matrix(arrmatr)



L'une de ces structures est un tableau ( ? Base :: array ). Comme dans d'autres langues, les tableaux sont multidimensionnels ici. Cependant, il est intéressant, par exemple, qu'un tableau à deux dimensions commence à hériter des propriétés de la classe de matrice (? Base :: matrix ), et qu'un tableau à une dimension, qui est également important, n'hérite pas du vecteur ( ? Base :: vector ).

Il faut comprendre que le type de données contenues dans une installation doit être vérifiée fonction de base :: le typeof , qui renvoie la description interne du type selon les R Internes - protocole de langage commun associé au premier-né le C .

Une autre commande pour déterminer la classe d'un objet est base :: class, renvoie dans le cas des vecteurs un type de vecteur (il diffère du nom interne, mais permet également de comprendre le type de données).

liste


A partir d'un tableau à deux dimensions, c'est une matrice, vous pouvez aller dans la liste ( ? Base :: list ).

Le code
## lists ------------------

mylist <- as.list(arrmatr)

is.vector(mylist)

is.list(mylist)


Dans ce cas, plusieurs choses se produisent à la fois:

  • La deuxième dimension de la matrice s'effondre, c'est-à-dire que nous obtenons à la fois une liste et un vecteur.
  • La liste hérite donc de ces classes. Il convient de garder à l'esprit qu'une seule valeur (scalaire) de la cellule du tableau matriciel correspondra à l'élément de liste.

Étant donné que la liste est également un vecteur, certaines fonctions pour les vecteurs peuvent lui être appliquées.

Trame de données


A partir d'une liste, d'une matrice ou d'un vecteur, vous pouvez accéder au bloc de données ( ? Base :: data.frame ).

Le code
## data.frames ------------

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

is.list(df)

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


Ce qui est intéressant: le bloc de données hérite de la liste! Les colonnes du bloc de données sont des cellules de liste. Cela sera important à l'avenir lorsque nous utiliserons les fonctions appliquées aux listes.

table de données


Vous pouvez obtenir DT ( ? Data.table :: data.table ) à partir d'une trame de données , d'une liste, d'un vecteur ou d'une matrice. Par exemple, comme ça (en place).

Le code
## data.tables -----------------------
library(data.table)

data.table::setDT(df)

is.list(df)

is.data.frame(df)

is.data.table(df)


Il est utile que, comme le bloc de données, DT hérite des propriétés de la liste.

DT et mémoire


Contrairement à tous les autres objets de la base R, les DT sont passés par référence. Si vous devez copier dans une nouvelle zone de mémoire, vous avez besoin de la fonction data.table :: copy ou vous devez faire une sélection à partir de l'ancien objet.

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


Sur cette introduction prend fin. DT est une continuation du développement des structures de données dans R, qui se produit principalement en raison de l'expansion et de l'accélération des opérations effectuées sur les objets de la classe de trame de données. Dans ce cas, l'héritage d'autres primitives est préservé.

Quelques exemples d'utilisation des propriétés data.table


Comme une liste ...


Itérer sur les lignes d'une trame de données ou d'un DT n'est pas une bonne idée, car le code de boucle dans le langage R est beaucoup plus lent que C , et il est tout à fait possible de parcourir les colonnes en colonnes qui sont généralement beaucoup moins. En parcourant les colonnes, n'oubliez pas que chaque colonne est un élément de liste, qui contient généralement un vecteur. Et les opérations sur les vecteurs sont bien vectorisées dans les fonctions de base du langage. Vous pouvez également utiliser les opérateurs de sélection spécifiques aux listes et aux vecteurs: `[[`, `$` .

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


Vectorisation


S'il est nécessaire de parcourir les lignes d'un grand DT, l'écriture d'une fonction avec vectorisation est la meilleure solution. Mais si cela ne fonctionne pas, il faut se rappeler que le cycle dans le cycle de DT encore plus rapide dans le R , parce qu'il fonctionne sur C .

Essayons un exemple plus grand avec 100K lignes. Nous tirerons la première lettre des mots inclus dans le vecteur colonne w .

Mise à jour

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


La première exécution parcourt les lignes:

Unité: millisecondes
expr min
{dt [, `: =` (first_l, unlist (strsplit (w, split = "", fixed = T)) [1]), by = 1: nrow (dt)]} 439.6217 lq
moyenne médiane uq max neval
451.9998 460.1593 456.2505 460.9147 621.4042 100

La deuxième exécution, où la vectorisation se fait en transformant la liste en matrice et en prenant des éléments sur une tranche d'index 1 (ce dernier est en fait la vectorisation) . Je vais récupérer: la vectorisation au niveau de la fonction strsplit , qui peut prendre un vecteur en entrée. Il s'avère que le processus de transformation de la liste en matrice est beaucoup plus difficile que la vectorisation elle-même, mais dans ce cas, il est beaucoup plus rapide que la version non vectorisée.

Unité: millisecondes
expr min lq moyenne médiane uq max neval
{dt [, `: =` (first_l,. (First_l_f (w)))]]} 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100

L'accélération médiane est de 3 fois .

La troisième exécution, où le schéma de transformation en matrice est modifié.
Unité: millisecondes
expr min lq moyenne médiane uq max neval
{dt [, `: =` (first_l,. (First_l_f2 (w))))]} 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100

L'accélération médiane est de 13 fois .

Vous devez expérimenter cette question, le plus - le mieux.

Un autre exemple avec la vectorisation, où il y a aussi du texte, mais qui est proche des conditions réelles: longueur de mots différente, nombre de mots différent. Il est nécessaire d'obtenir les 3 premiers mots. Comme ceci:



Ici, la fonction précédente ne fonctionne pas, car les vecteurs sont de longueurs différentes, et nous définissons la taille de la matrice. Refaire cela en plongeant dans Internet.

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



Unité: millisecondes
expr min lq médiane moyenne
{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


Le script s'est exécuté à une vitesse moyenne de 1 seconde. Pas mal.

Un autre moyen plus économique trouvékablag:

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

})



Médiane 186, 5 fois moins chère ...

Connecté par une chaîne ...


Vous pouvez travailler avec des objets DT à l'aide du chaînage. Cela ressemble à accrocher la syntaxe de parenthèse à droite, en fait, le sucre.

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



Circulant dans les tuyaux ...


Les mêmes opérations peuvent être effectuées via la tuyauterie, elles semblent similaires, mais plus riches en fonctionnalités, car vous pouvez utiliser n'importe quelle méthode, pas seulement DT. Nous dérivons les coefficients de régression logistique pour nos données synthétiques avec un certain nombre de filtres sur le carburant diesel.

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


Statistiques, apprentissage automatique, etc. dans DT


Vous pouvez utiliser les fonctions lambda, mais il est parfois préférable de les créer séparément, d’enregistrer l’ensemble du pipeline d’analyse de données, puis de travailler à l’intérieur du DT. L'exemple est enrichi avec toutes les fonctionnalités ci-dessus, ainsi que quelques éléments utiles de l'arsenal DT (tels que l'accès au DT lui-même à l'intérieur du DT par référence, parfois non inséré séquentiellement, mais pour être).

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



Conclusion


J'espère que j'ai pu créer une image complète, mais, bien sûr, incomplète, d'un objet tel que data.table, à partir de ses propriétés liées à l'héritage des classes R et se terminant par ses propres puces et son environnement à partir d'éléments bien rangés. J'espère que cela vous aidera à mieux apprendre et utiliser cette bibliothèque pour le travail et le plaisir .



Remercier!

Code complet


Le 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