morphospace
has been built to handle the most common
types of geometric morphometrics data that can be found in the
paleobiological and evolutionary biology literature. Below, the general
capabilities of this package are showcased using three data sets
representing these data types and various spatio-temporal scales that
are typically addressed when studying evolutionary variation.
Most of the capabilities for 2D landmark data visualization has been
already covered in General
usage. This section will quickly show how to use a curve or set of
curves outlining aspects of the phenotype not captured by the chosen
landmarks to improve visualizations via TPS interpolation. To do so we
use the wings
data set, which has been taken from Soto et
al. (2012) and includes wing shape data from 263 experimentally bred
specimens belonging to two recently diverged cactophilic
Drosophila species, D. koepferae and D.
buzzattii. These two species show preference to different host
cacti (columnar cacti from the genus Trichocereus and prickly
pears from the genus Opuntia, respectively) and are considered
morphologically cryptic (usually, they can be recognized only from
genital morphology). The data set include the centroid size of each
wing, as well as information on the host cacti each specimen was bred
on, its species, sex, isofemale line, and replica. Also included is a
template containing a series of curves describing wing outline and veins
(created using build_template2d
), which will be warped
using TPS interpolation and used to generate fancy shape models for our
morphospace.
# Load morphospace as well as geomorph and magrittr
library(morphospace)
library(geomorph)
library(magrittr)
# Load wing data and extract shapes, centroid sizes, classification
# of sex and species, links between landmarks, template, and phylogeny
data("wings")
shapes <- wings$shapes
sizes <- log(wings$sizes)
species <- wings$data$species
cactus <- wings$data$cactus
line <- wings$data$line
sex <- wings$data$sex
replica <- wings$data$replica
curvs <- wings$template
# Inspect shapes using links
pile_shapes(shapes, wings$links)
# Generate morphospace but use warped wings as background models
spxcac <- species:cactus
mspace(shapes, template = curvs, col.ldm = "black", cex.ldm = 5, plot = FALSE) %>%
proj_shapes(shapes = shapes,
col = rep(c("blue", "orange"), each = 2)[spxcac],
pch = rep(c(16, 1), times = 2)[spxcac]) %>%
proj_groups(shapes = shapes, groups = spxcac, ellipse = TRUE,
col = rep(c("blue", "orange"), each = 2),
lty = rep(c(1, 2), times = 2), lwd = 2) %>%
plot_mspace(legend = TRUE, cex.legend = 1.5)
title("Morphospace using template")
Assume for a second we are interested in exploring whether we can
identify morphological differences between species. The pattern we are
seeing suggets that variation in wing morphology is very subtle, that
wings of these two species are basically indistinguishable, and that
cactus has a large effect on wing morphology. However, there is a lot of
‘noise’ (i.e. sexual dimorphism, allometric variation, genetic
variation, even host cactus) we can get rid of using
detrend_shapes
. We do this species-wise, so that patterns
of interspecific variation are not blurred.
detr_shapes <- shapes * 0
for(i in 1:2) {
index <- species == levels(species)[i]
subshapes <- shapes[,,index]
subsex <- sex[index]
subline <- line[index]
subreplica <- replica[index]
subsizes <- sizes[index]
subcactus <- cactus[index]
subdetr_shapes1 <- lm(two.d.array(subshapes) ~ subsex * subline * subreplica) %>%
detrend_shapes(method = "residuals") %>% arrayspecs(p = 9, k = 2)
subdetr_shapes2 <- lm(two.d.array(subdetr_shapes1) ~ subsizes) %>%
detrend_shapes(xvalue = max(subsizes), method = "residuals") %>% arrayspecs(p = 9, k = 2)
subdetr_shapes3 <- lm(two.d.array(subdetr_shapes2) ~ subcactus) %>%
detrend_shapes(xvalue = c("Op", "Tr")[i], method = "residuals") %>% arrayspecs(p = 9, k = 2)
detr_shapes[,,index] <- subdetr_shapes3
}
(Note: we do this in several step for a good reason: when we
use detrend_shapes
to detrend shape variation, the
grand mean is used by default as the new mean shape of the sample;
however by specifying a value or level of x
from
the model in the xvalue
argument, we can use that
new value as the mean shape for our ‘detrended’ shape variation. In this
case, we are analitically displacing all the wings to the shapes they
should have if reared in their primary host cactus, and attained their
species’ maximum size).
Once shape variation has been ‘refined’, we can ordinate it again,
and magnify variation so the representation can be more easily
interpreted using the mag
argument.
mspace(detr_shapes, template = curvs, col.ldm = "black",
cex.ldm = 5, mag = 2, plot = FALSE) %>%
proj_shapes(shapes = detr_shapes,
col = rep(c("blue", "orange"), each = 2)[spxcac],
pch = rep(c(16, 1), times = 2)[spxcac]) %>%
proj_groups(groups = spxcac, ellipse = TRUE,
col = rep(c("blue", "orange"), each = 2),
lty = rep(c(1, 2), times = 2), lwd = 2) %>%
plot_mspace(legend = TRUE, cex.legend = 1.5)
So, after ‘cleaning’ the raw variation interspecific differences become apparent with each species occupying different regions of wing morphospace (elongated and more triangular in D. koepferae, relatively higher and more rounded in D. buzzattii).
morphospace
can also handle closed outline data in the
form of Fourier coefficients resulting from an elliptic Fourier
analysis. For the purposes of import, normalize and and analize this
data we rely on the Momocs
package, and use its
"OutCoe"
format for storing closed outlines as starting
point. Below the mspace %>% proj_*
workflow is applied
to the shells
data set taken from Milla Carmona et
al. (2018). This include data from 137 specimens belonging to 4 species
of the extinct bivalve genus Ptychomya, tracking their shape
changes through a 5 million years interval from the Lower Cretaceous of
Argentina. The data set includes the information about the shape
(measured using 7 harmonics), centroid size, geochronologic age (both
relative and absolute), geographic provenance, and taxonomic
classification of each specimen.
#Load packages
library(Momocs)
library(magrittr)
library(morphospace)
# Load data from shells, extract shapes, sizes and classification into
# species, absolute and relative ages
data("shells")
shapes <- shells$shapes$coe
sizes <- log(shells$sizes)
species <- shells$data$species
ages <- shells$data$age
bzones <- shells$data$zone
# Pile shapes
pile_shapes(shapes)
# Generate morphospace using raw variation
mspace(shapes, bg.model = "light gray", plot = FALSE) %>%
proj_shapes(shapes = shapes, col = species) %>%
proj_groups(shapes = shapes, groups = species, alpha = 0.5) %>%
proj_shapes(shapes = expected_shapes(shapes, species), bg = 1:4, pch = 21) %>%
plot_mspace(legend = TRUE, cex.legend = 1)
title("Morphospace")
In this case, we are interested in the anagenetic variation shown by
each species throughout the studied interval. As before, we can use
detrend_shapes
to remove variation associated to nuisance
factors (in this case, allometric variation). We do this for species
separately, and then use expected_shapes
to extract their
mean shapes.
# 'Clean' shapes separately for each species, removing 1) variation
#associated to geographic provenance and 2) allometric variation
detr_shapes <- shapes * 0
for(i in 1:nlevels(species)) {
index <- species == levels(species)[i]
subdetr_shapes <- lm(shapes[index,] ~ sizes[index]) %>% detrend_shapes(xvalue = max(sizes[index]), method = "residuals")
detr_shapes[rownames(detr_shapes) %in% rownames(subdetr_shapes),] <- subdetr_shapes
}
Note that we don’t need arrayspecs
when
using detrend_shapes
here, that’s because Fourier
coefficients are already stored in matrix format).
Now we can use plot_mspace
again to create a more
complex hybrid plot combining shape changes associated to PC1 against
the time axis. To do so, we first do a bit of variable manipulation to
obtain 1) a factor combining species classification and relative age
(i.e. biozones), and 2) the mean shapes and 3) mean ages of the
resulting levels. Then, we build the morphospace (this time, using
detrended shapes and between-groups PCA).
(Note: in this case, two separate instances of
proj_shapes
are used: one for projecting the sampled
shapes, and another one to project the mean shape of each species in
each biozone. The corresponding scores are stored together in the order
in which shapes were projected; see next chunk for an example of how to
call them.)
# Combine levels of species and biozones, then compute 1) the mean shapes
# 2) the mean ages of the resulting groups
sp.bz <- factor(species:bzones)
shapes_agesxspecies <- expected_shapes(detr_shapes, sp.bz)
mean_ages <- tapply(ages, sp.bz, mean)
# Generate morphospace (use an additional instance of proj_shapes to project
# the mean shapes of species:biozones)
msp <- mspace(detr_shapes, FUN = bg_prcomp, groups = species,
mag = 0.5, bg.model = "light gray") %>%
proj_shapes(shapes = shapes_agesxspecies, pch = 21,
bg = c(rep(1,3), rep(2,6), rep(3,9), rep(4,3)), cex = 1.2) %>%
proj_shapes(shapes = detr_shapes, col = species) %>%
proj_groups(shapes = detr_shapes, groups = species, alpha = 0.2)
# Add anagenetic trajectories in morphospace
for(i in 1:4) {
index <- grepl(x = rownames(msp$projected$scores), pattern = levels(species)[i])
points(msp$projected$scores[index,], type = "l", lwd = 2, col = i)
}
Finally, we use plot_mspace
to plot geochronologic age
against shape. To add lines connecting the mean shape of each
combination of species and biozone, we first bind them as columns, order
them, and use a for
loop . This particular ‘hybrid’
morphospace is much more useful to depict species’ mean morphological
changes through time.
# Another visualization (probably better to interpreting temporal patterns):
# plot time axis vs first bgPC
plot_mspace(msp, x = c(mean_ages, ages), groups = FALSE)
#> Warning in morphogrid(ordination = ordination, axes = args$axes, datype =
#> mspace$ordination$datype, : x or y has been specified, axes[2] will be ignored
# Combine 1) scores corresponding to mean shapes and 2) mean ages for each
# level of species:biozone
xy <- cbind(mean_ages, msp$projected$scores[1:nlevels(sp.bz), 1])
xy <- xy[order(xy[,1]),]
# Add anagenetic trajectories in 'stratomorphospace'
for(i in 1:nlevels(species)) {
index <- grepl(x = rownames(xy), pattern = levels(species)[i])
lines(xy[index,], col = i, lwd = 2)
}
title(' "Stratomorphospace" ')
From these plots we can say that, regarding the shell outline, 1) P esbelta is rather different from the rest of the species, 2) P coihuicoensis and P. windhauseni are very similar to each other, and 3) most of the species show little or no net change over their stratigraphic span (in other words they seem to be under morphological stasis), with the exception of P. windhauseni which shows a short but noticeable directional trend towards higher shells.
Last but not least, morphospace
can also deal with 3D
landmark data (my personal preference for importing and normalizing 3D
landmark data are the functions from Morpho
, but other
packages can do this too). To show how to apply the
mspace %>% proj_*
workflow we use the
shells3D
data set taken from Milla Carmona et al. (2021),
which gathers longitudinal ontogenetic data from 67 specimens belonging
to 7 species of the extinct bivalve genus Steinmanella from the
Lower Cretaceous of Argentina. The shape of these specimens was
registered at different sizes using growth lines, and so the data set
includes a total of 278 shapes quantified using 90 surface
semilandmarks. Also included are the corresponding centroid sizes, the
id of the specimen each shape was taken from, and information about
biostratigraphic age, taxonomic classification and geographic provenance
of each specimen. The surface mesh corresponding to the specimen closest
to the consensus of the sample (found using
geomorph::findMeanSpec
) is also included.
The rgl
package (Murduch & Adler 2021) is
responsible for depiction of 3D models, which imposes some important
differences in the way morphospace
functions work. Most
importantly, each time mspace
or plot_mspace
is called, a rgl
device will pop up and the user will be
asked to rotate a reference shape to a desired orientation. Do
not close or minimize this window, just expand it and/or rotate
and zoom in/out the model and then go to the console and hit Enter there
directly.
We start by loading the relevant packages and set a rotation matrix for rgl models (you wouldn’t normally need this step, but because I since cannot rotate the device when knitting this becomes necessary. However it can also be useful to use it in your workflow too, so you don’t need to worry about finding exactly the same orientation over and over again.)
# Load packages
library(geomorph)
library(magrittr)
library(Morpho)
library(morphospace)
library(rgl)
# Set a rotation matrix
par3d(userMatrix =
matrix(c(-0.92221391,-0.37156740,-0.10704762,0,-0.37703809,
0.92551011,0.03568961,0,0.08581252,0.07327446,
-0.99361324,0,0,0,0,1), nrow = 4, ncol = 4, byrow = TRUE)
)
# Load data from 3D shells, extract shapes and classification into species
data("shells3D")
shapes <- shells3D$shapes
sizes <- log(shells3D$sizes)
species <- shells3D$data$species
ind <- shells3D$data$individual
bzones <- shells3D$data$biozone
locality <- shells3D$data$locality
mesh_meanspec <- shells3D$mesh_meanspec
# compute consensus of each species
sp_shapes <- expected_shapes(shapes, species)
# Pile shapes
pile_shapes(shapes, alpha = 0.2)
# Create morphospace using raw variation. This is interactive,
# you need to rotate the 3D model by yourself and then press
# enter into the console
mspace(shapes, cex.ldm = 2, adj_frame = c(0.9, 0.85), plot = TRUE) %>%
proj_shapes(shapes = shapes, bg = species, pch = 21) %>%
proj_groups(shapes = shapes, groups = species, ellipse = TRUE, alpha = 0.2) %>%
proj_shapes(sp_shapes, pch = 21)
title("Morphospace")
We can now focus on ontogenetic shape variation, which was the
purpose this data set was built for. Again we resort to
detrend shapes
to remove variation introduced by undesired
sources (which in this case are biostratigraphic level, geographic
provenance and, especialy, individual differences).
# Remove nuisance variation from the sample, for each species
# (a couple were registered in a single biozone and/or locality,
# which require little adjustments)
detr_shapes <- shapes * 0
for(i in 1:nlevels(species)) {
index <- species == levels(species)[i]
subshapes_mat <- two.d.array(shapes[,,index])
subind <- ind[index]
subloc <- locality[index]
subzone <- bzones[index]
if(!any(i == c(4, 5))) {
detr_shapes[,,index] <- lm(subshapes_mat ~ subind * subzone * subloc) %>%
detrend_shapes(method = "residuals") %>% arrayspecs(p = 90, k = 3)
} else {
if(i == 4) {
detr_shapes[,,index] <- lm(subshapes_mat ~ subind) %>%
detrend_shapes(method = "residuals") %>% arrayspecs(p = 90, k = 3)
} else {
detr_shapes[,,index] <- lm(subshapes_mat ~ subind * subloc) %>%
detrend_shapes(method = "residuals") %>% arrayspecs(p = 90, k = 3)
}
}
}
As with 2D landmarks we can include a template to improve
interpretability, although in this case this template is a 3D surface
mesh. This can slow down the process a bit, especially if we ask for too
many models, use transparent meshes, or use LOOCV. The template used
must be the mesh corresponding to the mean shape of the sample, which
needs to be computed beforehand (the shells3D
data set
includes the mesh corresponding to the specimen closest to the
consensus, which can be warped using Morpho::tps3d
to
obtain the mean mesh, as shown in the next chunk).
# Generate morphospace using raw variation, but with a mesh template that
# improves visualization:
# First, get shape corresponding to shells3D$mesh_meanspec using
# geomorph::findMeanSpec,
meanspec_id<- findMeanSpec(shapes)
meanspec_shape <- shapes[,,meanspec_id]
# Then get consensus shape and warp the mean spec mesh to get the mesh
# corresponding to the consensus using Morpho::tps3d
detr_meanshape <- expected_shapes(detr_shapes)
detr_meanmesh <- tps3d(x = mesh_meanspec, refmat = meanspec_shape,
tarmat = detr_meanshape)
Once we got rid of the noise and have our template prepared, we use
the refined shapes to compute the ontogenetic allometric axis of each
species using pls_shapes
. Then, we use the former to
generate a morphospace in which to project the latter, using 3D meshes
as background shape models.
# Compute allometric axis of each species using pls_shapes
pls_list <- lapply(1:nlevels(species), function(i) {
index <- species == levels(species)[i]
subshapes <- detr_shapes[,,index]
subsizes <- sizes[index]
pls_shapes(shapes = two.d.array(subshapes), X = subsizes, LOOCV = TRUE)
})
# Generate morphospace from refined variation and project allometric
# axis, add legend
allomsp <- mspace(detr_shapes, template = detr_meanmesh, bg.model = "gray",
cex.ldm = 0, invax = 1, adj_frame = c(0.9, 0.85)) %>%
proj_shapes(shapes = detr_shapes, cex = 0, col = species)
title("Refined morphospace")
# use proj_axis outside the pipeline with pipe = FALSE
for(i in 1:nlevels(species)) {
proj_axis(mspace = allomsp, obj = pls_list[[i]], lwd = 3, col = i,
type = 2, axis = 1, pipe = FALSE)
}
We can use plot_mspace
to plot an allometric axis
(represented by the axis resulting from the PLS of shape and size of the
entire sample). In order to improve interpretation of results, we will
first use expected_shapes
to compute the shape expected for
each size by the linear regression of the latter on the former (we do
this species-wise), to be projected into the allometric axis. Then, we
call plot_mspace
, including 1) the "mspace"
object built with PLS (indicating the first and only axis with
axes = 1
), 2) the size variable using x
, and
3) a legend with legend = TRUE
.
# Compute shapes expected under the linear relationship of size and shape. The
# code below will apply expected_shapes to the shapes and sizes of each species,
# rearrange them as an array, and reorder them according to their original order
pred_shapes <- abind::abind(
lapply(1:nlevels(species), function(i) {
index <- species == levels(species)[i]
subshapes <- detr_shapes[,,index]
subsizes <- sizes[index]
expected_shapes(shapes = subshapes, x = subsizes)
}),
along = 3)[,,dimnames(shapes)[[3]]]
# Then create allomorphospace using PLS to emphasize allometric variation
# (this could take a moment due to the leave-one-out cross-validation) and
# project the expected shapes (with point sizes proportional to each specimen's
# original size) as well as the species classification (this last thing is
# necessary for adding a legend):
allomsp2 <- mspace(detr_shapes, FUN = pls_shapes, X = sizes, LOOCV = TRUE,
bg.model = "gray", cex.ldm = 0, template = detr_meanmesh,
invax = 1, plot = FALSE) %>%
proj_shapes(pred_shapes, pch = 21, bg = species, cex = (sizes/max(sizes))^3) %>%
proj_groups(groups = species)
# Finally, use plot_mspace to create hybrid morphospace with shape against
# Size, and add legend
plot_mspace(allomsp2, x = sizes, axes = 1, xlab = "log-size",
adj_frame = c(0.9, 0.85), groups = FALSE, legend = TRUE)
A quick assessment of these plots tells us that there are two main groups of ontogenies: those from S. quintucoensis and S. subquadrata (which go from roughly quadrate shells with expanded posteriors to more rectangular shells) and the rest of the species, whose ontogenetic trajectories seem to be very similar in terms of orientation and magnitude (all of them go from more oval-like shells to more triangular ones, although the position of the trajectory differs). The exception is S. vacaensis, which show a protracted trajectory which reach some very elongated shell shapes. It is also apparent that the common trend is one of anteroposterior elongation, which seems to be more marked for the two species reaching larger sizes (S. pehuenmapuensis and S. vacaensis).
Milla Carmona P.S, Lazo D.G., & Soto I.M. (2018). Morphological evolution of the bivalve Ptychomya through the Lower Cretaceous of Argentina. Paleobiology, 44(1), 101-117 https://doi.org/10.1017/pab.2017.32.
Milla Carmona P.S, Lazo D.G., & Soto I.M. (2021). Ontogeny in the steinmanellines (Bivalvia: Trigoniida): an intra- and interspecific appraisal using the Early Cretaceous faunas from the Neuquén Basin as a case study. Paleobiology, in press. https://doi.org/10.1017/pab.2021.32.
Murdoch D., & Adler D. (2021). rgl: 3D Visualization Using OpenGL. R package version 0.108.3. https://cran.r-project.org/package=rgl.
Soto E.M., Goenaga J., Hurtado J.P., & Hasson E. (2012). Oviposition and performance in natural hosts in cactophilic Drosophila. Evolutionary Ecology, 26, 975-990. https://doi.org/10.1007/s10682-011-9531-5