Appendix

R packages

The following is a non-exhaustive list of R packages which contain GAM functionality. Each is linked to the CRAN page for the package. Note also that several build upon the mgcv package used for this document. I haven’t really looked much lately, as between mgcv and brms there is little you can’t do. I can vouch that gamlss and VGAM are decent too, but I’ve not used either in a long time.

brms Allows for Bayesian GAMs via the Stan modeling language (very new implementation).

CausalGAM This package implements various estimators for average treatment effects.

gam Functions for fitting and working with generalized additive models.

gamboostLSS: Boosting models for fitting generalized additive models for location, shape and scale (gamLSS models).

GAMens: This package implements the GAMbag, GAMrsm and GAMens ensemble classifiers for binary classification.

gamlss: Generalized additive models for location, shape, and scale.

gamm4: Fit generalized additive mixed models via a version of mgcv’s gamm function.

gss: A comprehensive package for structural multivariate function estimation using smoothing splines.

mgcv: Routines for GAMs and other generalized ridge regression with multiple smoothing parameter selection by GCV, REML or UBRE/AIC. Also GAMMs.

VGAM: Vector generalized linear and additive models, and associated models.

A comparison to mixed models

We noted previously that there were ties between generalized additive and mixed models. Aside from the identical matrix representation noted in the technical section, one of the key ideas is that the penalty parameter for the smooth coefficients reflects the ratio of the residual variance to the variance components for the random effects (see Fahrmeier et al., 2013, p. 483). Conversely, we can recover the variance components by dividing the scale by the penalty parameter.

To demonstrate this, we can set things up by running what will amount to equivalent models in both mgcv and lme4 using the sleepstudy data set that comes from the latter55. I’ll run a model with random intercepts and slopes, and for this comparison the two random effects will not be correlated. We will use the standard smoothing approach in mgcv, just with the basis specification for random effects - bs='re'. In addition, we’ll use restricted maximum likelihood as is the typical default in mixed models.

library(lme4)
mixed_model = lmer(Reaction ~ Days + (1|Subject) + (0 + Days|Subject), 
                   data = sleepstudy)
ga_model = gam(
  Reaction ~  Days + s(Subject, bs = 're') + s(Days, Subject, bs = 're'), 
  data = sleepstudy, 
  method = 'REML'
)


In the following we can see they agree on the fixed/parametric effects, but our output for the GAM is in the usual, albeit, uninterpretable form. So, we’ll have to translate the smooth terms from the GAM to variance components as in the mixed model.


summary(mixed_model)
Linear mixed model fit by REML ['lmerMod']
Formula: Reaction ~ Days + (1 | Subject) + (0 + Days | Subject)
   Data: sleepstudy

REML criterion at convergence: 1743.7

Scaled residuals: 
    Min      1Q  Median      3Q     Max 
-3.9626 -0.4625  0.0204  0.4653  5.1860 

Random effects:
 Groups    Name        Variance Std.Dev.
 Subject   (Intercept) 627.57   25.051  
 Subject.1 Days         35.86    5.988  
 Residual              653.58   25.565  
Number of obs: 180, groups:  Subject, 18

Fixed effects:
            Estimate Std. Error t value
(Intercept)  251.405      6.885  36.513
Days          10.467      1.560   6.712

Correlation of Fixed Effects:
     (Intr)
Days -0.184
summary(ga_model)

Family: gaussian 
Link function: identity 

Formula:
Reaction ~ Days + s(Subject, bs = "re") + s(Days, Subject, bs = "re")

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  251.405      6.885  36.513  < 2e-16 ***
Days          10.467      1.560   6.712 3.67e-10 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
                  edf Ref.df      F  p-value    
s(Subject)      12.94     17  89.29 1.09e-06 ***
s(Days,Subject) 14.41     17 104.56  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.794   Deviance explained = 82.7%
-REML = 871.83  Scale est. = 653.58    n = 180


Conceptually, we can demonstrate the relationship with the following code that divides the scale by the penalty parameters, one for each of the smooth terms. However, there has been some rescaling behind the scenes regarding the Days effect, so we have to rescale it to get what we need.


