حول data.table

ستكون هذه المذكرة مثيرة للاهتمام لأولئك الذين يستخدمون مكتبة معالجة بيانات الجدول لـ R - data.table ، وربما يسعدهم رؤية مرونة تطبيقه مع أمثلة مختلفة.

مستوحاة من مثال جيد لزميل ، وآمل أن تكون قد قرأت مقاله بالفعل ، أقترح التعمق أكثر في تحسين التعليمات البرمجية والأداء استنادًا إلى data.table .

مقدمة: من أين تأتي data.table؟


من الأفضل البدء في التعرف على المكتبة قليلاً من بعيد ، أي من هياكل البيانات التي يمكن من خلالها الحصول على كائن data.table (فيما يلي ، DT).

مجموعة مصفوفة


الرمز
## arrays ---------

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

class(arrmatr)

typeof(arrmatr)

is.array(arrmatr)

is.matrix(arrmatr)



أحد هذه البنى عبارة عن صفيف ( ؟ Base :: array ). كما هو الحال في اللغات الأخرى ، المصفوفات متعددة الأبعاد هنا. ومع ذلك ، من المثير للاهتمام ، على سبيل المثال ، أن صفيف ثنائي الأبعاد يبدأ في وراثة الخصائص من فئة المصفوفة (؟ Base :: matrix ) ، والصفيف أحادي البعد ، وهو مهم أيضًا ، لا يرث من المتجه ( ؟ Base :: vector ).

يجب أن يُفهم أنه يجب التحقق من نوع البيانات الموجودة في منشأة قاعدة وظيفية :: typeof ، والتي تُرجع الوصف الداخلي للنوع وفقًا لـ R Internals - بروتوكول اللغة العامة المرتبط بـ C المولود الأول .

أمر آخر لتحديد فئة الكائن هو base :: class، ترجع في حالة المتجهات نوع متجه (يختلف عن الاسم الداخلي ، ولكنه يسمح لك أيضًا بفهم نوع البيانات).

قائمة


من مصفوفة ثنائية الأبعاد ، فهي مصفوفة ، يمكنك الانتقال إلى القائمة ( ؟ Base :: list ).

الرمز
## lists ------------------

mylist <- as.list(arrmatr)

is.vector(mylist)

is.list(mylist)


في هذه الحالة ، تحدث عدة أشياء في وقت واحد:

  • ينهار البعد الثاني للمصفوفة ، أي نحصل على قائمة وناقل.
  • وهكذا ترث القائمة من هذه الفئات. يجب أن يوضع في الاعتبار أن قيمة مفردة (خلية) من خلية مصفوفة المصفوفة سوف تتوافق مع عنصر القائمة.

نظرًا لحقيقة أن القائمة هي أيضًا متجه ، يمكن تطبيق بعض وظائف المتجهات عليها.

Dataframe


من قائمة أو مصفوفة أو متجه ، يمكنك الانتقال إلى إطار البيانات ( ؟ Base :: data.frame ).

الرمز
## data.frames ------------

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

is.list(df)

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


ما هو مثير للاهتمام فيه: يرث إطار البيانات من القائمة! أعمدة إطار البيانات هي خلايا قائمة. سيكون هذا مهمًا في المستقبل عندما نستخدم الوظائف المطبقة على القوائم.

جدول البيانات


يمكنك الحصول على DT ( ؟ Data.table :: data.table ) من إطار بيانات أو قائمة أو متجه أو مصفوفة. على سبيل المثال ، مثل هذا (في المكان).

الرمز
## data.tables -----------------------
library(data.table)

data.table::setDT(df)

is.list(df)

is.data.frame(df)

is.data.table(df)


من المفيد ، مثل إطار البيانات ، أن يرث DT خصائص القائمة.

DT والذاكرة


على عكس جميع الكائنات الأخرى في القاعدة R ، يتم تمرير DTs حسب المرجع. إذا كنت بحاجة إلى النسخ إلى منطقة ذاكرة جديدة ، فأنت بحاجة إلى وظيفة data.table :: copy أو تحتاج إلى تحديد من الكائن القديم.

الرمز
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)


على هذه المقدمة ينتهي. DT هو استمرار لتطوير هياكل البيانات في R ، والذي يحدث بشكل رئيسي بسبب توسيع وتسريع العمليات التي يتم إجراؤها على كائنات فئة إطار البيانات. في هذه الحالة ، يتم الحفاظ على الميراث من البدائيين الآخرين.

بعض الأمثلة على استخدام خصائص data.table


مثل القائمة ...


