-
Notifications
You must be signed in to change notification settings - Fork 33
Description
Using factors in ti smooth terms can result in errors or invalid behavior with draw.gam.
1D: Error
Here is just a small reprex that produces an error:
library(tidyverse)
library(mgcv)
#> Loading required package: nlme
#>
#> Attaching package: 'nlme'
#> The following object is masked from 'package:dplyr':
#>
#> collapse
#> This is mgcv 1.9-3. For overview type 'help("mgcv-package")'.
library(gratia)
#>
#> Attaching package: 'gratia'
#> The following object is masked from 'package:stringr':
#>
#> boundary
# 1D-interaction: Error
iris_gam_1d <- gam(
Sepal.Length ~ s(Sepal.Width) + ti(Sepal.Width, Species, bs="fs"),
data = iris,
method = "REML"
)
plt <- draw(iris_gam_1d)
#> Error in Summary.factor(structure(c(1L, 2L, 3L, 1L, 2L, 3L, 1L, 2L, 3L, : 'min' not meaningful for factorsCreated on 2025-08-27 with reprex v2.1.1
2D: Invalid behaviour
And here is the invalid behaviour:
library(tidyverse)
library(mgcv)
#> Loading required package: nlme
#>
#> Attaching package: 'nlme'
#> The following object is masked from 'package:dplyr':
#>
#> collapse
#> This is mgcv 1.9-3. For overview type 'help("mgcv-package")'.
library(gratia)
#>
#> Attaching package: 'gratia'
#> The following object is masked from 'package:stringr':
#>
#> boundary
# 2D-interaction: Invalid behaviour
iris_gam_2d <- gam(
Sepal.Length ~ s(Sepal.Width) + s(Petal.Width) + ti(Sepal.Width, Petal.Width, Species, bs="fs"),
data = iris,
method = "REML"
)
plt <- draw(iris_gam_2d)Created on 2025-08-27 with reprex v2.1.1
Fix
The behaviour is quite deeply integrated in gratia and the intended behaviour not entirely clear, but I took a stab at fixing it anyways.
The first issue is the error Error in Summary.factor(structure(c(1L, 2L, 3L, 1L, 2L, 3L, 1L, 2L, 3L, : 'min' not meaningful for factors, which is not super helpful, but I tracked it down to this function call:
Lines 165 to 171 in d79f691
| ind <- too_far( | |
| x = input[[sm_vars[1L]]], | |
| y = input[[sm_vars[2L]]], | |
| ref_1 = reference[[sm_vars[1L]]], | |
| ref_2 = reference[[sm_vars[2L]]], | |
| dist = dist | |
| ) |
Which doesn't check if input[[sm_vars[1L]]] and input[[sm_vars[2L]]] are numeric, as assumed by too_far. Once, I fixed that the regex for the 1D case doesn't give an error, but instead produces this figure:
and these warnings:
Warning messages:
1: `stat_contour()`: Zero contours were generated
2: In min(x) : no non-missing arguments to min; returning Inf
3: In max(x) : no non-missing arguments to max; returning -Inf
4: `stat_contour()`: Zero contours were generated
5: In min(x) : no non-missing arguments to min; returning Inf
6: In max(x) : no non-missing arguments to max; returning -Inf
7: `stat_contour()`: Zero contours were generated
8: In min(x) : no non-missing arguments to min; returning Inf
9: In max(x) : no non-missing arguments to max; returning -Inf
Which is an improvement I think, but is the same unintended behaviour as in the 2D case, only now it gives errors because they both call plot_smooth.bivariate_smooth, which accidentally works okay for the 2D case, but gives these not-so-easy-to-understand warnings for the 1D case.
In any case, I decided to try to fix the issue properly. I identified that when draw_smooth_estimates checks which type of plot to use, in particular in the first part (1D):
Lines 1198 to 1236 in d79f691
| if (sm_dim == 1L && | |
| sm_type %in% c( | |
| "TPRS", "TPRS (shrink)", "CRS", "CRS (shrink)", | |
| "Cyclic CRS", "Cyclic P spline", "P spline", "B spline", "Duchon spline", | |
| "GP", | |
| "Mono inc P spline", | |
| "Mono dec P spline", | |
| "Convex P spline", | |
| "Concave P spline", | |
| "Mono dec conv P spline", | |
| "Mono dec conc P spline", | |
| "Mono inc conv P spline", | |
| "Mono inc conc P spline", | |
| "Mono inc 0 start P spline", | |
| "Mono inc 0 start P spline" | |
| )) { | |
| class(object) <- append(class(object), "mgcv_smooth", after = 0L) | |
| } else if (grepl("1d Tensor product", sm_type, fixed = TRUE)) { | |
| class(object) <- append(class(object), "mgcv_smooth", after = 0L) | |
| } else if (sm_type == "Random effect") { | |
| class(object) <- append(class(object), | |
| c("random_effect", "mgcv_smooth"), | |
| after = 0 | |
| ) | |
| } else if (sm_type == "Factor smooth") { | |
| class(object) <- append(class(object), | |
| c("factor_smooth", "mgcv_smooth"), | |
| after = 0 | |
| ) | |
| } else if (sm_type == "Constr. factor smooth") { | |
| class(object) <- append(class(object), | |
| c("sz_factor_smooth", "mgcv_smooth"), | |
| after = 0 | |
| ) | |
| } else if (sm_type == "SOS") { | |
| class(object) <- append(class(object), | |
| c("sos", "mgcv_smooth"), | |
| after = 0 | |
| ) |
It has a if-else branch for "Factor smooth", but the ti term is not caught by it, as it has the type label "Tensor product int." instead. Unfortunately, this label is also shared with tensor product interactions between numeric-continuous variables, so it isn't as easy as just checking the label. My suggestion is to change the check to something like:
} else if (
sm_type == "Factor smooth" || (
sm_type %in% c("Tensor product int.", "Tensor product") &&
any(map_lgl(object[sm_vars], is.factor))
)) {Basically, manually checking if any of the smooth variables are a factor if it is either a "Tensor product int." (ti) or just "Tensor product" (te), at least that catches the most common use-cases.
The new plots for the two cases are then:
1D fixed
2D fixed
Can't currently plot multivariate 'fs' smooths.
Skipping: ti(Sepal.Width,Petal.Width,Species)
which at least seems more consistent, although it is a shame that the unintended behaviour which sort of worked, gives that error instead, although it seems (since I didn't add the error message) that it what is intended.
Code
I have forked the repository and created a feature branch here:
https://github.com/asgersvenning/gratia-pr/tree/fix-draw-factor-smooth-interaction
Which also includes a new unit test:
that fails without the code changes, but passes after (at least on my machine).
I hope that makes sense, and if you think my suggested fix looks alright I will open a PR as well!
PS: I also ran all the other unit tests after the fix, and except for some small changes to some snapshots which I think are an artefact of some system differences, not code changes, they all passed.