rescaled_results = c(
  ga_model$reml.scale / ga_model$sp[1],
  ga_model$reml.scale / (ga_model$sp[2] / ga_model$smooth[[2]]$S.scale),
  NA
)

lmer_vcov = VarCorr(mixed_model) %>% data.frame()
gam_vcov  = data.frame(var = rescaled_results, gam.vcomp(ga_model))

My personal package mixedup does this for you, and otherwise makes comparing mixed models from different sources easier.

mixedup::extract_variance_components(mixed_model)
mixedup::extract_variance_components(ga_model)
model group effect variance sd sd_2.5 sd_97.5 var_prop
mixed Subject Intercept 627.57 25.05 15.26 37.79 0.48
mixed Subject.1 Days 35.86 5.99 3.96 8.77 0.03
mixed Residual 653.58 25.57 22.88 28.79 0.50
gam Subject Intercept 627.57 25.05 16.09 39.02 0.48
gam Subject Days 35.86 5.99 4.03 8.91 0.03
gam Residual 653.58 25.57 22.79 28.68 0.50

Think about it this way. Essentially what is happening behind the scenes is that effect interactions with the grouping variable are added to the model matrix (e.g. ~ ... + Days:Subject - 1)56. The coefficients pertaining to the interaction terms are then penalized in the typical GAM estimation process. A smaller estimated penalty parameter suggests more variability in the random effects. A larger penalty means more shrinkage of the random intercepts and slopes toward the population level (fixed) effects.

Going further, we can think of smooth terms as adding random effects to the linear component57. A large enough penalty and the result is simply the linear part of the model. In this example here, that would be akin to relatively little random effect variance.

Time and Space

One of the things to know about GAMs is just how flexible they are. Along with all that we have mentioned, they can also be applied to situations where one is interested in temporal trends or the effects of spatial aspects of the data. The penalized regression approach used by GAMs can easily extend such situations, and the mgcv package in particular has a lot of options here.

Time

A natural setting for GAMs is where there are observations over time. Perhaps we want to examine the trend over time. The SLiM would posit a linear trend, but we often would doubt that is the case. How would we do this with a GAM? We can incorporate a feature representing the time component and add it as a smooth term. There will be some additional issues though as we will see.

Here I use the data and example at Gavin Simpon’s nifty blog, though with my own edits, updated data, and different model58. The data regards global temperature anomalies.

## Global temperatures
# Original found at "https://crudata.uea.ac.uk/cru/data/temperature/"

load(url('https://github.com/m-clark/generalized-additive-models/raw/master/data/global_temperatures.RData'))


Fitting a straight line to this would be disastrous, so let’s do a GAM.

hot_gam = gam(Annual ~ s(Year), data = gtemp)
summary(hot_gam)

Family: gaussian 
Link function: identity 

Formula:
Annual ~ s(Year)

Parametric coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -0.076564   0.007551  -10.14   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
          edf Ref.df     F p-value    
s(Year) 7.923  8.696 182.2  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.903   Deviance explained = 90.7%
GCV = 0.010342  Scale est. = 0.0098059  n = 172


We can see that the trend is generally increasing, and has been more or less since the beginning of the 20th century. We have a remaining issue though. In general, a time series is autocorrelated, i.e. correlated with itself over time. We can see this in the following plot.

acf(gtemp$Annual)


What the plot shows is the correlation of the values with themselves at different lags, or time spacings. Lag 0 is it’s correlation with itself, so the value is 1.0. It’s correlation with itself at the previous time point, i.e. lag = 1, is 0.92, it’s correlation with itself at two time points ago is slightly less, 0.86, and the decreasing trend continues slowly. The dotted lines indicate a 95% confidence interval around zero, meaning that the autocorrelation is still significant 25 years apart.

With our model, the issue remains in that there is still autocorrelation among the residuals, at least at lag 1.

The practical implications of autocorrelated residuals is that this positive correlation would result in variance estimates that are too low. However, we can take this into account with a slight tweaking of our model to incorporate such autocorrelation. For our purposes, we’ll switch to the gamm function. It adds additional functionality for generalized additive mixed models, though we can just use it to incorporate autocorrelation of the residuals. In running this, two sets of output are provided, one in our familiar gam model object, and the other as a lme object from the nlme package.

