install.packages(c("phylospatial", "nullcat", "ggplot2", "tidyterra", "patchwork","prioritizr", "highs"))# ps_dissim_focal() (section 5) needs the development version:# install.packages("pak"); pak::pak("matthewkling/phylospatial")
Roadmap
Concepts
Data
Diversity
Significance
Turnover
Prioritization
1. Concepts
What is spatial phylogenetics?
Mapping the distribution of evolutionary history across landscapes, by combining a phylogeny with species distributions.
Where is evolutionary diversity concentrated?
Where are old or young lineages restricted?
How does lineage composition change across space?
Where should we protect it?
Why measure biodiversity using phylogeny?
Why measure biodiversity using phylogeny?
The unit of phylogenetic diversity is the clade
Not an ancestor on a branch segment, but a whole branch you could break off the tree: all the descendants of a common ancestor at segment in the tree. Every clade has two properties:
A range: wherever any of its members occur
A unique branch segment: the evolutionary history its members share with no one else (its weight)
Every branch becomes a community matrix column
Add each clade to the species-level community matrix as a new “taxon”, and store its branch length as a weight. Diversity metrics then work on branches just as they do on species: PD is the sum of the weights of the branches present.
Common misconceptions
Misconception
Reality
Counting clades as well as species double-counts occurrences
Each branch counts once, weighted by its own length: a clade adds only the history unique to it
Clade ranges are reconstructed ancestral ranges
They are where a clade’s living descendants occur today
Phylogenetic endemism
Endemism weights each taxon by the inverse of its range size, so restricted taxa count more; phylogenetic endemism (PE) applies this to branches, summing each branch’s length divided by its range size.
Ranges don’t have to be binary
A clade’s value comes from its members: present if any are (binary), P = 1 − ∏(1 − pi) (probability), or the sum (abundance).
Quantitative measures paint richer pictures
Thresholded ranges
Continuous ranges
Moss neo- and paleo-endemism (Kling et al. 2026, Systematic Botany).
The phylospatial package
Spatial phylogenetic analysis in R, from data to conservation priorities
Quantitative data throughout: binary, probability, or abundance ranges
Spatial in and out: {terra} rasters, {sf} polygons, or plain matrices
One object, many analyses: diversity, null models, turnover, prioritization
Builds on {ape}, {vegan}, {nullcat}, {prioritizr}
On CRAN · Kling (2025) Methods in Ecology and Evolution
Other options for PD analyses in R
Many R packages compute PD across sites (Kling 2025, MEE, Table 1)
2. Data
Two ingredients
phylospatial() combines a tree and community data into one object, building a range for every branch.
A toy dataset in one line
ps_simulate() generates a random dataset, useful for learning and testing.
443 California moss species: occurrence probabilities from species distribution models on a 30 km grid, plus a phylogeny (Kling et al. 2026, Systematic Botany). Both ship with the package; moss() is a one-line shortcut for the code below.
ps_diversity() computes various measures of richness, divergence, and regularity, each in phylogenetic and species-level variations (?ps_diversity for the full list).
div <-ps_diversity(ps, metric =c("TD", "PD", "PE"))
Richness: species richness, PD, and PE
Each map scaled to its maximum. PD closely tracks richness; PE shifts weight toward places where range-restricted lineages concentrate.
Divergence and regularity
Beyond how much evolutionary history a site holds, ps_diversity() measures how it’s structured. Divergence (MPD): how distantly related co-occurring lineages are, on average. Regularity (VPD): how evenly spaced they are across the tree; low variance means evenly spaced lineages, high means clumped clusters of close relatives.
Recall that branch lengths can measure divergence, time, or diversification events. Tree scaling functions change which of those, and which part of history, the metrics weigh. Put the result in ps$tree and every metric follows.
tr <-rescale_tree(ps$tree, "tip1") # root-to-tip height = 1slice_tree(tr, min =0.5) # older half onlyslice_tree(tr, max =0.5) # recent half onlydelta_tree(tr, delta =0.3) # emphasize deep branchesdelta_tree(tr, delta =3) # emphasize recent branchesuniform_tree(tr) # every branch counts equally
4. Significance
Is there more PD than expected?
Raw PD mostly tracks richness. To see more, compare each site to a null expectation.
Null models test hypotheses
Randomize the data many times, holding some features fixed
Recompute the metric each time
Compare observed to the null distribution
What you hold fixed defines the hypothesis.
Different nulls, different questions
Null model
Holds fixed
Randomizes
Is the pattern unusual given…
"tip_shuffle"
every range map
which lineage owns each range
the ranges, ignoring relatedness?
"curvecat"
site richness, range sizes
which species occur where
richness and range sizes?
"r0cat"
site richness
occurrences, range sizes
richness alone?
"c0cat"
range sizes
occurrences, site richness
range sizes alone?
Matrix algorithms run on binary data (fun = "nullcat") or quantitative data (fun = "quantize"); fun = "nullmodel" adds {vegan}’s options.
Randomizing quantitative data
Each site keeps its values; each species keeps its mix of strata. quantize() from {nullcat}
How many iterations?
Swap-based nulls need enough swaps to forget the original data. ps_suggest_n_iter() runs several chains and suggests a burn-in, the minimum n_iter per null community.
iters <-ps_suggest_n_iter( ps, fun ="quantize",method ="curvecat", n_iter =3e5)
Real analyses: n_iter = iters, n_rand = 1000.
Running a randomization
ps_rand() compares each site’s metrics with randomized communities, returning a null quantile per site: green = more than expected, purple = less.
ps_canape() classifies sites as centers of neo-, paleo-, mixed, or super-endemism, from the significance of PE, RPE, and CE in a randomization (Mishler et al. 2014).
canape <-ps_canape(rand)
Neo-endemism: concentrations of short, range-restricted branches
Paleo-endemism: concentrations of long, range-restricted branches
Patterns of where restricted lineages are today, not a direct test of how they got there
5. Turnover
What does phylogenetic turnover measure?
Phylogenetic dissimilarity is the share of branch length not shared by two sites. Species turnover treats every pair of different species as equally different; phylogenetic turnover is low when sites hold close relatives, and high when they hold distant lineages.
Dissimilarity from a focal site
ps_dissim_focal() maps how different every other site is from a focal location. Here, sites in the Sierra Nevada and the Mojave Desert.
A wide range of dissimilarity measures are available, including any {vegan} index and the turnover and nestedness components of Sorensen (Baselga & Orme 2012).
method = "focal_difference" is the share of the focal site’s branch length missing from each other site. Unlike Sorensen, it ignores lineages the focal site lacks, so it shows where a site’s own lineages occur. For the lineage-poor desert site, they occur almost everywhere.
ps_add_dissim() computes dissimilarity between every pair of sites and stores the site × site matrix in the object, ready for the analyses that follow.
ps <-ps_add_dissim(ps) # Sorensen
Using the dissimilarity matrix
ps$dissim is an ordinary dist object, so any distance-based method applies:
Mantel tests against geographic or environmental distance
What drives turnover: GDM ({gdm})
Group differences: PERMANOVA (adonis2())
Ordination and color (ps_ordinate(), ps_rgb())
Regionalization (ps_regions())
geo <-ps_geodist(ps)mt <- vegan::mantel(ps$dissim, geo)round(c(mt$statistic, mt$signif), 3) # r, p
[1] 0.406 0.001
Ordination
ps_ordinate() reduces the dissimilarity matrix to a few axes that capture the main gradients in lineage composition (classical or nonmetric multidimensional scaling, or PCA).
ord <-ps_ordinate(ps, method ="cmds", k =3)
Mapping turnover as color
ps_rgb() maps three ordination axes to red, green, and blue, so sites with similar colors have similar lineage composition.
rgb <-ps_rgb(ps, method ="nmds",trans = rank)
Phylogenetic regions
ps_regions() clusters sites into k phylogenetic regions; ps_regions_eval() compares how well different k fit.
regions <-ps_regions(ps, k =4,method ="ward.D2")
ps_regions_eval(ps, k =1:15,method ="ward.D2")
6. Prioritization
What is conservation prioritization?
Choosing where to add new protection, given limited resources.
Complementarity: a site’s value depends on what’s already protected and what other chosen sites already cover
Efficiency: maximize biodiversity gained per unit cost
Targets or benefits: aim to protect a set share of each feature’s range, or maximize total benefit with diminishing returns
In phylospatial, the “features” are branches of the tree, so priorities favor sites that add evolutionary history not yet represented in protected areas.
Prioritization approaches
phylospatial supports two complementary approaches:
ps_prioritize() ranks every site by its added conservation value, building the ranking one site at a time. The result is nested: any top-k sites form a sensible plan, which is useful when budgets are uncertain or protection will happen incrementally.
ps_prioritizr() sets up a problem for the {prioritizr} package, with a range-protection target or a budget for every branch, and an integer programming solver finds the optimal set of sites for that one scenario.
Prioritization inputs
Existing protection (init, from 0 to 1) and the relative cost of protecting each site, both on the moss grid (ext is the package data folder from earlier).
init <-rast(file.path(ext,"moss_protection.tif"))
pop <-rast(file.path(ext,"moss_popdens.tif"))cost <-1+log1p(pop)
Diminishing returns
A lineage’s benefit rises as more of its range is protected
lambda sets how fast the returns diminish
plot_lambda()
A priority ranking
ps_prioritize() ranks every site by its added conservation value, given existing protection, cost, and what earlier picks already protect. Rank 1 = top priority.
ps_performance() tracks how much of the tree reaches a protection target as sites are added in priority order: here, the fraction of branch length with at least 80% of its range protected.
ps_prioritizr() builds a {prioritizr} problem in which every branch is a feature with a range-protection target. A solver (e.g., {highs}) then finds the cheapest set of new sites that meets every target, rather than a ranking.