我正在编写一个包来分析@Transactional
中的高吞吐量动物行为数据。
数据是多变量时间序列。
我选择使用R
代表他们,我觉得这很方便。
对于一只动物,我会有类似的东西:
data.tables
然而,我和我的用户和许多动物一起工作,这些动物具有不同的任意治疗,条件和其他变量,这些变量在每只动物中不变。
最后,我发现代表数据的最方便的方法是合并来自所有动物的行为和单个数据表中的所有实验,并使用我设置的额外列作为关键,对于这些重复变量中的每一个"。
所以,从概念上讲,就是这样:
one_animal_dt <- data.table(t=1:20, x=rnorm(20), y=rnorm(20))
这种方式可以非常方便地计算每只动物的摘要,同时对所有生物信息(治疗等)不可知。
在实践中,我们为每只动物提供了数百万(而不是20)个连续读取,因此我们为方便起见而添加的列包含高度重复的值,这不是内存效率。
有没有办法压缩这个高度冗余的密钥而不会丢失表格的结构(即列)?理想情况下,我不想强迫我的用户自己使用JOIN。
答案 0 :(得分:4)
我们假设,我们是一名数据库管理员,负责在SQL数据库中有效地实现这一点。 数据库规范化的目标之一是减少冗余。
根据OP的描述,每只动物有很多(大约1M)的观察结果(多变量,纵向数据),而动物的数量似乎要小得多。
因此,每只动物的常数(或不变)基础数据,例如treatment
,date
,应与observations
分开保存。
animal_id
是唯一的(顾名思义), animal_id
是两个表的关键。
(请注意,这是使用treatment
作为关键字的Mallick's answer的主要区别,不保证是唯一的,即两只动物可以接受相同的治疗,并且还会增加冗余。)
为了示范,正在为10只动物创建更现实的“基准”数据,每只动物观察1次:
library(data.table) # CRAN version 1.10.4 used
# create observations
n_obs <- 1E6L
n_animals <-10L
set.seed(123L)
observations <- data.table(
animal_id = rep(seq_len(n_animals), each = n_obs),
t = rep(seq_len(n_obs), n_animals),
x = rnorm(n_animals * n_obs),
y = rnorm(n_animals * n_obs))
# create animal base data
animals = data.table(
animal_id = seq_len(n_animals),
treatment = wakefield::string(n_animals),
date = wakefield::date_stamp(n_animals, random = TRUE))
此处wakefield
包用于创建虚拟名称和日期。请注意,animal_id
的类型为整数。
> str(observations) Classes ‘data.table’ and 'data.frame': 10000000 obs. of 4 variables: $ animal_id: int 1 1 1 1 1 1 1 1 1 1 ... $ t : int 1 2 3 4 5 6 7 8 9 10 ... $ x : num -0.5605 -0.2302 1.5587 0.0705 0.1293 ... $ y : num 0.696 -0.537 -3.043 1.849 -1.085 ... - attr(*, ".internal.selfref")=<externalptr> > str(animals) Classes ‘data.table’ and 'data.frame': 10 obs. of 3 variables: $ animal_id: int 1 2 3 4 5 6 7 8 9 10 $ treatment:Classes 'variable', 'character' atomic [1:10] MADxZ9c6fN ymoJHnvrRx ifdtywJ4jU Q7ZRwnQCsU ... .. ..- attr(*, "varname")= chr "String" $ date : variable, format: "2017-07-02" "2016-10-02" ... - attr(*, ".internal.selfref")=<externalptr>
组合大小约为240 MB:
> object.size(observations) 240001568 bytes > object.size(animals) 3280 bytes
我们以此为参考,并与OP的方法final_dt
进行比较:
# join both tables to create equivalent of final_dt
joined <- animals[observations, on = "animal_id"]
现在这个大小几乎翻了一倍(400 MB),这不是内存效率。
> object.size(joined) 400003432 bytes
请注意,目前尚未设置data.table
个密钥。相反,on
参数用于指定要加入的列。如果我们设置密钥,则加速将加速,on
参数可以省略:
setkey(observations, animal_id)
setkey(animals, animal_id)
joined <- animals[observations]
现在,我们已经证明使用两个单独的表是有效的内存。
对于后续分析,我们可以聚合每只动物observations
,例如,
observations[, .(.N, mean(x), mean(y)), by = animal_id]
animal_id N V2 V3 1: 1 1000000 -5.214370e-04 -0.0019643145 2: 2 1000000 -1.555513e-03 0.0002489457 3: 3 1000000 1.541233e-06 -0.0005317967 4: 4 1000000 1.775802e-04 0.0016212182 5: 5 1000000 -9.026074e-04 0.0015266330 6: 6 1000000 -1.000892e-03 0.0003284044 7: 7 1000000 1.770055e-04 -0.0018654386 8: 8 1000000 1.919562e-03 0.0008605261 9: 9 1000000 1.175696e-03 0.0005042170 10: 10 1000000 1.681614e-03 0.0020562628
并使用animals
animals[observations[, .(.N, mean(x), mean(y)), by = animal_id]]
animal_id treatment date N V2 V3 1: 1 MADxZ9c6fN 2017-07-02 1000000 -5.214370e-04 -0.0019643145 2: 2 ymoJHnvrRx 2016-10-02 1000000 -1.555513e-03 0.0002489457 3: 3 ifdtywJ4jU 2016-10-02 1000000 1.541233e-06 -0.0005317967 4: 4 Q7ZRwnQCsU 2017-02-02 1000000 1.775802e-04 0.0016212182 5: 5 H2M4V9Dfxz 2017-04-02 1000000 -9.026074e-04 0.0015266330 6: 6 29P3hFxqNY 2017-03-02 1000000 -1.000892e-03 0.0003284044 7: 7 rBxjewyGML 2017-02-02 1000000 1.770055e-04 -0.0018654386 8: 8 gQP8cZhcTT 2017-04-02 1000000 1.919562e-03 0.0008605261 9: 9 0GEOseSshh 2017-07-02 1000000 1.175696e-03 0.0005042170 10: 10 x74yDs2MdT 2017-02-02 1000000 1.681614e-03 0.0020562628
OP指出他不想强迫他的用户自己使用连接。不可否认,键入animals[observations]
需要比final_dt
更多的击键。因此,由OP决定是否值得节省内存。
例如,如果我们想比较具有某些特征的动物,例如,
,可以过滤此结果animals[observations[, .(.N, mean(x), mean(y)), by = animal_id]][date == as.Date("2017-07-02")]
animal_id treatment date N V2 V3 1: 1 MADxZ9c6fN 2017-07-02 1000000 -0.000521437 -0.001964315 2: 9 0GEOseSshh 2017-07-02 1000000 0.001175696 0.000504217
在this coment中,OP已经描述了一些他希望透明地为其用户实现的用例:
final_dt[, x2 := 1-x]
:由于只涉及obervations,因此直接转换为observations[, x2 := 1-x]
。使用各种条件final_dt[t > 5 & treatment == "A"]
进行选择:此处涉及两个表的列。这可以通过data.table
以不同方式实现(请注意,已针对实际样本数据修改了条件):
animals[observations][t < 5L & treatment %like% "MAD"]
这类似于预期的语法,但比下面的替代方法慢,因为此处的过滤条件应用于完整连接的所有行。
更快的替代方法是拆分过滤条件,以便在连接之前过滤observations
以在最终应用基础数据列上的过滤条件之前减少结果集:
animals[observations[t < 5L]][treatment %like% "MAD"]
请注意,这看起来非常类似于预期的语法(少了一次击键)。
如果用户认为这是不可接受的,则可以在一个函数中隐藏连接操作:
# function definition
filter_dt <- function(ani_filter = "", obs_filter = "") {
eval(parse(text = stringr::str_interp(
'animals[observations[${obs_filter}]][${ani_filter}]')))
}
# called by user
filter_dt("treatment %like% 'MAD'", "t < 5L")
animal_id treatment date t x y 1: 1 MADxZ9c6fN 2017-07-02 1 -0.56047565 0.6958622 2: 1 MADxZ9c6fN 2017-07-02 2 -0.23017749 -0.5373377 3: 1 MADxZ9c6fN 2017-07-02 3 1.55870831 -3.0425688 4: 1 MADxZ9c6fN 2017-07-02 4 0.07050839 1.8488057
警告:您的里程可能会有所不同,因为以下结论取决于计算机上整数的内部表示以及数据的基数。有关此主题,请参阅Matt Dowle's excellent answer。
Mallick已经提到如果整数被整体存储为数字,则可能会浪费内存。这可以证明:
n <- 10000L
# integer vs numeric vs logical
test_obj_size <- data.table(
rep(1, n),
rep(1L, n),
rep(TRUE, n))
str(test_obj_size)
Classes ‘data.table’ and 'data.frame': 10000 obs. of 3 variables: $ V1: num 1 1 1 1 1 1 1 1 1 1 ... $ V2: int 1 1 1 1 1 1 1 1 1 1 ... $ V3: logi TRUE TRUE TRUE TRUE TRUE TRUE ... - attr(*, ".internal.selfref")=<externalptr>
sapply(test_obj_size, object.size)
V1 V2 V3 80040 40040 40040
请注意,数字向量需要的内存是整数向量的两倍。因此,总是使用后缀字符L
来限定整数常量是一种很好的编程习惯。
如果字符串被强制为因子,则可以减少字符串的内存消耗:
# character vs factor
test_obj_size <- data.table(
rep("A", n),
rep("AAAAAAAAAAA", n),
rep_len(LETTERS, n),
factor(rep("A", n)),
factor(rep("AAAAAAAAAAA", n)),
factor(rep_len(LETTERS, n)))
str(test_obj_size)
Classes ‘data.table’ and 'data.frame': 10000 obs. of 6 variables: $ V1: chr "A" "A" "A" "A" ... $ V2: chr "AAAAAAAAAAA" "AAAAAAAAAAA" "AAAAAAAAAAA" "AAAAAAAAAAA" ... $ V3: chr "A" "B" "C" "D" ... $ V4: Factor w/ 1 level "A": 1 1 1 1 1 1 1 1 1 1 ... $ V5: Factor w/ 1 level "AAAAAAAAAAA": 1 1 1 1 1 1 1 1 1 1 ... $ V6: Factor w/ 26 levels "A","B","C","D",..: 1 2 3 4 5 6 7 8 9 10 ... - attr(*, ".internal.selfref")=<externalptr>
sapply(test_obj_size, object.size)
V1 V2 V3 V4 V5 V6 80088 80096 81288 40456 40464 41856
存储为因子,只需要一半的内存。
同样适用于Date
和POSIXct
类:
# Date & POSIXct vs factor
test_obj_size <- data.table(
rep(as.Date(Sys.time()), n),
rep(as.POSIXct(Sys.time()), n),
factor(rep(as.Date(Sys.time()), n)),
factor(rep(as.POSIXct(Sys.time()), n)))
str(test_obj_size)
Classes ‘data.table’ and 'data.frame': 10000 obs. of 4 variables: $ V1: Date, format: "2017-08-02" "2017-08-02" "2017-08-02" "2017-08-02" ... $ V2: POSIXct, format: "2017-08-02 18:25:55" "2017-08-02 18:25:55" "2017-08-02 18:25:55" "2017-08-02 18:25:55" ... $ V3: Factor w/ 1 level "2017-08-02": 1 1 1 1 1 1 1 1 1 1 ... $ V4: Factor w/ 1 level "2017-08-02 18:25:55": 1 1 1 1 1 1 1 1 1 1 ... - attr(*, ".internal.selfref")=<externalptr>
sapply(test_obj_size, object.size)
V1 V2 V3 V4 80248 80304 40464 40480
请注意data.table()
拒绝创建类POSIXlt
的列,因为它以40个字节而不是8个字节存储。
因此,如果您的应用程序对内存至关重要,则可能值得考虑在适用的情况下使用因子。
答案 1 :(得分:3)
您应该考虑使用嵌套的data.frame
library(tidyverse)
使用我rbind
mtcars
new <- rbind(mtcars,mtcars,mtcars,mtcars) %>%
select(cyl,mpg)
object.size(new)
11384 bytes
如果我们对您可能用于汇总值的数据进行分组,则大小会增加一点
grp <- rbind(mtcars,mtcars,mtcars,mtcars)%>%
select(cyl,mpg) %>%
group_by(cyl)
object.size(grp)
14272 bytes
如果我们嵌套数据
alt <- rbind(mtcars,mtcars,mtcars,mtcars) %>%
select(cyl,mpg) %>%
group_by(cyl) %>%
nest(mpg)
object.size(alt)
4360 bytes
您可以大幅减少对象大小。
注意在这种情况下,您必须有许多重复值来节省内存;例如,nested
mtcars
单个副本的内存大小比mtcars
-----您的案例-----
alt1 <- final_dt %>%
group_by(animal_id, treatment, date) %>%
nest()
看起来像
alt1
animal_id treatment date data
1 1 A 2017-02-21 20:00:00 <tibble [20 x 3]>
2 1 B 2017-02-21 22:00:00 <tibble [20 x 3]>
答案 2 :(得分:3)
非常感谢您的所有反馈。你鼓励我回答我自己的问题,所以就是这样。
我考虑了三种不同的数据结构:
data
列。请参阅answer by @ChiPak。 原始方法非常方便。例如,在数据和元数据之间编写操作非常高效和简单(因为它们在同一个表中)。例如,可以使用:=
有效地创建或更改新元数据或新数据。
我已经研究过嵌套方法,并且发现它很优雅,但我对编写语句以执行简单操作(例如根据值创建变量)的难度和容易出错感到不满意元变量(见我的评论)。
我也非常重视两个表选项。 如果用户知道如何执行连接(非常详细)以及他们是否可以保持数据和元数据之间的关系(例如,如果您有多个数据集,则需要确保为正确的数据提供正确的元数据),这是非常有效的。 )。理想情况下,元数据和数据应该在同一个结构中,就像嵌套表是&#34;内部&#34;他们独特的父表。
最后,我试图采用所有三种方法,并附带一个新数据结构,我将其放在一个名为behavr的包中。
数据内部存储在派生自data.table
的类的实例中,但它还具有元数据作为属性。数据和元数据共享相同的密钥。对于数据,它可用作常规数据表,例如dt[, y2 := y +1]
由于元数据在同一个结构中,我可以编写一个函数(xmd
)来扩展元数据&#34;并隐式加入它。例如,dt[, y3 := xmd(a_meta_variable) + y]
在元数据中查找a_meta_variable
并将其加入以用作常规 - 匿名 - 向量。
另外,我转换了[
运算符,以便我们可以使用[..., meta=T]
访问元数据:
dt[meta=T]
要查看元数据,dt[, new_meta_var := meta_var1+ meta_var2, meta=T]
要创建新的元变量,dt[id < 10,meta=T]
要对其进行子集化。
目前它确实是一个包装草案,所以我很乐意得到一些反馈和贡献!阅读更多https://github.com/rethomics/behavr
答案 3 :(得分:2)
以下是两种可能性(使用一种,两种,或者不使用):
首先,我将表格分开:
# Same code as in question to generate data
animal_list <- list()
animal_list[[1]] <- data.table(t=1:20, x=rnorm(20), y=rnorm(20),
treatment="A", date="2017-02-21 20:00:00",
animal_id=1)
animal_list[[2]] <- data.table(t=1:20, x=rnorm(20), y=rnorm(20),
treatment="B", date="2017-02-21 22:00:00",
animal_id=2)
# ...
final_dt <- rbindlist(animal_list)
# Separating into treatment and animal data.tables
animals_dt <- unique(final_dt[, .(date), key = animal_id])
treatments_dt <- final_dt[, .(t, x, y, treatment), key = animal_id]
然后是用于合并用户的功能
get_animals <- function(animal_names) {
output <- animals_dt[animal_id %in% animal_names] # Getting desired animals
output <- treatments_dt[output] # merging in treatment details
return(output)
}
已编辑以使用animal_id作为唯一标识符而不是处理。 h / t Uwe