0

I have a longitudinal dataset for plant growth recorded in different seasons. I fitted the data to growth models with/without seasonal effect using nlme function from nlme package. To see the seasonal effect on the estimated parameter (k), I compared the two models using anova function, however, I got "Error in getResponseFormula(el) : 'form' must be a two-sided formula" and stuck in this problem. Can anyone help me solve this problem?

Here is the reproducible example.

data

library(tidyverse)
# generate dummy data
n = 100
id = as.factor(1:n)
season = c("s1","s2","s3","s4")
L0 = rnorm(n,40,5)
td = rnorm(n, 180, 10)

growth <- function(td, Lt_1){
  return(function(k,p){
    150*((1 + ((150/Lt_1)^(1/p)-1)*exp(-k*td/365))^(-p))
  })
}
L1 = growth(Lt_1 = L0, td = td)(p = 1.2, k = 0.55)
L2 = growth(Lt_1 = L1, td = td)(p = 1.2, k = 1.09)
L3 = growth(Lt_1 = L2, td = td)(p = 1.2, k = 0.62)
L4 = growth(Lt_1 = L3, td = td)(p = 1.2, k = 0.97)

df <- data.frame(id = id, L0 = L0, L1 = L1, L2 = L2, L3 = L3, L4 = L4) %>% 
  pivot_longer(cols = L0:L4, names_to = "Ltype", values_to = "Lobs") %>% 
  mutate(Lobs = round(Lobs)) %>% 
  group_by(id) %>% 
  mutate(Lt_1 = lag(Lobs)) %>% 
  filter(!is.na(Lt_1)) %>% 
  mutate(td = round(rnorm(4, 180, 10))) %>% 
  mutate(season = factor(season)) %>% 
  ungroup() %>% 
  mutate(season2 = case_when(
    season == "s3" ~ "s1",
    season == "s4" ~ "s2",
    TRUE ~ season
  )) %>% 
  mutate(season2 = factor(season2)) %>% 
  mutate(log_Lobs = log(Lobs))

> head(df)
# A tibble: 6 x 8
  id    Ltype  Lobs  Lt_1    td season season2 log_Lobs
  <fct> <chr> <dbl> <dbl> <dbl> <fct>  <fct>      <dbl>
1 1     L1       57    48   178 s1     s1          4.04
2 1     L2       76    57   184 s2     s2          4.33
3 1     L3       87    76   188 s3     s1          4.47
4 1     L4      103    87   194 s4     s2          4.63
5 2     L1       43    35   175 s1     s1          3.76
6 2     L2       63    43   176 s2     s2          4.14

fitting

library(nlme)
## model without seasonal effect
formula = Lobs ~ 150*((1 + ((150/Lt_1)^(1/p)-1)*exp(-k*td/365))^(-p))
p = 1.2
k = 0.4

fit_null <- nlme(formula,
              fixed = c(p ~ 1, k ~ 1),
              random = p ~ 1 | id,
              data = df,
              start = list(fixed = c(p, k)),
              na.action = na.exclude,
              control=list(maxIter=1e6, msMaxIter = 1e6, msVerbose = TRUE))

## model with seasonal effect
p = 1.2
k = c(0.4, 1.09)

fit_withSeason <- nlme(formula,
                fixed = c(p ~ 1, k ~ 1 + season2),
                random = k ~ 1 | id,
                data = df,
                start = list(fixed = c(p, k)),
                na.action = na.exclude,
                control=list(maxIter=1e6, msMaxIter = 1e6, msVerbose = TRUE))

summary of the two models

> summary(fit_null)
Nonlinear mixed-effects model fit by maximum likelihood
  Model: formula 
  Data: df 
       AIC      BIC    logLik
  2312.975 2328.941 -1152.488

Random effects:
 Formula: p ~ 1 | id
                   p Residual
StdDev: 8.300513e-05 4.315789

Fixed effects:  c(p ~ 1, k ~ 1) 
      Value  Std.Error  DF   t-value p-value
p 0.7335954 0.09075133 299  8.083577       0
k 0.9823510 0.05594228 299 17.560080       0
 Correlation: 
  p     
k -0.969

Standardized Within-Group Residuals:
       Min         Q1        Med         Q3        Max 
-1.4898414 -0.8966244 -0.2154542  0.8401292  2.2148509 

Number of Observations: 400
Number of Groups: 100 

> summary(fit_withSeason)
Nonlinear mixed-effects model fit by maximum likelihood
  Model: formula 
  Data: df 
       AIC      BIC    logLik
  1466.922 1486.879 -728.4608

Random effects:
 Formula: k ~ 1 | id
        k.(Intercept) Residual
StdDev:    0.03294767  1.37547

