Эффективное моделирование данных с помощью data.table

Я пытаюсь имитировать новый набор данных из двух меньших наборов данных. Для меня важно сохранить маргинальный рассчитывается из этих меньших наборов данных в конечном наборе данных. Надеюсь, этот воспроизводимый пример должен объяснить, что Я имею в виду.

Создание поддельных данных

library(data.table) # 1.10.5
set.seed(123)
meanVal <- 40

demoDat

Здесь я имитирую некоторые возрастные и гендерные данные. Каждое место всегда будет иметь 2 уровня пола и 100 уровней возраста.

demoDat <- CJ(with(CJ(letters,letters[1:5]), paste0(V1,V2)), c("M","F"), 0:99)
setnames(demoDat, c("Location","Gender","Age"))
demoDat[, Val := rpois(.N, meanVal)]


       Location Gender Age Val
    1:       aa      F   0  36
    2:       aa      F   1  47
    3:       aa      F   2  29
   ---                        
25998:       ze      M  97  45
25999:       ze      M  98  38
26000:       ze      M  99  39

timeDat

Этот код имитирует измерение временных данных. В этом случае даты разворачиваются по неделям, но фактические данные не должны придерживаться этой однородности. Неделя могут отсутствовать.

timeDat <- with(demoDat, CJ(unique(Location), seq(from=as.Date("2016-01-01"),by=7,length.out = 52)))
setnames(timeDat, c("Location","Date"))
totals <- demoDat[, .(Val=sum(Val)), by=.(Location)]
timeDat[totals, Val := rmultinom(1:.N, i.Val, prob=rep(1,.N)), by=.EACHI,on=.(Location)]

      Location       Date Val
   1:       aa 2016-01-01 176
   2:       aa 2016-01-08 143
   3:       aa 2016-01-15 143
  ---                        
6758:       ze 2016-12-09 165
6759:       ze 2016-12-16 142
6760:       ze 2016-12-23 156

Быстрое согласование

Каждое место должно содержать столбец Val, который будет таким же, как в наборах demoDat, так и timeDat.

timeDat[, sum(Val), by=.(Location)][order(-V1)][1:5]
#    Location   V1
# 1:       jb 8229
# 2:       xb 8223
# 3:       ad 8179
# 4:       nc 8176
# 5:       gd 8173
demoDat[, sum(Val), by=.(Location)][order(-V1)][1:5]
#    Location   V1
# 1:       jb 8229
# 2:       xb 8223
# 3:       ad 8179
# 4:       nc 8176
# 5:       gd 8173

Желаемый окончательный набор данных

Затем я хочу создать набор данных с переменными Age, Gender и Date. Но мне нужно сохранить свои предельные суммы Val из наборов данных demoDat и timeDat.

У меня есть одна стратегия, которая выполняет эту задачу, но она занимает довольно много ОЗУ. Есть ли другая стратегия, которую я могу использовать, которая выполняет расширение внутри каждой группы за раз? Возможно, используя .EACHI?

Разверните оба набора данных и слейте

Это дорогая часть операции. Наборы данных расширяются, поэтому количество строк равно sum(Val). В случаях, когда sum(Val) > 500,000,000, это может быть дорогостоящим. Тем более, что операция повторяется для второго набора данных. Я надеюсь использовать .EACHI, поэтому расширяются только данные внутри групп, что существенно снижает объем памяти.

library(pryr)
memUsed <- mem_used() 
demoDatBig <- demoDat[rep(1:.N, Val), .(Location, Gender, Age, ID=rowid(Location))]
timeDatBig <- timeDat[rep(1:.N, Val), .(Location, Date, ID=rowid(Location))]
demoDatBig[timeDatBig, Date := i.Date, on=.(Location, ID)]
finalBigDat <- demoDatBig[, .(Val=.N), by=.(Location, Gender, Age, Date)]
mem_used() - memUsed
# 47 MB

Итак, эта операция заняла 47 МБ ОЗУ, но если я увеличу meanVal, она значительно возрастет. Я бы хотел, чтобы это использовало столько оперативной памяти, как эта операция будет выполняться для той же функции в самой большой группе Location и ID. Я думаю, что это возможно с помощью .EACHI, но я точно не знаю, как это сделать.