hot_gam_ar = gamm(Annual ~ s(Year),
                  data = gtemp,
                  correlation = corAR1(form = ~ Year))

summary(hot_gam_ar$gam)

Family: gaussian 
Link function: identity 

Formula:
Annual ~ s(Year)

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept) -0.07696    0.01130  -6.812 1.73e-10 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
          edf Ref.df     F p-value    
s(Year) 6.879  6.879 104.1  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.901   
  Scale est. = 0.010297  n = 172
summary(hot_gam_ar$lme)
Linear mixed-effects model fit by maximum likelihood
  Data: strip.offset(mf) 
        AIC       BIC   logLik
  -289.0641 -273.3266 149.5321

Random effects:
 Formula: ~Xr - 1 | g
 Structure: pdIdnot
             Xr1      Xr2      Xr3      Xr4      Xr5      Xr6      Xr7      Xr8  Residual
StdDev: 1.252053 1.252053 1.252053 1.252053 1.252053 1.252053 1.252053 1.252053 0.1014737

Correlation Structure: AR(1)
 Formula: ~Year | g 
 Parameter estimate(s):
      Phi 
0.3614622 
Fixed effects:  y ~ X - 1 
                  Value Std.Error  DF   t-value p-value
X(Intercept) -0.0769567 0.0113296 170 -6.792539  0.0000
Xs(Year)Fx1   0.4282956 0.1692888 170  2.529970  0.0123
 Correlation: 
            X(Int)
Xs(Year)Fx1 0     

Standardized Within-Group Residuals:
        Min          Q1         Med          Q3         Max 
-2.21288171 -0.73869325  0.04665656  0.70416540  3.25638634 

Number of Observations: 172
Number of Groups: 1 

In the gam output, we see some slight differences from the original model, but not much (and we wouldn’t expect it). From the lme output we can see the estimated autocorrelation value denoted as Phi59. Let’s see what it does for the uncertainty in our model estimates.


We can in fact see that we were a bit optimistic in the previous fit (darker band). Our new fit expreses more uncertainty at every point60. So, in using a GAM for time-series data, we have similar issues that we’d have with standard regression settings, and we can deal with them in much the same way to get a better sense of the uncertainty in our estimates.

Space

Consider a data set with latitude and longitude coordinates to go along with other features used to model some target variable. A spatial regression analysis uses an approach to account for spatial covariance among the observation points. A common technique used is a special case of Gaussian process which, as we noted previously, certain types of GAMs can be seen as such also. In addition, some types of spatial models can be seen similar to random effects models, much like GAMs. Such connections mean that we can add spatial models to the sorts of models covered by GAMs too.

When dealing with space, we may have spatial locations of a continuous sort, such as with latitude and longitude, or in a discrete sense, such as regions. In what follows we’ll examine both cases.

Continuous Spatial Setting

Our example61 will use census data from New Zealand and focus on median income. It uses the nzcensus package62 which includes median income, latitude, longitude and several dozen other variables. The latitude and longitude are actually centroids of the area unit, so this technically could also be used as a discrete example based on the unit.

Let’s take an initial peek. You can hover over the points to get the location and income information.

library(nzcensus)
nz_census = AreaUnits2013 %>%
  filter(WGS84Longitude > 0 & !is.na(MedianIncome2013)) %>%
  rename(lon = WGS84Longitude,
         lat = WGS84Latitude,
         Income = MedianIncome2013) %>%
  drop_na()


So we can go ahead and run a model predicting median income solely by geography. We’ll use a Gaussian process basis, and allowing latitude and longitude to interact (bumping up the default wiggliness possible to allow for a little more nuance). What the GAM will allow us to do is smooth our predictions beyond the points we have in the data to get a more complete picture of income distribution across the whole area6364.

nz_gam = gam(Income ~ s(lon, lat, bs = 'gp', k = 100, m = 2), data = nz_census)
summary(nz_gam)

Family: gaussian 
Link function: identity 

Formula:
Income ~ s(lon, lat, bs = "gp", k = 100, m = 2)