Fixed effects:  c(p ~ 1, k ~ 1 + season2) 
                  Value  Std.Error  DF  t-value p-value
p             1.9509211 0.16984927 298 11.48619       0
k.(Intercept) 0.5212180 0.01156849 298 45.05498       0
k.season2s2   0.4151107 0.00828721 298 50.09055       0
 Correlation: 
              p      k.(In)
k.(Intercept) -0.868       
k.season2s2   -0.572  0.265

Standardized Within-Group Residuals:
         Min           Q1          Med           Q3          Max 
-2.341255091 -0.727512901  0.003841993  0.622830078  3.190926108 

Number of Observations: 400
Number of Groups: 100 

Error

# model comparison to test the seasonal effect
> anova(fit_null, fit_withSeason)
Error in getResponseFormula(el) : 'form' must be a two-sided formula
Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
TKH_9
  • 105
  • 1
  • 7
  • Can you please link to your previous questions? I'm curious how you got the seasonal fixed effect to work. – Ben Bolker Jul 06 '23 at 23:17
  • @BenBolker, I did link to my post here: https://stackoverflow.com/questions/76607837/lme4-how-to-specify-estimated-parameters-for-a-specific-model-in-the-lmer-funct/76608658?noredirect=1#comment135070263_76608658 – TKH_9 Jul 07 '23 at 22:13
  • I meant that the link should be in the body of the question. (Sorry if I've overlooked it, but I still don't see it there ... ??) – Ben Bolker Jul 08 '23 at 01:45

1 Answers1

1

This is arguably a bug in nlme, but a tricky one. When you pass the formula as a symbol called "formula" (i.e. "formula", rather than "Lobs ~ "), it fails to extract the formula from the model call properly.

If you change the variable name of your formula from formula to (e.g.) form1, I think everything should work fine (another reason not to use names of built-in objects as variable names!)

More generally (for other issues involving improper evaluation of objects stored as symbols) you can work around this by using do.call(), which tricks R into evaluating the formula before storing the call:

fit_null <- do.call(nlme,
          list(formula,
               fixed = c(p ~ 1, k ~ 1),
               random = p ~ 1 | id,
               data = df,
               start = list(fixed = c(p, k)),
               na.action = na.exclude,
               control=list(maxIter=1e6, msMaxIter = 1e6, msVerbose = TRUE)
         ))

## model with seasonal effect
p = 1.2
k = c(0.4, 1.09)

fit_withSeason <- do.call(nlme,
                list(formula,
                fixed = c(p ~ 1, k ~ 1 + season2),
                random = k ~ 1 | id,
                data = df,
                start = list(fixed = c(p, k)),
                na.action = na.exclude,
                control=list(maxIter=1e6, msMaxIter = 1e6, msVerbose = TRUE)))

anova(fit_null, fit_withSeason)

               Model df      AIC      BIC     logLik   Test  L.Ratio p-value
fit_null           1  4 2317.913 2333.879 -1154.9565                        
fit_withSeason     2  5 1540.088 1560.045  -765.0438 1 vs 2 779.8253  <.0001

I've submitted this as an R bug

Ben Bolker
  • 211,554
  • 25
  • 370
  • 453
  • Thank you. This worked well. As you mentioned, it also worked when I directory put the formula in the function like nlme(Lobs ~ 150*((1 + ((150/Lt_1)^(1/p)-1)*exp(-k*td/365))^(-p)), fixed = c(p ~ 1, k ~ 1), random = p ~ 1 | id, data = df, start = list(fixed = c(p, k)), na.action = na.exclude, control=list(maxIter=1e6, msMaxIter = 1e6, msVerbose = TRUE)) – TKH_9 Jul 07 '23 at 16:10
  • thank you for your comment. I would like to ask you another question. How can I remove the rarndom effect from the model? Simply removing the line "random = k ~ 1 | id," from the nlme() function gives me the following error and does not work. "Error in parse(text = paste("~", paste(nVal, collapse = "/"))) : :2:0: unexpected end of input 1: ~ ^ " – TKH_9 Jul 07 '23 at 23:07
  • If you want to fit a model without a random effect you need to use `gnls()` instead of `nlme` (and the syntax for specifying fixed effects is different - use `params` instead of `fixed`, see the help page ...) – Ben Bolker Jul 08 '23 at 01:44
  • ```gnls()``` seems to use generalized least squere method to fit a model whereas ```nlme``` uses maximum likelihood method. Can I simply compare the results of two models using the different fitting methods? I tried to compare the two models (gnls, nlme) to see if I should include random effects using ```anova(gnls, nlme)```. The gnls model (the model without random effects) looks better because the AIC value is lower than the nlme. But I am not sure if I am comparing the models in an appropriate way. – TKH_9 Jul 19 '23 at 04:19