Итоговая таблица данных

       Location Gender Age       Date Val
    1:       aa      F   0 2016-01-01  36
    2:       aa      F   1 2016-01-01  47
    3:       aa      F   2 2016-01-01  29
    4:       aa      F   3 2016-01-01  40
    5:       aa      F   4 2016-01-01  24
   ---                                   
32430:       ze      M  96 2016-12-16   7
32431:       ze      M  96 2016-12-23  34
32432:       ze      M  97 2016-12-23  45
32433:       ze      M  98 2016-12-23  38
32434:       ze      M  99 2016-12-23  39

Решение, надеюсь, пройдет эти тесты

#### Test 1
test1 <- finalBigDat[, .(Val = sum(Val)), by=.(Location, Gender, Age)]
test1[demoDat, ValCheck := i.Val, on=.(Location, Gender, Age)]
test1[Val != ValCheck]
#Empty data.table (0 rows) of 5 cols: Location,Gender,Age,Val,ValCheck

#### Test 2
test2 <- finalBigDat[, .(Val = sum(Val)), by=.(Location, Date)]
test2[timeDat, ValCheck := i.Val, on=.(Location, Date)]
test2[Val != ValCheck]
#Empty data.table (0 rows) of 4 cols: Location,Date,Val,ValCheck

Результаты

Я прошел через оба решения и отслеживал память и системные тайминги для обоих. Оба решения были потрясающими и огромными обновлениями для того, что у меня есть. Решение @swihart масштабирует невероятно хорошо до больших meanVal, поэтому я выбрал это как принятый ответ. Ответ Хизер поможет в ситуациях, когда meanVal не такой большой. Частоты большого и малого meanVal встречаются часто, поэтому мне понадобятся оба.

   meanVal            Ans            Time      Mem    Rows
1:      40     Mike.Gahan  0.6245470 secs 44.54293   32434
2:      40 Heather Turner  0.6391492 secs 38.65355 1352000
3:      40        swihart 11.1602619 secs 66.97550 1352000
4:     400     Mike.Gahan  2.593275 secs 437.23832   32611
5:     400 Heather Turner  1.303993 secs  38.79871 1352000
6:     400        swihart 11.736836 secs  66.97550 1352000
7:    4000     Mike.Gahan 30.390986 secs 4364.51501   32629
8:    4000 Heather Turner  6.279249 secs   38.79871 1352000
9:    4000        swihart 11.427965 secs   66.97550 1352000
10:   20000     Mike.Gahan -------did not finish----------
11:   20000 Heather Turner 23.78948 secs 36.30617 1352000
12:   20000        swihart 11.53811 secs 66.97550 1352000
13:   30000     Mike.Gahan -------did not finish----------
14:   30000 Heather Turner 537.6459  secs 57.15375 1352000
15:   30000        swihart 11.970013 secs 66.97474 1352000

Ответ 1

Я выполнил ваш подход для разных размеров meanVal и увидел проблему масштабирования для подхода генерации demoDatBig и timeDatBig. У меня есть подход (заключенный внизу этого сообщения), который генерирует cartDat - декартово крест дат и половозрастных групп, который устойчив к увеличению в meanVal и sum(Val), как видно из этой таблицы, что перечисляет результаты object.size() для обсуждаемых данных.

| meanVal  | sum(Val) | demoDatBig (MB)  | timeDatBig (MB)  | cartDat (MB)  |
|----------|----------|------------------|------------------|---------------|
|      40  |     1e6  |            27.8  |            15.9  |          67.1 |
|     400  |     1e7  |           277.6  |           158.7  |          67.1 |
|   4,000  |     1e8  |         2,776.8  |         1,586.8  |          67.1 |
|  40,000  |     1e9  |        27,770.3  |        15,868.7  |          67.1 |

Ключом к моему подходу является создание декартового перехода между нерасширенными исходными данными data.tables demoDat и timeDat, а затем использовать схему "итеративной многомерной гипергеометрической выборки" (IMHS) для сохранения полей оба источника data.tables. Чтобы иметь функциональность R для IMHS, я взял из CRAN пакет R BiasedUrn и перекомпилировал его так, чтобы он мог обрабатывать 52 цвета (в нашем приложении, Даты). Если необходимо настроить максимальное количество дат для заданного местоположения, сообщите мне, и я буду перекомпилировать. Таким образом, R-пакет BiasedUrn52 находится на github.

