Моделирование временного ряда в dplyr вместо использования цикла for

Так что, хотя lag и lead в dplyr великолепны, я хочу смоделировать временной ряд чего-то вроде роста населения. Мой старый школьный код выглядел бы примерно так:

tdf <- data.frame(time=1:5, pop=50)
for(i in 2:5){
  tdf$pop[i] = 1.1*tdf$pop[i-1]
}

который производит

  time    pop
1    1 50.000
2    2 55.000
3    3 60.500
4    4 66.550
5    5 73.205

Я чувствую, что должен быть dplyr или tidyverse способ сделать это (насколько я люблю свой цикл for).

Но что-то вроде

tdf <- data.frame(time=1:5, pop=50) %>%
  mutate(pop = 1.1*lag(pop))

что было бы моим первым предположением, просто производит

  time pop
1    1  NA
2    2  55
3    3  55
4    4  55
5    5  55

Я чувствую, что упускаю что-то очевидное... что это?

Примечание — это тривиальный пример — в моих реальных примерах используется несколько параметров, многие из которых меняются во времени (я моделирую прогнозы в разных сценариях GCM), поэтому tidyverse оказывается мощным инструментом для объединения моих симуляций. .


person jebyrnes    schedule 17.10.2016    source источник
comment
Я думаю, что философия dplyr коренится в манипулировании данными, а не в их создании. Вероятно, есть способ dplyr сделать это как-то, но я бы не рекомендовал его.   -  person Curt F.    schedule 18.10.2016
comment
Я довольно много занимаюсь динамическим моделированием сложных систем, где скорость изменения зависит от других параметров, которые также меняются со временем, с другими параметрами, которые также меняются со временем... Звучит похоже на ваш случай. В то время как простая динамика может быть векторизована в R, сложные циклы становятся единственным реалистичным решением. Но тогда скорость может стать очень низкой, если вы попытаетесь сделать эти циклы в R. Мое решение обычно состоит в том, чтобы придерживаться циклов, но делать интенсивные циклы в RCpp. R великолепен, но не всегда подходит для всего. К счастью, Rcpp избавляет от необходимости связывать C++ с R.   -  person dww    schedule 18.10.2016
comment
В частности, в этом случае проблема заключается в том, что lag() и lead() не работают построчно, а просто сдвигают индекс столбца на единицу. Новый pop — это просто 1.1*c(NA, tdf$pop[-length(pop)].   -  person Noam Ross    schedule 18.10.2016
comment
Я слышу вас, dww, но в преподавании можно зайти так далеко и охватить так много тем! Я думаю, если я введу RCpp, может быть бунт... Ха!   -  person jebyrnes    schedule 18.10.2016


Ответы (5)


Reduce (или его муррр-варианты, если хотите) — это то, что вам нужно для кумулятивных функций, для которых еще не написана версия cum*:

data.frame(time = 1:5, pop = 50) %>%
    mutate(pop = Reduce(function(x, y){x * 1.1}, pop, accumulate = TRUE))

##   time    pop
## 1    1 50.000
## 2    2 55.000
## 3    3 60.500
## 4    4 66.550
## 5    5 73.205

или с мурлыканьем,

data.frame(time = 1:5, pop = 50) %>%
    mutate(pop = accumulate(pop, ~.x * 1.1))

##   time    pop
## 1    1 50.000
## 2    2 55.000
## 3    3 60.500
## 4    4 66.550
## 5    5 73.205
person alistaire    schedule 17.10.2016
comment
Да! Хотя — один вопрос (который связан с тем, что я не знаком с муррр) — если бы у меня было несколько изменяющихся во времени столбцов — скажем, gr как скорость роста, как бы это передавалось в accumulate? - person jebyrnes; 18.10.2016
comment
Предполагая, что вы вычисляете каждый по отдельности, используйте одну из других форм mutate, то есть mutate_all, mutate_if или mutate_at, оберните функцию в funs и замените имя столбца на ., например. mutate_all(funs(accumulate(., ~.x * 1.1))) - person alistaire; 18.10.2016
comment
Меня беспокоит, что нужен только первый элемент pop, а весь столбец установлен на 50 и заменен. pop также может быть c(50, 51, -32, 1, 2) или любым другим вектором, начинающимся с 50, и вы получите тот же результат. - person jtr13; 02.11.2018
comment
@jtr13 Ага. На самом деле это довольно эффективный подход (отсутствие векторизации), потому что добавление всего столбца правильно предварительно выделяет память для цикла Reduce. В общем, Reduce может обрабатывать бинарные функции, где одна переменная будет унаследованным значением, а другая (y выше) будет повторным значением в векторе. ОП просто не нуждалась в этой второй переменной. - person alistaire; 02.11.2018
comment
Есть ли другая функция в Rcpp более быстрая, чем accumulate2 в purrr? - person Omar Abd El-Naser; 23.07.2019
comment
@OmarAbdEl-Naser Rcpp позволяет запускать C++ из R, но не содержит большого количества предварительно скомпилированных функций. Поэтому, если вы хотите перевести accumulate2 и свою лямбду на C++, это, вероятно, будет быстрее. Писать не будет. - person alistaire; 24.07.2019

Если начальное значение pop равно, скажем, 50, то pop = 50 * 1.1^(0:4) даст вам следующие четыре значения. С вашим кодом вы можете сделать:

data.frame(time=1:5, pop=50) %>%
  mutate(pop = pop * 1.1^(1:n() - 1))

Or,

base = 50

data.frame(time=1:5) %>%
  mutate(pop = base * 1.1^(1:n()-1))
person eipi10    schedule 17.10.2016
comment
это хорошо, но случай, когда вы можете получить точное аналитическое решение, по сути, тривиален ... (да, это пример, который привел ОП, так что это законное решение - просто, я думаю, не так уж полезно) - person Ben Bolker; 18.10.2016

Функция накопления Purrr может обрабатывать изменяющиеся во времени индексы, если вы передаете их в свою функцию моделирования в виде списка со всеми параметрами в нем. Тем не менее, нужно немного повозиться, чтобы заставить это работать правильно. Хитрость здесь в том, что функция calculate() может работать как со списком, так и с векторными столбцами. Вы можете использовать функцию nest() tidyr, чтобы сгруппировать столбцы в вектор списка, содержащий текущее состояние населения и параметры, а затем использовать функцию calculate() для результирующего столбца списка. Это немного сложно объяснить, поэтому я включил демонстрацию, моделирующую логистический рост либо с постоянной скоростью роста, либо с изменяющейся во времени стохастической скоростью роста. Я также включил пример того, как использовать это для имитации нескольких повторений для данной модели, используя dpyrr+purrr+tyr.

library(dplyr)
library(purrr)
library(ggplot2)
library(tidyr)

# Declare the population growth function. Note: the first two arguments
# have to be .x (the prior vector of populations and parameters) and .y,
# the current parameter value and population vector. 
# This example function is a Ricker population growth model. 
logistic_growth = function(.x, .y, growth, comp) {
  pop = .x$pop[1]
  growth = .y$growth[1]
  comp  = .y$comp[1]
  # Note: this uses the state from .x, and the parameter values from .y.
  # The first observation will use the first entry in the vector for .x and .y
  new_pop = pop*exp(growth - pop*comp)
  .y$pop[1] = new_pop
  return(.y)
}

# Starting parameters the number of time steps to simulate, initial population size,
# and ecological parameters (growth rate and intraspecific competition rate)
n_steps  = 100
pop_init = 1
growth = 0.5
comp = 0.05

#First test: fixed growth rates
test1 = data_frame(time = 1:n_steps,pop = pop_init, 
                   growth=growth,comp =comp)


# here, the combination of nest() and group_by() split the data into individual 
# time points and then groups all parameters into a new vector called state.
# ungroup() removes the grouping structure, then accumulate runs the function
#on the vector of states. Finally unnest transforms it all back to a
#data frame
out1 = test1 %>%
  group_by(time)%>%
  nest(pop, growth, comp,.key = state)%>%
  ungroup()%>%
  mutate(
    state = accumulate(state,logistic_growth))%>%
  unnest()

# This is the same example, except I drew the growth rates from a normal distribution
# with a mean equal to the mean growth rate and a std. dev. of 0.1
test2 = data_frame(time = 1:n_steps,pop = pop_init, 
                   growth=rnorm(n_steps, growth,0.1),comp=comp)

out2 = test2 %>%
  group_by(time)%>%
  nest(pop, growth, comp,.key = state)%>%
  ungroup()%>%
  mutate(
    state = accumulate(state,logistic_growth))%>%
  unnest()

# This demostrates how to use this approach to simulate replicates using dplyr
# Note the crossing function creates all combinations of its input values
test3 = crossing(rep = 1:10, time = 1:n_steps,pop = pop_init, comp=comp) %>%
  mutate(growth=rnorm(n_steps*10, growth,0.1))

out3 = test3 %>%
  group_by(rep)%>%
  group_by(rep,time)%>%
  nest(pop, growth, comp,.key = state)%>%
  group_by(rep)%>%
  mutate(
    state = accumulate(state,logistic_growth))%>%
  unnest()

print(qplot(time, pop, data=out1)+
        geom_line() +
        geom_point(data= out2, col="red")+
        geom_line(data=out2, col="red")+
        geom_point(data=out3, col="red", alpha=0.1)+
        geom_line(data=out3, col="red", alpha=0.1,aes(group=rep)))
person Eric Pedersen    schedule 18.10.2016

Проблема здесь в том, что dplyr выполняет это как набор векторных операций, а не оценивает термин по одному. Здесь 1.1*lag(pop) интерпретируется как «вычислить запаздывающие значения для всего поп-музыки, а затем умножить их все на 1,1». Поскольку вы set pop=50 запаздывали, значения для всех шагов были 50.

dplyr имеет несколько вспомогательных функций для последовательной оценки; стандартные функции cumsum, cumprod и т. д. работают, а несколько новых (см. ?cummean) работают в dplyr. В вашем примере вы можете смоделировать модель с помощью:

tdf <- data.frame(time=1:5, pop=50, growth_rate = c(1, rep(1.1,times=4)) %>%
    mutate(pop = pop*cumprod(growth_rate))


time    pop     growth_rate
1       50.000  1.0
2       55.000  1.1
3       60.500  1.1
4       66.550  1.1
5       73.205  1.1

Обратите внимание, что здесь я добавил скорость роста в качестве столбца и установил для первой скорости роста значение 1. Вы также можете указать его следующим образом:

tdf <- data.frame(time=1:5, pop=50, growth_rate = 1.1) %>%
    mutate(pop = pop*cumprod(lead(growth_rate,default=1))

Это делает явным, что столбец скорости роста относится к скорости роста на текущем временном шаге по сравнению с предыдущим.

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

person Eric Pedersen    schedule 17.10.2016
comment
Hrm - это близко, так как в cumprod можно включить другие изменяющиеся во времени параметры. Но все еще не совсем гибкий для моей конечной цели. - person jebyrnes; 18.10.2016
comment
Правда, не такой гибкий. Кроме того, подумав над этим, я понял, что было бы очень сложно (а может быть, и невозможно) добавить взаимодействия, зависимость от плотности или нелинейные члены таким образом. Я использовал dplyr таким образом для имитации случайных блужданий, но для этого не требуются взаимодействующие термины, так как большая его часть генерирует независимые переменные и агрегирует. - person Eric Pedersen; 18.10.2016

Как насчет функций карты, т.е.

tdf <- data_frame(time=1:5)
tdf %>% mutate(pop = map_dbl(.x = tdf$time, .f = (function(x) 50*1.1^x)))
person biomiha    schedule 17.10.2016
comment
Это все хорошо для аппроксимации непрерывного времени, но что, если есть параметры, изменяющиеся во времени? Хотя мне нравится, что мурлыкание здесь является частью решения! - person jebyrnes; 18.10.2016
comment
Если вы можете зафиксировать изменяющийся во времени аспект в функции, вы можете легко применить ту же логику или, возможно, вложить функции сопоставления. - person biomiha; 18.10.2016