لا يعد التكرار فوق صفوف إطار البيانات أو DT فكرة جيدة ، لأن رمز الحلقة في لغة R أبطأ بكثير من C ، ومن الممكن جدًا السير عبر الأعمدة في الأعمدة التي عادة ما تكون أقل بكثير. عند التمشي عبر الأعمدة ، تذكر أن كل عمود هو عنصر قائمة يحتوي عادةً على متجه. والعمليات على ناقلات متجهة بشكل جيد في الوظائف الأساسية للغة. يمكنك أيضًا استخدام عوامل التحديد الخاصة بالقوائم والمتجهات: `[[` `` $ ` .

الرمز
## 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)))


المتجه


إذا كانت هناك حاجة للذهاب في سطور DT كبيرة ، فإن كتابة دالة باستخدام vectorization هو الحل الأفضل. ولكن إذا لم يحدث ذلك العمل، ينبغي أن نتذكر أن دورة داخل DT دورة يزال أسرع في مجال البحث ، لأنه يعمل على وC .

دعنا نجرب مثال أكبر مع خطوط 100K. سنقوم بسحب الحرف الأول من الكلمات المضمنة في متجه العمود w .

محدث

الرمز
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))
		]
})


يتكرر التشغيل الأول عبر الخطوط:

الوحدة: مللي ثانية
expr min
{dt [، `: =` (first_l، unlist (strsplit (w، split = ""، ثابت = T)) [1]) ، بمقدار = 1: nrow (dt)]} 439.6217 lq
متوسط الوسيط uq max neval
451.9998 460.1593 456.2505 460.9147 621.4042 100

المرحلة الثانية ، حيث تتم عملية التحويل عن طريق تحويل القائمة إلى مصفوفة وأخذ العناصر على شريحة مع الفهرس 1 (الأخير هو في الواقع التوجه) . سوف أستعيد: التجهيل على مستوى دالة strsplit ، والتي يمكن أن تأخذ المتجه كمدخل. اتضح أن عملية تحويل القائمة إلى مصفوفة هي أصعب بكثير من الرسوم المتحركة نفسها ، ولكنها في هذه الحالة تكون أسرع بكثير من النسخة غير الموجهة.

الوحدة: مللي ثانية
expr min lq يعني متوسط ​​uq max neval
{dt [، `: =` (first_l ،. (First_l_f (w)))]} 93.07916 112.1381 161.9267 149.6863 185.9893 442.5199 100

تسارع متوسط 3 مرات .

الجولة الثالثة ، حيث يتم تغيير مخطط التحول إلى مصفوفة.
الوحدة: مللي ثانية
expr min lq يعني متوسط ​​uq max neval
{dt [، `: =` (first_l ،. (First_l_f2 (w)))]} 32.60481 34.13679 40.4544 35.57115 42.11975 222.972 100

التسارع الوسطي 13 مرة .

عليك تجربة هذه المسألة ، كلما كان ذلك أفضل.

مثال آخر على المتجه ، حيث يوجد أيضًا نص ، لكنه قريب من الظروف الحقيقية: طول الكلمات المختلفة ، وعدد مختلف من الكلمات. مطلوب للحصول على الكلمات الثلاث الأولى. مثل هذا:



هنا لا تعمل الوظيفة السابقة ، حيث تكون المتجهات ذات أطوال مختلفة ، ونحدد حجم المصفوفة. أعد هذا من خلال الخوض في الإنترنت.

الرمز
# 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))
	]



الوحدة: مللي ثانية
expr min lq متوسط
{dt [، `: =` ((لصق 0 ("w_"، 1: 3)) ، strsplit (w، split = ""، ثابت = T))]} 851.7623 916.071 1054.5 1035.199
uq max neval
1178.738 1356.816 100


تم تشغيل البرنامج النصي بمتوسط ​​سرعة 1 ثانية. ليس سيئا.

وجدت طريقة أخرى أكثر اقتصاداkablag:

الرمز
# 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]])
		)
		]

})



متوسط ​​186 ، أرخص 5 مرات ...

متصل بسلسلة واحدة ...


يمكنك العمل مع كائنات DT باستخدام التسلسل. يبدو أن تركيب بنية القوس إلى اليمين ، في الواقع ، السكر.

الرمز
# 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)]



تتدفق من خلال الأنابيب ...


يمكن إجراء نفس العمليات من خلال الأنابيب ، فهي تبدو متشابهة ، ولكنها أكثر ثراءً من الناحية الوظيفية ، حيث يمكنك استخدام أي طرق ، وليس فقط DT. نستمد معاملات الانحدار اللوجستي لبياناتنا الاصطناعية مع عدد من المرشحات على وقود الديزل.

الرمز
# 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]]


الإحصاءات والتعلم الآلي وما إلى ذلك داخل DT


يمكنك استخدام وظائف لامدا ، ولكن في بعض الأحيان يكون من الأفضل إنشاؤها بشكل منفصل ، وتسجيل خط أنابيب تحليل البيانات بالكامل ، ومن ثم تعمل داخل DT. تم إثراء المثال بجميع الميزات المذكورة أعلاه ، بالإضافة إلى بعض الأشياء المفيدة من ترسانة DT (مثل الوصول إلى DT نفسها داخل DT حسب المرجع ، في بعض الأحيان لا يتم إدراجها بالتسلسل ، ولكن في بعض الأحيان).

الرمز
# 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)



استنتاج


آمل أن أكون قد تمكنت من إنشاء صورة كاملة ، ولكن بالطبع ، غير كاملة ، لشيء مثل data.table ، بدءًا من خصائصه المتعلقة بالميراث من فئات R وتنتهي بالرقائق الخاصة بها والمناطق المحيطة بها من عناصر مرتبة. آمل أن يساعدك هذا على تعلم واستخدام هذه المكتبة بشكل أفضل للعمل والمتعة .



شكرا!

كود كامل


الرمز
## 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