Мое решение передает test1 и test2 и сохраняет маргиналы. Однако, по-видимому, распределение маржинального возраста по гендерному признаку происходит через большее количество дат, чем процедура OP. Позвольте мне уточнить:

Если взять первые 5 строк timeDat:

> head(demoDat,5)
   Location Gender Age Val
1:       aa      F   0  36
2:       aa      F   1  47
3:       aa      F   2  29
4:       aa      F   3  40
5:       aa      F   4  50

И первые 6 из finalBigDat:

> head(finalBigDat,6)
   Location Gender Age       Date Val
1:       aa      F   0 2016-01-01  36
2:       aa      F   1 2016-01-01  47
3:       aa      F   2 2016-01-01  29
4:       aa      F   3 2016-01-01  40
5:       aa      F   4 2016-01-01  24
6:       aa      F   4 2016-01-08  26

Мы видим, что все 36 для возрастной группы F-0 были отнесены к 2016-01-01, тогда как 50 для группы F-4 были распределены по 2016-01-01 (24) и 2016-01 -08 (26), но никаких других дат (50 = 24 + 26).

Метод IMHS распределяет маргиналы на многие другие даты (я не уверен, что это желательно или нет - сообщите мне об этом). Например, IMHS взял 36 из группы F-0 и вместо того, чтобы поместить все 36 на 2016-01-01, как в finalBigDat, он распространил их на большее количество дат (посмотрите seq.Draws):

> cartDat[Location=='aa' & Gender=="F" & Age==0,
+         c("Location", "Gender", "Age", "Date", "seq.Draws"),
+         with=FALSE]
    Location Gender Age       Date seq.Draws
 1:       aa      F   0 2016-01-01         1
 2:       aa      F   0 2016-01-08         0
 3:       aa      F   0 2016-01-15         1
 4:       aa      F   0 2016-01-22         1
 5:       aa      F   0 2016-01-29         0
 6:       aa      F   0 2016-02-05         0
 7:       aa      F   0 2016-02-12         0
 8:       aa      F   0 2016-02-19         0
 9:       aa      F   0 2016-02-26         0
10:       aa      F   0 2016-03-04         0
11:       aa      F   0 2016-03-11         0
12:       aa      F   0 2016-03-18         0
13:       aa      F   0 2016-03-25         3
14:       aa      F   0 2016-04-01         1
15:       aa      F   0 2016-04-08         0
16:       aa      F   0 2016-04-15         0
17:       aa      F   0 2016-04-22         1
18:       aa      F   0 2016-04-29         1
19:       aa      F   0 2016-05-06         0
20:       aa      F   0 2016-05-13         2
21:       aa      F   0 2016-05-20         0
22:       aa      F   0 2016-05-27         0
23:       aa      F   0 2016-06-03         0
24:       aa      F   0 2016-06-10         0
25:       aa      F   0 2016-06-17         1
26:       aa      F   0 2016-06-24         2
27:       aa      F   0 2016-07-01         0
28:       aa      F   0 2016-07-08         0
29:       aa      F   0 2016-07-15         0
30:       aa      F   0 2016-07-22         1
31:       aa      F   0 2016-07-29         0
32:       aa      F   0 2016-08-05         1
33:       aa      F   0 2016-08-12         1
34:       aa      F   0 2016-08-19         1
35:       aa      F   0 2016-08-26         1
36:       aa      F   0 2016-09-02         1
37:       aa      F   0 2016-09-09         2
38:       aa      F   0 2016-09-16         0
39:       aa      F   0 2016-09-23         1
40:       aa      F   0 2016-09-30         0
41:       aa      F   0 2016-10-07         2
42:       aa      F   0 2016-10-14         3
43:       aa      F   0 2016-10-21         0
44:       aa      F   0 2016-10-28         1
45:       aa      F   0 2016-11-04         1
46:       aa      F   0 2016-11-11         1
47:       aa      F   0 2016-11-18         0
48:       aa      F   0 2016-11-25         0
49:       aa      F   0 2016-12-02         2
50:       aa      F   0 2016-12-09         1
51:       aa      F   0 2016-12-16         1
52:       aa      F   0 2016-12-23         1

