The objective of this project was to leverage ten years of Melanoplus sanguinipes (Msang) field survey data to assess the response of nymph recruitment under projected climate conditions through the year 2040. To achieve this goal, a multi-level, joint modeling framework was developed that individually assessed nymph and adult life stages while concurrently accounting for observation bias connected to preferential sampling. To ensure that density-dependence between Msang life stages was realistically captured, the model incorporated shared spatiotemporal effects that permitted adult–nymph interactions to inform demographic estimates.
Example code, simulated grasshopper occurrences, and additional information are available at OSF: https://osf.io/ny4b5/
Humphreys JM, Srygley RB, Branson DH. Geographic Variation in Migratory Grasshopper Recruitment under Projected Climate Change. Geographies. 2022; 2(1):12-30. https://doi.org/10.3390/geographies2010003
Simplified grasshopper lifecycle.
suppressMessages(library(INLA))
suppressMessages(library(rgdal))
suppressMessages(library(reshape2))
suppressMessages(library(raster))
suppressMessages(library(rasterVis))
suppressMessages(library(rgeos))
suppressMessages(library(maptools))
suppressMessages(library(mapproj))
suppressMessages(library(spdep))
suppressMessages(library(ggplot2))
suppressMessages(library(ggthemes))
suppressMessages(library(GISTools))
suppressMessages(library(lattice))
suppressMessages(library(gridExtra))
suppressMessages(library(dplyr))
suppressMessages(library(deldir))
suppressMessages(library(sp))
suppressMessages(library(viridis))
suppressMessages(library(pals))
suppressMessages(library(kableExtra))
suppressMessages(library(stringi))
Study area jurisdictional boundaries.
= "+proj=longlat +datum=WGS84 +no_defs +ellps=WGS84 +towgs84=0,0,0"
LL84
= "+proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0
nProj +x_0=0 +y_0=0 +k=1 +units=km +nadgrids=@null +no_defs"
= map("state", fill = TRUE, plot = FALSE)
States
= sapply(strsplit(States$names, ":"), function(x) x[1])
IDs
= map2SpatialPolygons(States, IDs = IDs, proj4string = CRS(LL84))
States
= sapply(slot(States, "polygons"), function(x) slot(x, "ID"))
pid
= data.frame(ID = 1:length(States), row.names = pid)
p.df
= SpatialPolygonsDataFrame(States, p.df)
States $NAME = rownames(States@data)
States
= subset(States, NAME == "wyoming")
West.states $NAME = stri_trans_totitle(West.states$NAME)
West.states
$Miss = "West"
West.states
= West.states Domain
Key data attributes include:
1. Set indicating if location is a grasshopper (GH), grid location from raster (grid), or spatial node (Node) from triangulated mesh reconstructed below.
2. Class Age class of grasshopper or designation as grid or node as decribed in line above.
3. Year Calendar year of grasshopper survey. A value of 0 (zero) indicates data is a node or grid location as described above.
4. Exp.w Geographic exposure, or area of natural neighborhood surronding location.
= read.csv("simulated_gh.csv", stringsAsFactors = FALSE)
all.data
dim(all.data)
[1] 185915 29
unique(all.data$Set)
[1] "GH" "grid" "Node"
unique(all.data$Class)
[1] "Adult" "Nymph" "grid" "Node"
unique(all.data$YEAR)
[1] 2020 2021 0
range(all.data$Exp.w, na.rm = T)
[1] 0.0000 174.5256
= SpatialPointsDataFrame(all.data[, c("Long", "Lat")], all.data)
all.data proj4string(all.data) = nProj
= subset(all.data, Set == "GH")
GH.pnts.p GH.pnts.p
class : SpatialPointsDataFrame
features : 596
extent : -12361.26, -11585.93, 5015.391, 5620.163 (xmin, xmax, ymin, ymax)
crs : +proj=merc +a=6378137 +b=6378137 +lat_ts=0 +lon_0=0 +x_0=0 +y_0=0 +k=1 +units=km +nadgrids=@null +wktext +no_defs
variables : 29
names : ID, Set, YEAR, Long, Lat, Count, Class, Exp.w, Domain, NN, cur_clim_LD, clim_LD_2040_126, clim_LD_2040_245, clim_LD_2040_370, clim_LD_2040_585, ...
min values : 8, GH, 2020, -12361.2554370416, 5015.39106268664, 1, Adult, 0, in, 1.56953592522768e-06, -2.86047489912884, -3.87705961002293, -3.77291540223685, -3.81862904696371, -4.04823840040748, ...
max values : 1767, GH, 2021, -11585.9311691453, 5620.16327383931, 30, Nymph, 0, in, 78.1600000219902, 2.10521616116018, 0.900618845429641, 0.734917451059067, 0.802405526310221, 0.888729793142446, ...
State of Wyoming wrt the United States
= fortify(States)
States.ll = fortify(Domain)
Domain.ll
= seq(-120, -80, 10)
ewbrks = seq(25, 50, 5)
nsbrks = unlist(lapply(ewbrks, function(x) ifelse(x < 0, paste(x, "°W"), ifelse(x >
ewlbls 0, paste(x, "°W"), x))))
= unlist(lapply(nsbrks, function(x) ifelse(x < 0, paste(x, "°S"), ifelse(x >
nslbls 0, paste(x, "°N"), x))))
ggplot(States.ll, aes(long, lat, group = group)) + geom_polygon(fill = "gray95",
col = "gray60", size = 0.5, alpha = 0.5) + geom_polygon(data = Domain.ll, aes(long,
group = group), col = "gray80", fill = "gray60", size = 0.5) + xlab(" ") +
lat, ylab(" ") + scale_x_continuous(breaks = ewbrks, labels = ewlbls, expand = c(0,
0)) + scale_y_continuous(breaks = nsbrks, labels = nslbls, expand = c(0, 0)) +
coord_equal() + theme(panel.grid.minor = element_line(colour = "gray90", size = 0.5),
panel.grid.major = element_blank(), panel.background = element_blank(), plot.background = element_blank(),
panel.border = element_blank(), strip.text = element_text(face = "bold", size = 18),
strip.background = element_blank(), axis.text.x = element_text(face = "bold",
size = 22), axis.text.y = element_text(face = "bold", size = 22), legend.title = element_text(size = 17,
face = "bold"), legend.text = element_text(size = 11, face = "bold"), legend.position = "bottom",
plot.title = element_blank())
Panels depict location and relative counts of grasshoppers by year.
= spTransform(GH.pnts.p, LL84)
GH.plt.ll
= GH.plt.ll@data %>%
GH.plt.df mutate(Long = GH.plt.ll@coords[, 1], Lat = GH.plt.ll@coords[, 2])
$Size = Scale(GH.plt.df$Count) * 8
GH.plt.df$Size = GH.plt.df$Size + 0.5
GH.plt.df
ggplot(Domain.ll, aes(long, lat, group = group, col = Class, size = Size)) + geom_polygon(fill = "gray90",
col = "darkgray", size = 0.01) + geom_point(data = GH.plt.df, aes(Long, Lat,
group = NA), pch = 1, stroke = 0.5, size = GH.plt.df$Size) + xlab(" ") + ylab(" ") +
facet_wrap(~YEAR, ncol = 5) + coord_equal() + theme(panel.grid.minor = element_blank(),
panel.grid.major = element_blank(), panel.background = element_blank(), plot.background = element_blank(),
panel.border = element_blank(), strip.text = element_text(face = "bold", size = 18),
strip.background = element_blank(), axis.text.x = element_blank(), axis.text.y = element_blank(),
axis.ticks.x = element_blank(), axis.ticks.y = element_blank(), legend.title = element_text(size = 17,
face = "bold"), legend.text = element_text(size = 11, face = "bold"), legend.position = "bottom",
axis.title.x = element_blank(), axis.title.y = element_blank(), plot.title = element_blank())
Example spatial triangulation with buffer around exterior. Simulated grasshopper points overlain.
= spTransform(Domain, nProj)
Domain.p
= gConvexHull(Domain.p)
Hull = gBuffer(Hull, width = 10)
Hull
= inla.sp2segment(Hull)
dom.bnds
= inla.mesh.2d(boundary = dom.bnds, loc = GH.pnts.p, cutoff = 10, max.edge = 15,
mesh.dom offset = c(10, 25), min.angle = 30)
$n mesh.dom
[1] 6441
plot(mesh.dom, main = " ")
plot(Domain, add = T, border = "red", lwd = 2)
plot(GH.pnts.p, col = "red", add = T)
Duplicate mesh nodes for each year in analysis. A separate copy of mesh nodes are created for each year with grasshopper observations.
= length(which(all.data@data$Set == "GH"))
gh.N .1 = all.data@data %>%
nodefilter(Set == "Node")
.1 = node.1[sample(nrow(node.1), gh.N), ]
nodedim(node.1)
[1] 596 29
= seq(2020, 2021, 1)
myYears for (i in 1:length(myYears)) {
= node.1
node.tmp $YEAR = myYears[i]
node.tmp
if (i == 1) {
= node.tmp
time.nodes else {
} = rbind(time.nodes, node.tmp)
time.nodes
}
}
dim(time.nodes)
[1] 1192 29
dim(time.nodes)[1]/mesh.dom$n
[1] 0.1850644
unique(time.nodes$YEAR)
[1] 2020 2021
= all.data@data %>%
GH.A filter(Class == "Adult")
= all.data@data %>%
GH.N filter(Class == "Nymph")
= rbind(GH.A, time.nodes)
FE.A = rbind(GH.N, time.nodes) FE.N
Combing and organizing data as list() objects.
= FE.A #Copy for adults
FE.df
= length(levels(factor(factor(FE.df$YEAR)))) #Time steps
k $Year.int = as.integer(factor(FE.df$YEAR)) #Year as integer
FE.df
= cbind(FE.df$Long, FE.df$Lat) #projected locations
locs
= inla.spde.make.A(mesh.dom, #Matrix joining matching to spatial mesh
A.mat alpha = 2,
loc=locs,
group = FE.df$Year.int) #indexed by time
= A.mat #copy matrix
A.mat2
= inla.spde2.pcmatern(mesh.dom, alpha = 2, #Spatial prior
spde0 prior.range=c(600, 0.01), # a 1% probability that spatial range is withing 600km
prior.sigma=c(1, 0.01),
constr = TRUE)
.1 = inla.spde.make.index("Field.1",
Field$n.spde,
spde0n.group=k) #Index adults spatial fields by time (Pattern level)
.2 = inla.spde.make.index("Field.2",
Field$n.spde,
spde0n.group=k) #Copy of above index adults spatial fields by time (process level)
$Exp.sc = FE.df$Exp.w #Exposure: geographic areal extents (natural neighborhoods) for mesh nodes
FE.df
$NN2 = round(FE.df$NN/10, 3) #scale
FE.dfrange(FE.df$NN2)
[1] 0.000 10.597
= list(c(Field.1, #spatial index (pattern level)
FE.lst list(intercept1 = 1)), #dedicated intercept
list(NN.1 = FE.df[,"NN2"],
Steps = FE.df[,"Year.int"],
ID = FE.df[,"ID"]))
$OBS = ifelse(FE.df$Set == "GH", 1, 0) #detection = 1, non detection = 0
FE.df
= inla.stack(data = list(Y = cbind(FE.df$OBS, NA, NA, NA), #four columns response matrix (2 adult levels, 2 nymph levels); adult pattern level here
App.stk e = FE.df$Exp.sc), #exposure
A = list(A.mat2, 1), #projection matrix
effects = FE.lst, #variables
tag = "App.0") #label
= list(c(Field.2, #Process Level spatial index
FE.lst2 list(intercept2 = 1)), #dedicated intercept
list(hist.clim1 = FE.df[,"cur_clim_LD"], #climate
pred.soil1 = FE.df[,"soils_LD"], #soils
pred.gHab1 = FE.df[,"gHab_LD"], #vegetation
NN = FE.df[,"NN2"],
Year = FE.df[,"YEAR"],
Year.int = FE.df[,"Year.int"],
Step = FE.df[,"Year.int"],
IDx = FE.df[,"ID"]))
= inla.stack(data = list(Y = cbind(NA, FE.df$Count, NA, NA), #2nd column = adults counts
A.resp.stk e = FE.df$Exp.sc),
A = list(A.mat, 1),
effects = FE.lst2,
tag = "Aresp.0")
= inla.stack(App.stk, A.resp.stk) #combine adult pattern and process levels Adult.stk
Same code used to setup adult levels, but now specific to nymphs.
= FE.N #Copy for nymphs
FE.df
= length(levels(factor(factor(FE.df$YEAR)))) #time steps
k $Year.int = as.integer(factor(FE.df$YEAR)) #year ids
FE.df
= cbind(FE.df$Long, FE.df$Lat) #nymph locations
locs
= inla.spde.make.A(mesh.dom, #spatial projection matrix
A.nym alpha = 2,
loc=locs,
group = FE.df$Year.int) #index by year
= A.nym #copy
A.nym2
.1n = inla.spde.make.index("Field.1n",
Field$n.spde,
spde0n.group=k)
.2n = inla.spde.make.index("Field.2n",
Field$n.spde,
spde0n.group=k)
.3a = inla.spde.make.index("Field.3a", #extra index to copy adult level
Field$n.spde,
spde0n.group=k)
#Nymph Pattern Level
$Exp.sc = FE.df$Exp.w #exposure
FE.df$NN2 = round(FE.df$NN/10, 3) #scaling
FE.df
= list(c(Field.1n, #Pattern level spatial index for nymphs
FE.lst list(intercept3 = 1)),
list(NN.1 = FE.df[,"NN2"],
Steps = FE.df[,"Year.int"],
ID = FE.df[,"ID"]))
$OBS = ifelse(FE.df$Set == "GH", 1, 0) #detection = 1, non detection = 0
FE.df
= inla.stack(data = list(Y = cbind(NA, NA, FE.df$OBS, NA), # 3rd column is nymph pattern
Npp.stk e = FE.df$Exp.sc), #areal exposure
A = list(A.nym2, 1), #spatial matrix
effects = FE.lst, #variables
tag = "Npp.0") #label
#Nymph Process Level
= list(c(Field.2n, #Nymph spatial index
FE.lst2 .3a, #spatial inde to copy adult level
Fieldlist(intercept4 = 1)),
list(hist.clim2 = FE.df[,"cur_clim_LD"],
pred.soil2 = FE.df[,"soils_LD"],
pred.gHab2 = FE.df[,"gHab_LD"],
NN = FE.df[,"NN2"],
Year = FE.df[,"YEAR"],
Year.int = FE.df[,"Year.int"],
Step = FE.df[,"Year.int"],
IDx = FE.df[,"ID"]))
= inla.stack(data = list(Y = cbind(NA, NA, NA, FE.df$Count), #4th column is nymph counts
Nresp.stk e = FE.df$Exp.sc),
A = list(A.nym, 1),
effects = FE.lst2,
tag = "Nresp.0")
= inla.stack(Npp.stk, Nresp.stk) # combine nymph pattern and process levels Nym.stk
= inla.stack(Adult.stk, Nym.stk) big.stk
= list(theta = list(prior = 'normal', param = c(0, 5))) #normal prior
hc1 = list(model = 'ar1', hyper = hc1) #order-1 autoregressive prior
ctr.g
= Y ~ -1 + intercept1 + #adult pattern intercept
Frm1 + #adult process intercept
intercept2 + #nymph pattern intercept
intercept3 + #nymph process intercept
intercept4 f(Field.1, #adult pattern spatial index
model=spde0, #spatial prior as defined above
group = Field.1.group, #group by year
control.group=ctr.g) + #prior
f(Field.2, #copy adult pattern to adult process level
copy = "Field.1", #copy this field
group = Field.2.group,
fixed = FALSE,
hyper = hc1) + #noraml prior
f(Field.1n, #nymph pattern
model=spde0,
group = Field.1n.group,
control.group=ctr.g) +
f(Field.2n, #copy nymph pattern to nymph process level
copy = "Field.1n",
group = Field.2n.group,
fixed = FALSE,
hyper = hc1) +
f(Field.3a, # copy adult pattern to nymph process level
copy = "Field.2",
group = Field.3a.group,
fixed = FALSE,
hyper = hc1) +
+ pred.soil1 + pred.gHab1 + #environmental variables specific to adults
hist.clim1 + pred.soil2 + pred.gHab2 #environmental variables specific to nymphs hist.clim2
# theta is mean hyperparametrs from prior model run, inclusion speeds up reruns
= c(3.653, 3.552, 3.101, -0.893, 3.456, 3.246, -0.806, 3.521, 5.517, 5.622, -0.241)
theta1
= inla(Frm1, #model formula
Model.gh num.threads = 12, #cores per node to use (turn off for default desktops)
data = inla.stack.data(big.stk), #combined adult and nymph data
family = c("gaussian", "poisson", "gaussian", "poisson"), #pattern levels with gaussian, process level counts are poisson
verbose = FALSE, #show outputs as model runs
E = inla.stack.data(big.stk)$e, #Areal exposure
control.fixed = list(prec = 1, prec.intercept=1), #proper intercepts (fixed priors)
control.predictor = list(
A = inla.stack.A(big.stk), #matrices
compute = TRUE, # include fitted results
link = 1), # link functions
control.mode = list(restart = TRUE, theta = theta1), # use theta above to speed up run
control.inla = list(strategy="adaptive", #strategy to reduce run time
int.strategy = "eb"), # use modes to speed up this example
control.compute=list(dic = TRUE, cpo = TRUE, waic = TRUE)) #mdel indices to estimate