class: center, middle, inverse, title-slide .title[ # Time-varying coefficients with sdmTMB ] .subtitle[ ## DFO DSAF workshop ] .author[ ### ] .date[ ### January 12–16 2026 ] --- <!-- Build with: xaringan::inf_mr() --> .small[ # Why might we want time-varying effects? * Time-varying slopes: * To allow for evolving responses to covariates (e.g., species moving deeper over time) * Example use: [English et al. (2021) Fish and Fisheries](https://doi.org/10.1111/faf.12613) Modelled groundfish density with depth; didn't want to constrain fish if they were moving deeper when water was warmer * Time-varying intercepts: * To allow variable means across time with constraints * To have a model to interpolate or forecast over time ] --- # Time-varying intercepts Several ways in sdmTMB: * factors: `as.factor(year)` (independent) * random effects: ` + (1 | year)` (drawn from normal distribution) * smooth: ` + s(year)` * as random walk (shown next) --- # Random walk covariates in sdmTMB Random walk: $$ `\begin{aligned} x_t &= x_{t-1} + \epsilon_t\\ \epsilon &\sim \mathrm{Normal(0, \sigma^2)} \end{aligned}` $$ Defined by `time_varying` argument Takes a *one-sided* formula, e.g. `~ 1` or `~ 0 + depth` --- # Time-varying intercept .small[ ``` r mesh <- make_mesh(pcod, xy_cols = c("X", "Y"), cutoff = 10) fit <- sdmTMB( density ~ 0 + s(depth, k = 5), * time_varying = ~ 1, * time_varying_type = "rw", data = pcod, mesh = mesh, time = "year", family = tweedie(link = "log") ) ``` ] .xsmall[ Note: a `0` or `-1` in formula for suppressing global intercept in this case `time_varying_type = "rw"` (default) estimates the first time step as independent. I.e. **do not place the same covariate in the `formula` argument** (this includes the intercept). This does not apply to other time-varying types in sdmTMB. Sorry! Also `"ar1"` or `"rw0"` ] --- # Getting coefficients Return with .small[ ``` r print(fit) ``` ] .small[ ``` r #> Spatiotemporal model fit by ML ['sdmTMB'] #> Formula: density ~ 0 + s(depth, k = 5) #> Time column: "year" #> ... *#> Time-varying parameters: *#> coef.est coef.se *#> (Intercept)-2003 1.96 0.29 *#> (Intercept)-2004 2.31 0.27 *#> (Intercept)-2005 2.06 0.27 *#> ... *#> (Intercept)-2015 2.07 0.27 *#> (Intercept)-2017 1.55 0.29 #> ... ``` ] --- # Getting coefficients Or via `tidy()`: ``` r tidy(fit, "ran_vals") #> # A tibble: 9 × 5 #> term estimate std.error conf.low conf.high #> <chr> <dbl> <dbl> <dbl> <dbl> #> 1 (Intercept):2003 1.96 0.289 1.39 2.52 #> 2 (Intercept):2004 2.31 0.272 1.78 2.85 #> 3 (Intercept):2005 2.06 0.271 1.53 2.60 #> 4 (Intercept):2007 1.23 0.301 0.642 1.82 #> 5 (Intercept):2009 1.51 0.276 0.971 2.05 #> 6 (Intercept):2011 1.93 0.271 1.40 2.46 #> 7 (Intercept):2013 2.16 0.267 1.64 2.68 #> 8 (Intercept):2015 2.07 0.273 1.54 2.61 #> 9 (Intercept):2017 1.55 0.293 0.975 2.12 ``` --- # Other approaches to modeling time-varying intercepts .small[ ``` r density ~ s(depth) + 0 + as.factor(year) ``` ] .small[ ``` r density ~ s(depth) + (1 | year) ``` ] .small[ ``` r density ~ s(depth) + s(year) ``` ] --- # These approaches are similar but subtly different <img src="08-time-varying_files/figure-html/compare-fits-1.png" width="700px" style="display: block; margin: auto;" /> <!-- <img src="images/spidey.jpeg" width="650px" class="center" /> --> <!-- * H/T Eric Pederson --> --- # Time-varying coefficients Time-varying (random walk) effect of depth Intercept in this model NOT time-varying ``` r fit_tv <- sdmTMB( density ~ 1, * time_varying = ~ 0 + depth_scaled + depth_scaled2, data = pcod, mesh = mesh, time = "year", family = tweedie(link = "log"), spatial = "on", spatiotemporal = "iid", silent = FALSE ) ``` --- # Time-varying coefficients Time-varying (random walk) effect of depth <!-- To plot these, we make a data frame that contains all combinations of the time-varying covariate and time. This is easily created using `expand.grid()` or `tidyr::expand_grid()`. --> <img src="08-time-varying_files/figure-html/tv-depth-eff-1.png" width="700px" style="display: block; margin: auto;" /> --- # Time-varying coefficient notes * `time_varying` is a formula for coefficients that follow a random walk or AR1 process over time -- * Make sure a coefficient isn't in both `formula` and `time_varying` for `time_varying_type = "rw"`, this includes the intercept -- * The `time_varying` formula cannot have smoothers `s()` in it! Instead: * Polynomials: `time_varying = ~ x + I(x^2)` * `formula = s(depth, by = factor_year)` (independent smooths) * `formula = s(depth, year)` (2D smooth) <!-- See the vignette [Intro to modelling with sdmTMB](https://pbs-assess.github.io/sdmTMB/articles/basic-intro.html) for more details. -->