Замечание о различиях в распределении между OP-подходом и подходом IMHS cartDat - это просто в стороне. Маргиналы сохраняются, как показано ниже.

Маргиналы для timeDat сохраняются:

> cartDat[, sum(seq.Draws), by=.(Location, Date)]
      Location       Date  V1
   1:       aa 2016-01-01 176
   2:       aa 2016-01-08 143
   3:       aa 2016-01-15 143
   4:       aa 2016-01-22 154
   5:       aa 2016-01-29 174
  ---                        
6756:       ze 2016-11-25 169
6757:       ze 2016-12-02 148
6758:       ze 2016-12-09 165
6759:       ze 2016-12-16 142
6760:       ze 2016-12-23 156
> timeDat
      Location       Date Val
   1:       aa 2016-01-01 176
   2:       aa 2016-01-08 143
   3:       aa 2016-01-15 143
   4:       aa 2016-01-22 154
   5:       aa 2016-01-29 174
  ---                        
6756:       ze 2016-11-25 169
6757:       ze 2016-12-02 148
6758:       ze 2016-12-09 165
6759:       ze 2016-12-16 142
6760:       ze 2016-12-23 156

как и маргиналы для demoDat:

> cartDat[, sum(seq.Draws), by=.(Location, Gender, Age)]
       Location Gender Age V1
    1:       aa      F   0 36
    2:       aa      F   1 47
    3:       aa      F   2 29
    4:       aa      F   3 40
    5:       aa      F   4 50
   ---                       
25996:       ze      M  95 48
25997:       ze      M  96 41
25998:       ze      M  97 45
25999:       ze      M  98 38
26000:       ze      M  99 39
> demoDat
       Location Gender Age Val
    1:       aa      F   0  36
    2:       aa      F   1  47
    3:       aa      F   2  29
    4:       aa      F   3  40
    5:       aa      F   4  50
   ---                        
25996:       ze      M  95  48
25997:       ze      M  96  41
25998:       ze      M  97  45
25999:       ze      M  98  38
26000:       ze      M  99  39

Ниже приведен подход IMHS cartDat и некоторые тесты:

#Cartesian cross of demoDat and timeDat
devtools::install_github("swihart/BiasedUrn52")
library(BiasedUrn52)
setkey(timeDat, Location)
setkey(demoDat, Location, Gender, Age)
cartDat <- demoDat[timeDat, allow.cartesian=TRUE]
setkeyv(cartDat, key(demoDat))
cartDat
cartDat[,group:=.GRP,by=c("Gender", "Age") ]
cartDat[,demoDat.Val:=Val]
cartDat[,timeDat.Val:=i.Val]
setcolorder(cartDat, c("Location", 
                       "group",
                       "Gender",
                       "Age",
                       "Val",
                       "demoDat.Val",
                       "Date",
                       "timeDat.Val",
                       "i.Val"))

#Define Iterative Multivariate Hypergeometric Sampling function
imhs <- function(.N, Val, i.Val, group){

  grp.ind <- unique(group)
  num.grp <- max(group)
  grp.size <- as.numeric(table(group))

  draws <- rep(NA, length(group))
  for(grp in grp.ind){

    if(grp==1){
      draws[group==1] = rMFNCHypergeo(1, 
                                      i.Val[group==1], 
                                      Val[group==1][1], 
                                      rep(1/grp.size[grp.ind==1],grp.size[grp.ind==1])
      )
      i.Val[group==2]= i.Val[group==1]-draws[group==1]
    }else{
      draws[group==grp] = rMFNCHypergeo(1, 
                                        i.Val[group==grp], 
                                        Val[group==grp][1], 
                                        rep(1/grp.size[grp.ind==grp],grp.size[grp.ind==grp])
      )
      if(grp<=num.grp){
        i.Val[group==(grp+1)]= i.Val[group==grp]-draws[group==grp]
      }
    }

  }

  list(i.Val, draws)
}


