Ответ 1
Предположим, что мы являемся администратором базы данных, которому дана задача эффективно реализовать это в базе данных SQL. Одной из целей нормализации базы данных является сокращение избыточности.
Согласно описанию ОП, существует много (около 1 М) наблюдений на животных (многомерные продольные данные), в то время как количество животных кажется намного меньшим.
Таким образом, постоянные (или инвариантные) базовые данные каждого животного, например treatment
, date
, должны храниться отдельно от observations
.
animal_id
является ключом к обеим таблицам, предполагая, что animal_id
является уникальным (как следует из названия).
(Обратите внимание, что это основное отличие от ответа Mallick, который использует treatment
как ключ, который не гарантированно будет уникальным, т.е. два животных могут получить такое же лечение и, кроме того, увеличивает избыточность.)
Отдельные таблицы эффективны с точки зрения памяти
В целях демонстрации более реалистичные "контрольные" данные создаются для 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
имеет тип integer.
> 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 Мбайт:
> 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 Мбайт), что неэффективно.
> 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
ОП указал, что он не хочет принуждать своих пользователей к использованию самих подключений. Правда, набрав 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
В этом коммменте ОП описал некоторые варианты использования, которые он хочет видеть прозрачно для своих пользователей:
- Создание новых столбцов
final_dt[, x2 := 1-x]
: поскольку задействованы только оберванности, это переводится непосредственно на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
Использование факторов для уменьшения объема памяти
Предостережение. Ваш пробег может варьироваться, поскольку выводы ниже зависят от внутреннего представления целых чисел на вашем компьютере и мощности данных. Пожалуйста, см. Мэтт Доулл отличный ответ по этому вопросу.
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 байтов.
Итак, если ваше приложение имеет критическую критичность, может быть целесообразно рассмотреть возможность использования фактора, где это применимо.