Spatial modeling using mgcv soap film smoothing

Hi Paul,

I’m interested in reproducing Gavin Simpson’s example on soap film smoothers and lake bathymetries (URL: https://www.fromthebottomoftheheap.net/2016/03/27/soap-film-smoothers/) using brms.

His model structure is as follows:

library(mgcv)
m2 <- gam(-depth ~ s(os_x, os_y, bs = "so", xt = list(bnd = bound)),
          data = depth, method = "REML", knots = knots)

where bound is a list of spatial points (x,y) representing the lake perimeter to constrain the predictions and knots is a data frame of spatial points (x,y) representing a sequence of grid points that fall within that boundary.

What would be a comparable model structure using brmsformula syntax? I haven’t been able to figure it out on my own yet and am looking for your insight.

Thanks a ton,
Denise

  • Operating System: Windows
  • brms Version: 2.9.0

It should work with the exact same syntax. What is the error message when trying that?

The error is as follows:

Error in smooth.construct.so.smooth.spec(object, dk$data, dk$knots) :
need at least one interior knot

Hmm. Can you provide a minimal reproducible example for me to try out?

Yes- absolutely - will prepare that right now thanks.

Here is a reproducible example:

library(tidyverse)
library(mgcv)
library(brms)

x <- c(0,  0,  1, 1,0)
y <- c(0, 1, 1, 0, 0)

x2 <- runif(100)
y2 <- runif(100)

z = rnorm(100)

seq(0.1,0.9, length=5)
expand.grid(seq(0.1,0.9, length=5), seq(0.1,0.9, length=5)) %>% rename(x = Var1, y=Var2) -> knots_test
tibble(z = z, x= x2, y=y2) -> testdata

gam(formula = z ~ s(x, y, bs = "so", xt = list(bnd = list(list(x=x, y=y)))),
    data = testdata, method = "REML",
    knots = knots_test)
# works

brm(formula = z ~ s(x, y, bs = "so", xt = list(bnd = list(list(x=x, y=y)))),
    data = testdata,
    knots = knots_test)

# need at least one interior knot

Thanks! I will take a look.

1 Like

Fixed the error you see, but now we get

Error in smooth2random.mgcv.smooth(sm, names(data), type = 2) : 
  Can not convert this smooth class to a random effect 

which is no longer a bug but a limitation of brms’ spline functionality.

2 Likes

I am also very much interested in this and having the same problem, thanks paul

This is a limitation of how the GAMs smooth functions are represented as random effects. It’s the same reason why you can only use t2() for tensor product smooths with brm(), gamm4::gamm4() (and hence lmer() or glmer() in the lme package), whereas you can use te(), ti() and t2() smooths in gam(), bam() etc.

Is there a particular reason you wanted the fully Bayesian fit that brm() would give you? GAMs fitted using gam() and bam() are (empircal) Bayesian models and you can get at the posterior distribution of the model and sample from it in a similar sense to predict() and fitted() applied to a brm() model. The obvious difference is that mgcv doesn’t do the actual sampling and is using improper priors, and there isn’t a prior on the smoothness parameter(s) in the model (but you can to some extent account for this when sampling from the posterior).

Unfortunately there is nothing I can do about it at this point.

1 Like

Thanks for looking into this- I ended up using mgcv instead of calling it through brms and the result was very slick.

1 Like

But isn’t the limitation rather in the implementation in mgcv::smooth2random rather than in the equivalence between smooths and random effects?

I’d say it is a limitation of how random effects are implemented in (g)lmer and how the so basis, in particular, is constructed. In the same way that te() smooths don’t work in gamm4() and brm() because the way those penalties are formed is incompatible with how random effects are represented in those functions.

Writing that got me thinking, however; I’d forgotten that Simon had implemented separate basis types for the hoop (boundary; bs = 'sf') and the film inside the hoop (bs = 'sw'). These should have separable penalties which can be fit in lme() and (g)lmer() and hence smooth2random() should work for these and thence (fingers crossed) should also work in brms.

Here is a modified version of your reproducible example:

library('tibble')
library('dplyr')
library('mgcv')
library('brms')
library('gamm4')

x <- c(0,  0,  1, 1,0)
y <- c(0, 1, 1, 0, 0)
x2 <- runif(100)
y2 <- runif(100)
z <- rnorm(100)

knots_test <- expand.grid(seq(0.1,0.9, length=5), 
                          seq(0.1,0.9, length=5)) %>%
  rename(x = Var1, y = Var2)
testdata <- tibble(z = z, x= x2, y=y2)

gam(z ~ s(x, y, bs = "so", xt = list(bnd = list(list(x=x, y=y)))),
    data = testdata, method = "REML", knots = knots_test)

bndry <- list(list(x = x, y = y))
gamm(z ~ s(x, y, bs = "sf", xt = list(bnd = bndry)) +
         s(x, y, bs = "sw", xt = list(bnd = bndry)),
    data = testdata, method = "REML", knots = knots_test)
# works
gamm4(z ~ s(x, y, bs = "sf", xt = list(bnd = bndry)) +
          s(x, y, bs = "sw", xt = list(bnd = bndry)),
     data = testdata, method = "REML", knots = knots_test)
# works

brm(z ~ s(x, y, bs = "sf", xt = list(bnd = bndry)) +
        s(x, y, bs = "sw", xt = list(bnd = bndry)),
    data = testdata, knots = knots_test)

The brm() call ends with the original error:

Error in smooth.construct.sw.smooth.spec(object, dk$data, dk$knots) : 
  need at least one interior knot

but I believe @paul.buerkner has this fixed in the GitHub version of brms. I haven’t had chance to test that version yet so if someone wanted to try this out I suspect it should work, at least in principle now.

3 Likes

I can confirm that this approach works for estimation, but not for any post-processing function (e.g. posterior_predict, conditional_effects). I get an error from tcrossprod: “non-conformable arguments”.

Backtrace:

<error/rlang_error>
Error in `tcrossprod()`:
! non-conformable arguments
---
Backtrace:
     x
  1. +-brm_model %>% predict()
  2. +-stats::predict(.)
  3. \-brms:::predict.brmsfit(.)
  4.   +-rstantools::posterior_predict(...)
  5.   \-brms:::posterior_predict.brmsprep(...)
  6.     \-brms::get_dpar(object, dpar = dp)
  7.       +-brms:::predictor(x, i = i, fprep = prep)
  8.       \-brms:::predictor.bprepl(x, i = i, fprep = prep)
  9.         \-brms:::predictor_sm(prep, i)
 10.           \-brms:::.predictor_fe(X = Zs, b = s)
 11.             \-base::tcrossprod(b, X)
1 Like

It seems like there is something going on when we have both smoothers in the model.

brm(z ~ s(x, y, bs = "sf", xt = list(bnd = bndry)),
    data = testdata, knots = knots_test) -> brm_model1

brm_model1 %>% conditional_smooths()

brm(z ~ s(x, y, bs = "sw", xt = list(bnd = bndry)),
    data = testdata, knots = knots_test) -> brm_model2

brm_model2 %>% conditional_smooths()

1 Like