# run it the data.table way:
cartDat[,
        c("seq.Val", "seq.Draws") := imhs(.N, demoDat.Val, timeDat.Val, group),        
        by=c("Location") ]

# take a look:
cartDat

# reconciliation
demoDat[, sum(Val), by=.(Location)][order(-V1)]
cartDat[, sum(seq.Draws), by=.(Location)][order(-V1)]

# do the checks for the margins:
cartDat[, sum(seq.Draws), by=.(Location, Date)]
timeDat
cartDat[, sum(seq.Draws), by=.(Location, Gender, Age)]
demoDat


# such different sizes due to distributing across more dates:
nrow(demoDat)
nrow(cartDat)
nrow(cartDat[seq.Draws != 0])
nrow(finalBigDat)
nrow(cartDat[seq.Draws != 0])/nrow(finalBigDat)

# attain and print object sizes for cartDat
print(object.size(cartDat), units = "Mb")
print(object.size(cartDat[seq.Draws!=0]), units="Mb")

# attain and print object sizes for demoDatBig, timeDatBig, finalBigData
print(object.size(demoDatBig), units = "Mb")
print(object.size(timeDatBig), units = "Mb")
print(object.size(finalBigDat), units = "Mb")



## (OP) The solution would pass these tests:
finalBigDat2 <- cartDat

#### Test 1 (change to sum(seq.Draws))
test1 <- finalBigDat2[, .(Val = sum(seq.Draws)), by=.(Location, Gender, Age)]
test1[demoDat, ValCheck := i.Val, on=.(Location, Gender, Age)]
test1[Val != ValCheck]
#Empty data.table (0 rows) of 5 cols: Location,Gender,Age,Val,ValCheck

#### Test 2 (change to sum(seq.Draws))
test2 <- finalBigDat2[, .(Val = sum(seq.Draws)), by=.(Location, Date)]
test2[timeDat, ValCheck := i.Val, on=.(Location, Date)]
test2[Val != ValCheck]
#Empty data.table (0 rows) of 4 cols: Location,Date,Val,ValCheck

Ответ 2

Объединяясь по местоположению и идентификатору, ваш текущий подход сопоставляет даты с наблюдениями в гендерных категориях поэтапно, например. назначая первую дату первым 176 наблюдениям (охватывающим первые четыре пола: возрастные категории и часть пятого) и т.д.

Вместо этого вы можете моделировать подсчеты каждой даты в каждой категории пола: возрастная категория с помощью функции r2dtable из пакета stats для имитации двухсторонней (пол: возраст): таблицы событий так что маргинальные суммы фиксированы.

Сначала создайте комбинации "Пол, возраст и дата" в каждом месте.

setkey(timeDat, Location)
setkey(demoDat, Location)
finalBigDat <- demoDat[timeDat, .(Location, Gender, Age, Date),
                       allow.cartesian=TRUE]

Затем примените r2dtable в пределах каждого местоположения, установите итоговые значения строк как количество в каждом поле: возрастную категорию и итоговые значения столбцов в качестве подсчета в каждую дату:

setkey(finalBigDat, Location)
finalBigDat[, Val := c(r2dtable(1, demoDat[.BY, Val], timeDat[.BY, Val])[[1]]),
            by = Location]
head(finalBigDat)
#    Location Gender Age       Date Val
# 1:       aa      F   0 2016-01-01   0
# 2:       aa      F   1 2016-01-01   3
# 3:       aa      F   2 2016-01-01   2
# 4:       aa      F   3 2016-01-01   2
# 5:       aa      F   4 2016-01-01   1
# 6:       aa      F   5 2016-01-01   0

При необходимости проходит тесты 1 и 2. Размеры finalBigDat всегда одинаковы независимо от значения meanVal, однако rd2table займет больше времени с более высоким meanVal. Вот некоторые иллюстративные моменты:

| meanVal  | sum(Val) | time (s) |
|----------|----------|----------|
|      40  |     1e6  |     0.36 | 
|    4000  |     1e8  |     7.72 |
|   20000  |     5e8  |    35.09 |

Я попытался с meanVal = 40000, но остановил код, работающий примерно на 300 секунд, поэтому вы можете столкнуться с трудностями, если хотите установить высокий средний вал.