Parametric coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  29497.8      148.1   199.2   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
             edf Ref.df     F p-value    
s(lon,lat) 76.38   90.1 7.445  <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =   0.27   Deviance explained = 30.1%
GCV = 4.0878e+07  Scale est. = 3.9105e+07  n = 1784


Using the Gaussian process smooth produces a result that is akin to a traditional spatial modeling technique called kriging. There are many other features to play with, as well as other bases that would be applicable, so you should feel free to play around with models that include those.

Alternatively, as we did with the time series, we could instead deal with spatial autocorrelation by specifying a model for the residual structure. First, we can simply test for spatial autocorrelation in the income variable via the well-worn Moran’s I statistic. Given some weight matrix that specifies the neighborhood structure, such that larger values mean points are closer to one another, we can derive an estimate. The following demonstrates this via the ape package65.

inv_dist = with(nz_census, 1/dist(cbind(lat, lon), diag = TRUE, upper = TRUE))
inv_dist = as.matrix(inv_dist)
## ape::Moran.I(nz_census$Income, weight = inv_dist, scaled = TRUE)
observed expected sd p.value
0.14 0.00 0.00 0.00


While statistically significant, there actually isn’t too much going on, though it may be enough to warrant dealing with in some fashion. As with the time series, we’ll have to use the functionality with gamm, where the underlying nlme package provides functions for spatial correlation structures. The following shows how this might be done. If you run this be prepared to wait for a few minutes.

gamm_spat = gamm(
  Income ~ s(x) + s(y) + z, # choose your own features here
  data = nz_census,
  correlation = corSpatial(form = ~ lon + lat, type = 'gaussian')
)
plot(gamm_spat)

So whether you choose to deal with the spatial autocorrelation explicitly by using something like coordinates as features in the model itself, or via the residual correlation structure, or perhaps both, is up to you66.

Discrete

What about the discrete case, where the spatial random effect is based on geographical regions? This involves a penalty that is based on the adjacency matrix of the regions, where if there are \(g\) regions, the adjacency matrix is a \(g \times g\) indicator matrix where there is some non-zero value when region i is connected to region j, and 0 otherwise. In addition, an approach similar to that for a random effect is used to incorporate observations belonging to specific regions. These are sometimes referred to as geoadditive models.

You’ll be shocked to know that mgcv has a smooth construct for this situation as well, bs = 'mrf', where mrf stands for Markov random field, which is an undirected graph.

The following67 will model the percentage of adults with only a high school education. Unfortunately, when dealing with spatial data, getting it into a format amenable to modeling will often take some work. Specifically, mgcv will need a neighborhood list to tell it how the different areas are linked68. Furthermore, the data we want to use will need to be linked to the data used for mapping.

The first step is to read a shapefile that has some county level information. You could get this from census data as well69.

# contiguous states c(1,4:6, 8:13, 16:42, 44:51, 53:56)
library(sp)
shp = rgdal::readOGR('data/us_county_hs_only')
OGR data source with driver: ESRI Shapefile 
Source: "/Users/micl/Documents/Stats/Repositories/Docs/generalized-additive-models/data/us_county_hs_only", layer: "us_county_hs_only"
with 3233 features
It has 11 fields
Integer64 fields read as strings:  ALAND AWATER 
## select michigan, and convert % to proportion
mich_df = shp[shp$STATEFP %in% c(26),] %>%   # add other FIPS codes as desired
  as_tibble() %>%
  droplevels() %>%
  mutate(
    hsd = hs_pct / 100,
    county = stringr::str_replace(tolower(NAME), pattern = '\\.', ''),
    county = factor(county)
  )

The following creates a neighborhood list70. We also need names to match the values in the data, as well as the plotting data to be used later. I just made them lower case and remove punctuation. If you use more than one state, you will have to deal with duplicated names in some fashion.

nb = spdep::poly2nb(shp[shp$STATEFP %in% c(26), ], row.names = mich_df$county)
names(nb) = attr(nb, "region.id")

With neighborhood in place, we can now finally run the model. Note that the ID used for the smooth, in this case county, needs to be a factor variable. If not, you will get an uninformative error message that doesn’t tell you that’s the problem. For this demonstration we’ll not include any other features in the model, but normally you would include any relevant ones.

