使用高度重复的密钥减少data.table的内存占用

时间:2017-07-19 15:48:34

标签: r memory data.table

我正在编写一个包来分析@Transactional中的高吞吐量动物行为数据。 数据是多变量时间序列。 我选择使用R代表他们,我觉得这很方便。

对于一只动物,我会有类似的东西:

data.tables

然而,我和我的用户和许多动物一起工作,这些动物具有不同的任意治疗,条件和其他变量,这些变量在每只动物中不变

最后,我发现代表数据的最方便的方法是合并来自所有动物的行为和单个数据表中的所有实验,并使用我设置的额外列作为关键,对于这些重复变量中的每一个"。

所以,从概念上讲,就是这样:

one_animal_dt <- data.table(t=1:20, x=rnorm(20), y=rnorm(20))

这种方式可以非常方便地计算每只动物的摘要,同时对所有生物信息(治疗等)不可知。

在实践中,我们为每只动物提供了数百万(而不是20)个连续读取,因此我们为方便起见而添加的列包含高度重复的值,这不是内存效率。

有没有办法压缩这个高度冗余的密钥而不会丢失表格的结构(即列)?理想情况下,我不想强​​迫我的用户自己使用JOIN。

4 个答案:

答案 0 :(得分:4)

我们假设,我们是一名数据库管理员,负责在SQL数据库中有效地实现这一点。 数据库规范化的目标之一是减少冗余。

根据OP的描述,每只动物有很多(大约1M)的观察结果(多变量,纵向数据),而动物的数量似乎要小得多。

因此,每只动物的常数(或不变)基础数据,例如treatmentdate,应与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

OP的用例

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

存储为因子,只需要一半的内存。

同样适用于DatePOSIXct类:

# 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

的4份副本的玩具示例
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
  • 两张表。一个用于元数据,一个用于数据。可以使用共享id(密钥)将这两个表映射到彼此。请参阅answer by @UweBlock

原始方法非常方便。例如,在数据和元数据之间编写操作非常高效和简单(因为它们在同一个表中)。例如,可以使用:=有效地创建或更改新元数据或新数据。

我已经研究过嵌套方法,并且发现它很优雅,但我对编写语句以执行简单操作(例如根据值创建变量)的难度和容易出错感到不满意元变量(见我的评论)。

我也非常重视两个表选项。 如果用户知道如何执行连接(非常详细)以及他们是否可以保持数据和元数据之间的关系(例如,如果您有多个数据集,则需要确保为正确的数据提供正确的元数据),这是非常有效的。 )。理想情况下,元数据和数据应该在同一个结构中,就像嵌套表是&#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)

以下是两种可能性(使用一种,两种,或者不使用):

  1. 确保所有列类型的内存效率最高。如果你有整数存储为数字,那会占用大量内存。
  2. 由于您不希望您的用户必须自己进行联接,因此请根据他们想要的动物编写一个简短的函数来为他们进行联接。只需将动物信息放在一个data.table中,将治疗信息放在另一个data.table中,然后在函数中进行合并
  3. 首先,我将表格分开:

    # 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