gam_mrf = gam(
  # define MRF smooth
  hsd ~ s(county, bs = 'mrf', xt = list(nb = nb)),
  data   = mich_df,
  method = 'REML',
  # fit a beta regression
  family = betar
)
summary(gam_mrf)

Family: Beta regression(103.966) 
Link function: logit 

Formula:
hsd ~ s(county, bs = "mrf", xt = list(nb = nb))

Parametric coefficients:
            Estimate Std. Error z value Pr(>|z|)    
(Intercept) -0.57837    0.02237  -25.85   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Approximate significance of smooth terms:
            edf Ref.df Chi.sq p-value  
s(county) 29.75  45.58  65.33  0.0297 *
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

R-sq.(adj) =  0.492   Deviance explained = 67.4%
-REML = -113.47  Scale est. = 1         n = 83
mich_df = mich_df %>%
  mutate(fit = predict(gam_mrf, type = 'response'))

Now we can plot it. Plotly works with maps package objects that have been converted via ggplot2’s map_data function. So, we create some plot-specific data, and then add our fitted values to it. We then add our own coloring based on the fitted values, and a custom clean theme.

plotdat = map_data("county", 'michigan') %>%
  left_join(mich_df, by = c('subregion' = 'county')) %>%
  mutate(fillcol = cut(fit, breaks = seq(.25, .45, by = .025)))

p = plotdat %>%
  group_by(subregion) %>%
  plot_ly(
    x = ~ long,
    y = ~ lat,
    color  = ~ fillcol,
    colors = scico::scico(100, palette = 'tokyo'),
    text   = ~ subregion,
    hoverinfo = 'text'
  ) %>%
  add_polygons(line = list(width = 0.4)) %>%
  layout(title = "% with Maximum of HS Education in Michigan") %>%
  theme_blank()


Be prepared, as this potentially will be a notable undertaking to sort out for your given situation, depending on the map objects and structure you’re dealing with.

A Discrete Alternative

One of the things that has puzzled me is just how often people deal with geography while ignoring what would almost always be inherent correlation in discrete geographical or other units. In the social sciences for example, one will see a standard random effects approach, i.e. a mixed model, applied in the vast majority of situations where the data comes from multiple regions. This will allow for region specific effects, which is very useful, but it won’t take advantage of the fact that the regions may be highly correlated with one another with regard to the target variable of interest, even after controlling for other aspects.

We’ve already been using gamm, but haven’t been using the typical random effects approach with it. We could do so here, but we can also just stick to the usual gam function, as it has a basis option for random effects. One thing that distinguishes the mixed model setting is that observations will be clustered within the geographical units. So for our example, we’ll use the Munich rent data available from the gamlss family of packages, which contains objects for the Munich rent data and boundaries files of the corresponding districts from the 1999 survey. The rent99 data contains information about rent, year of construction, weather it has central heating, etc. Important for our purposes is the district identifier. The following shows the data structure for some observations.

rent rentsqm area yearc location bath kitchen cheating district
109.95 4.23 26.00 1,918.00 2 0 0 0 916.00
243.28 8.69 28.00 1,918.00 2 0 0 1 813.00
261.64 8.72 30.00 1,918.00 1 0 0 1 611.00
106.41 3.55 30.00 1,918.00 2 0 0 0 2,025.00
133.38 4.45 30.00 1,918.00 2 0 0 1 561.00
339.03 11.30 30.00 1,918.00 2 0 0 1 541.00
215.23 6.94 31.00 1,918.00 1 0 0 0 822.00
323.23 10.43 31.00 1,918.00 1 0 1 1 1,713.00
216.31 6.76 32.00 1,918.00 1 0 0 0 1,812.00
245.28 7.43 33.00 1,918.00 2 0 0 0 152.00
285.38 8.39 34.00 1,918.00 2 0 0 0 943.00
238.31 6.81 35.00 1,918.00 1 0 0 1 1,711.00
374.46 10.70 35.00 1,918.00 2 0 0 1 231.00
137.95 3.83 36.00 1,918.00 2 0 0 1 411.00
188.36 4.96 38.00 1,918.00 1 0 0 0 1,711.00


Here again we’ll use a Markov random field smooth, and for comparison a mixed model with a random effect for district. The plots show that, while close, they don’t exactly come to the same conclusions for the district fitted values.

library(gamlss.data)

# prep data
rent99 = rent99 %>%
  mutate(district = factor(district))

rent99.polys[!names(rent99.polys) %in% levels(rent99$district)] = NULL

# run mrf and re models
gam_rent_mrf = gam(rent ~ s(district, bs = 'mrf', xt = list(polys = rent99.polys)),
                   data = rent99,
                   method = 'REML')

gam_rent_re = gam(rent ~ s(district, bs = 're'),
                  data = rent99,
                  method = 'REML')


Next we show the plot of the estimated random effects of both models on the map of districts71. Gaps appear because there isn’t data for every district available, as some are districts without houses like parks, industrial areas, etc.

As we might have expected, there appears to be more color coordination with the MRF result (left), since neighbors are more likely to be similar. Meanwhile, the mixed model approach, while showing similar patterning, does nothing inherently to correlate one district with the ones it’s next to, but may allow for more regularization.

Either of these models is appropriate, but they ask different questions. The MRF approach may produce better results since it takes into account the potential similarity among neighbors, but also may not be necessary if there isn’t much similarity among geographical units. One should also think about whether the other features in the model may account spatial autocorrelation or not, or unspecified unit effects, and proceed accordingly. In addition, there are approaches that would allow for a mix of both the unstructured and spatial random effects. See Riebler et al. (2016) and the brms package and Mitzi Morris’ note here .

References

Riebler, Andrea, Sigrunn H Sørbye, Daniel Simpson, and Håvard Rue. 2016. “An Intuitive Bayesian Spatial Model for Disease Mapping That Accounts for Scaling.” Statistical Methods in Medical Research 25 (4): 1145–65.
Simpson, Gavin L. 2018. “Modelling Palaeoecological Time Series Using Generalised Additive Models.” Frontiers in Ecology and Evolution 6. https://doi.org/10.3389/fevo.2018.00149.

  1. See ?sleepstudy for details.↩︎

  2. You can verify this by running model.matrix(ga_model).↩︎

  3. Much like with Gaussian process regression, where it’s perhaps a bit more explicit.↩︎

  4. Simpson (2018) has offered an article regarding this topic as well.↩︎

  5. All the same variables in the lme output start with X. This is more to avoid confusion in the functions behind the scenes.↩︎

  6. I don’t show it to keep the plot clean, but the fitted values are essentially the same.↩︎

  7. This example is inspired by the post by Peter Ellis, which you can find here.↩︎

  8. Because of course there is an R package just for New Zealand census data.↩︎

  9. The m argument allows one to specify different types of covariance functions.↩︎

  10. This visualization was created with plotly. In case you’re wondering, it was a notable ordeal to figure out how to make it, and nowadays, presumably there would be more efficient ways using sf/spatial features with ggplot.↩︎

  11. Using scaled = TRUE results in a correlation metric that goes from -1 to 1.↩︎

  12. For a nice discussion of this, see the Q & A at Stack Exchange, and note the top two answers to the question “Why does including latitude and longitude in a GAM account for spatial autocorrelation?”.↩︎

  13. This example comes from Gavin Simpson’s blog, which itself is based on an article at The Pudding.↩︎

  14. There are actually three different types of objects one could supply here, but unfortunately the one thing mgcv doesn’t do is work with any spatial objects one would already have from the popular R packages used for spatial modeling and visualization. The fact that this list is actually a special class object is of no importance here, it is read simply as a standard list object.↩︎

  15. Data with this high school graduation rate found on GitHub. But you can also find this sort of thing with the tigris and tidycensus packages.↩︎

  16. If you look at the markdown document for this on GitHub you’ll see the code for how to create this using an object from the maps packages rather than needing a shapefile.↩︎

  17. Unfortunately, I have neither the data nor the desire to try and make this a pretty plot. It just uses the basic mgcv plot and an attempted trick (which may not be entirely accurate) to superimpose the mixed model results onto the MRF data.↩︎