The ascent package implements the
ASC-CFD (Assemblage Shift Characterization — Community
Functional Dynamics) framework. Communities can show the same amount of
functional turnover while following very different trajectories: one
moves to another region of trait space, another redistributes abundance
internally, a third gains or loses extreme phenotypes.
ascent separates these processes into three layers measured
in a functional space (PCoA of Gower distances between species
traits):
Table 1. The three layers of ASC-CFD.
| Layer | Metric | Ecological question |
|---|---|---|
| Position | ΔC | Where is the community moving? |
| Dispersion | ΔFDis | How is abundance being redistributed? |
| Volume | ΔFRic | Are functional boundaries expanding or collapsing? |
Each layer is tested against two null models that ask different questions:
Table 2. Reading A and B together.
| A | B | Reading |
|---|---|---|
| no | — | no evidence of change beyond sampling |
| yes | upper tail | real change, directed by traits |
| yes | no | real change without detectable trait structure |
| yes | lower tail | less change than expected from traits (compensation) |
B has little power to detect compensation; functional entities (Part V) are the recommended tool for that.
The package includes birds_sim, a simulated data set: 24
bird species with four traits (two continuous, two categorical) and
counts of individuals. Two sites were surveyed five times before and
five times after an event. At Logged, forest-dependent and
fruit-eating species decline and small open-habitat species increase; at
Control nothing changes, so any difference there is
sampling noise. A second matrix holds five communities along a landscape
gradient.
library(ascent)
# birds_sim is loaded with the package
traits <- birds_sim$traits # species x traits (categorical traits as factors)
counts <- birds_sim$counts # surveys x species, counts of individuals
samples <- birds_sim$samples # site and time of each survey
head(traits)
#> Body_Mass Beak_Length Diet Forest_Dep
#> Bird_01 90.5 24.0 seeds yes
#> Bird_02 88.0 23.5 insects yes
#> Bird_03 64.4 15.7 fruit yes
#> Bird_04 15.4 7.3 fruit yes
#> Bird_05 65.0 13.2 insects yes
#> Bird_06 31.3 13.9 seeds yes
table(samples$site, samples$time)
#>
#> After Before
#> Control 5 5
#> Logged 5 5Abundances must be counts of individuals (whole numbers): null model A needs them. Species are matched to traits by name, so the column order does not matter.
assess_functional_space(traits)
#> ==================================================
#> Functional Space Diagnostic (ascent)
#> ==================================================
#>
#> Species: 24
#> Traits: 4
#> PCoA Quality: 84.5%
#> Axes Retained: 2 (81.5% variance)
#> Negative Eigenvalues: 12 (15.5% of total)
#>
#> Quality is adequate for analysis.If the quality is below 0.8, the package warns: use
correction = "sqrt" (Gower 1971) in all functions, which
makes the space Euclidean. FRic can only be computed in samples with
more species than retained axes.
asc_paired() compares a reference state with a later
state at each site. Here each site is surveyed five times in each state,
so the surveys act as replicates and null model A permutes whole surveys
between states (accounting for variation between surveys).
res <- asc_paired(traits, counts,
sites = samples$site, time = samples$time,
ref_time = "Before")
res <- asc_null(res, n_perm = 199, seed = 1)
summary(res)
#> ==================================================
#> ASC-CFD: Multidimensional Functional Restructuring
#> ==================================================
#>
#> Number of Contrasts: 2
#> Functional Dimensionality (k): 2 (Explaining 81.5% of variance)
#> PCoA Quality: 84.5%
#>
#> --- Functional Shift Overview ---
#> Contrast Layer1_rDeltaC Layer2_DeltaFDis Layer3_DeltaFRic
#> Logged 21.01 0.0387 -6e-04
#> Control 0.38 0.0000 0e+00
#>
#> --- Null Models (A: sampling | B: traits, stratified) ---
#> Contrast Test Layer Observed SES P P_lower
#> Logged A_samples Position 2.062e-01 4.44 0.005 1.000
#> Logged A_samples Dispersion 3.875e-02 2.85 0.005 1.000
#> Logged A_samples Volume -5.724e-04 -4.63 0.015 0.015
#> Logged B_stratified Position 2.062e-01 2.27 0.005 1.000
#> Logged B_stratified Dispersion 3.875e-02 1.29 0.220 0.910
#> Logged B_stratified Volume -5.724e-04 0.58 1.000 0.535
#> Control A_samples Position 3.730e-03 -1.09 0.885 0.120
#> Control A_samples Dispersion -1.078e-05 -0.05 0.970 0.510
#> Control A_samples Volume 0.000e+00 NaN NA NA
#> Control B_stratified Position 3.730e-03 -1.03 0.870 0.135
#> Control B_stratified Dispersion -1.078e-05 0.01 0.985 0.470
#> Control B_stratified Volume 0.000e+00 NaN NA NA
#> Reading
#> larger than sampling
#> increase beyond sampling
#> decrease beyond sampling
#> directed by traits
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#>
#> Position: P = upper tail (B lower tail = compensation). Dispersion, volume: P = two-tailed.
#> Guide: A no -> no evidence of change beyond sampling | A yes + B upper -> directed by traits |
#> A yes + B no -> real change without detectable trait structure | many within-entity shifts
#> (asc_transitions) with A_entities no -> replacement among similar species.
#> B is slightly liberal for dispersion and for individuals captured in groups: treat P close to 0.05 with caution.
#>
#> --- Top Functional Drivers (Leverage, midpoint reference) ---
#> Logged: Bird_20 (+0.06), Bird_17 (+0.03), Bird_04 (+0.02), Bird_11 (+0.02), Bird_18 (+0.01)
#> Control: Bird_17 (+0.01)n_perm = 199 keeps the vignette fast; use 999 for
research. A by samples can only produce as many distinct results as
there are ways of splitting the surveys between the two states: with 5
surveys per state there are choose(10, 5) = 252; with 4
there would be only 70 (all of them are enumerated when they are fewer
than n_perm), and because swapping the two groups gives the
same distance, the smallest possible P for position would be 2/70 ≈
0.03. With few replicates, A has a floor on its p-values.
Reading. At Logged, A gives P = 0.005
for the centroid shift and B gives P = 0.005. At Control,
where nothing changed in the simulation, A gives P = 0.88: no evidence
of change beyond sampling, as expected. Read B only where A detects a
change: it tells whether the change is organised by the species’ traits
(upper tail) or smaller than expected from them (lower tail).
At Logged, the volume shrank more than expected from
sampling: some species at the edge of the functional space disappeared.
At Control, the Volume rows show NA: the
convex hull (set by the extreme species) did not change, so ΔFRic is 0
in the data and in every permutation. FRic only responds when species at
the edges of the functional space appear or disappear.
asc_transitions() describes the change without p-values.
The leverage of a species is its contribution to the
centroid shift; leverages add up exactly to the shift. The
partition says how much of the shift comes from species
shared by both states, gained or lost; its range across reference points
shows how robust that split is.
tr <- asc_transitions(res)
summary(tr, n_sp = 5)
#> ==================================================
#> ASC-CFD: Functional Transitions
#> ==================================================
#>
#> Contrasts Analyzed: 2
#>
#> --- Logged (shift = 0.2062, reference point: midpoint) ---
#> Species Group Delta_p Projection Leverage
#> Bird_20 shared 0.2374 0.2676 0.0635
#> Bird_17 shared 0.1159 0.2612 0.0303
#> Bird_04 shared -0.0906 -0.2578 0.0234
#> Bird_11 shared -0.0863 -0.2299 0.0198
#> Bird_18 shared -0.0530 -0.2015 0.0107
#> Partition of the shift:
#> Group Fraction Min_across_refs Max_across_refs
#> shared 0.994 0.991 0.996
#> gained 0.000 0.000 0.000
#> lost 0.006 0.004 0.009
#>
#> --- Control (shift = 0.0037, reference point: midpoint) ---
#> Species Group Delta_p Projection Leverage
#> Bird_17 shared -0.0143 -0.3824 0.0055
#> Bird_12 shared 0.0090 -0.2494 -0.0022
#> Bird_11 shared 0.0104 0.1588 0.0016
#> Bird_18 shared -0.0121 0.1161 -0.0014
#> Bird_04 shared -0.0081 0.1629 -0.0013
#> Partition of the shift:
#> Group Fraction Min_across_refs Max_across_refs
#> shared 1 1 1
#> gained 0 0 0
#> lost 0 0 0
#>
#> A wide Min-Max range across reference points means the partition depends on the reference: interpret with caution.
plot(res, contrast = "Logged", type = "both", n_sp = 5)Positive leverage: the species pushes the centroid in the direction of the change (it increased in the direction of the shift, or declined on the opposite side). Negative leverage: the species resists the shift.
Functional entities are groups of species with similar traits. They separate a change that reorganises the functional groups (shift between entities) from the replacement of species by functionally similar ones (shift within entities: redundancy or compensation).
ent <- asc_entities(traits, k_max = 10, B = 50, seed = 1)
summary(ent)
#> ==================================================
#> ASC-CFD: Functional Entities Classification
#> ==================================================
#>
#> Total Species: 24 | Functional Entities: 6
#> Distance Metric: Gower | Clustering Method: ward.D2
#> Suggested k -> silhouette: 6 (max. mean silhouette 0.67) | Mojena: 4 | gap: 1
#> Silhouette guide: > 0.5 clear groups; 0.25-0.5 weak; < 0.25 no clear groups. Gap = 1: no natural groups.
#>
#> --- Species per Entity ---
#>
#> Entity_1 Entity_2 Entity_3 Entity_4 Entity_5 Entity_6
#> 3 10 5 3 2 1
#>
#> --- Classification Preview (Head) ---
#> Species Entity_ID Body_Mass Beak_Length Diet Forest_Dep
#> Bird_01 Entity_1 90.5 24.0 seeds yes
#> Bird_02 Entity_2 88.0 23.5 insects yes
#> Bird_03 Entity_3 64.4 15.7 fruit yes
#> Bird_04 Entity_3 15.4 7.3 fruit yes
#> Bird_05 Entity_2 65.0 13.2 insects yes
plot(ent)If k is not given, the number of entities with the
highest mean silhouette is used. Mean silhouette below 0.25 means no
clear groups; a gap statistic of 1 means the entities divide a continuum
(still useful, but check that conclusions hold for neighbouring values
of k).
Entities are used in the null models (A on counts summed by entity) and in the transitions (split between and within entities):
res_e <- asc_null(res, n_perm = 199, seed = 1, entities = ent)
res_e$null_models[res_e$null_models$Layer == "Position", c("Contrast", "Test", "SES", "P_upper")]
#> Contrast Test SES P_upper
#> 1 Logged A_samples 4.442944 0.005
#> 4 Logged B_stratified 2.271769 0.005
#> 7 Logged A_entities_samples 4.197292 0.005
#> 10 Control A_samples -1.014403 0.890
#> 13 Control B_stratified -1.081306 0.880
#> 16 Control A_entities_samples -1.078005 0.860
asc_transitions(res, entities = ent)$Logged$entities
#> Shift Between Within Within_fraction
#> 2.061566e-01 2.060903e-01 6.638813e-05 3.220276e-04Reading. If A is significant for species but not for
entities, and most of the shift is within entities, species
were replaced by functionally similar ones. Here 100 % of the shift at
Logged occurs between entities: the change
reorganised the functional groups.
asc_pairwise() describes all pairs of communities. With
many communities, one p-value per pair would multiply false alarms, so
asc_null() runs one global test per layer:
the mean distance of the community centroids to their common centroid,
and the standard deviation of FDis and FRic among communities. It also
reports how far each community lies from the common centroid.
pw <- asc_pairwise(traits, birds_sim$landscape)
pw <- asc_null(pw, n_perm = 199, seed = 1)
summary(pw)
#> ==================================================
#> ASC-CFD: Pairwise Spatial Functional Network
#> ==================================================
#>
#> Communities Analyzed: 5 | Spatial Contrasts: 10
#> Functional Dimensionality (k): 2 (Explaining 81.5% of variance)
#> PCoA Quality: 84.5%
#>
#> --- Global Divergence Summary ---
#> Mean Position Shift (rDelta_C): 12.31%
#> Mean Dispersion Shift (Delta_FDis): 0.0030
#> Mean Volume Shift (Delta_FRic): 0.0026
#>
#> --- Global Test Over All Communities (A: sampling | B: traits, stratified) ---
#> Contrast Test Layer Observed SES P P_lower
#> Global A_global Position 0.076980 7.93 0.005 1.000
#> Global A_global Dispersion 0.017350 1.13 0.125 0.880
#> Global A_global Volume 0.005934 -0.24 0.545 0.475
#> Global B_global_stratified Position 0.076980 0.71 0.250 0.755
#> Global B_global_stratified Dispersion 0.017350 -0.18 0.455 0.550
#> Global B_global_stratified Volume 0.005934 -0.62 0.660 0.350
#> Reading
#> more variation than sampling
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#> no evidence
#>
#> --- Distance of Each Community to the Common Centroid ---
#> Community Distance_to_common_centroid Fraction
#> Primary 0.1258 0.3267
#> Farmland 0.1081 0.2808
#> Old_secondary 0.0652 0.1693
#> Agroforest 0.0634 0.1647
#> Young_secondary 0.0225 0.0584
#>
#> P = upper tail. B lower tail (P_lower) <= 0.05: less variation among communities than
#> expected from the traits (compensation among communities).
#>
#> --- Top Functional Drivers (Leverage, midpoint reference - Head) ---
#> Primary_vs_Old_secondary: Bird_20 (+0.03), Bird_11 (+0.01), Bird_17 (+0.01), Bird_18 (+0.01)
#> Primary_vs_Young_secondary: Bird_20 (+0.06), Bird_17 (+0.04), Bird_11 (+0.02), Bird_18 (+0.01), Bird_01 (-0.01)
#> Primary_vs_Agroforest: Bird_20 (+0.06), Bird_11 (+0.03), Bird_17 (+0.02), Bird_04 (+0.02), Bird_09 (+0.01)
#> Primary_vs_Farmland: Bird_20 (+0.07), Bird_17 (+0.04), Bird_11 (+0.03), Bird_18 (+0.02), Bird_04 (+0.02)
#> Old_secondary_vs_Young_secondary: Bird_20 (+0.03), Bird_17 (+0.02), Bird_09 (+0.01), Bird_12 (-0.01), Bird_15 (+0.01)
#>
#> (first 5 of 10 pairs; see asc_transitions())
plot(pw, contrast = "Primary_vs_Farmland", type = "both", n_sp = 5)For the global test, P is the upper tail (more variation among communities than expected). For B, the lower tail means less variation than expected from the traits (compensation among communities).
asc_baseline() reports the absolute metrics (CWM, FDis,
FRic) of each community, without contrasts.
summary(asc_baseline(traits, birds_sim$landscape))
#> ==================================================
#> ASC-CFD: Baseline Functional Topology
#> ==================================================
#>
#> Entities Analyzed: 5
#> Functional Dimensionality (k): 2 (Explaining 81.5% of variance)
#> PCoA Quality: 84.5%
#>
#> --- Absolute Entity Metrics ---
#> Entity CWM_Axis_1 CWM_Axis_2 FDis FRic
#> Primary 0.0102 0.0007 0.2860 0.3435
#> Old_secondary 0.0139 0.0691 0.3038 0.3299
#> Young_secondary 0.0560 0.1394 0.3112 0.3429
#> Agroforest 0.1011 0.1636 0.3245 0.3429
#> Farmland 0.1094 0.2121 0.2832 0.3435Table 3. Functions of ascent.
| Objective | Function |
|---|---|
| Quality of the functional space | assess_functional_space() |
| Paired (temporal) contrasts | asc_paired() |
| All pairs of communities | asc_pairwise() |
| Null models A and B, global test | asc_null() |
| Species leverage and partition | asc_transitions() |
| Functional entities | asc_entities() |
| Absolute metrics per community | asc_baseline() |
stratum_sizeNull model B shuffles traits among groups of
stratum_size species of similar abundance (default 5). In
the validation, groups of 5 and of 10 species behaved almost
identically, whereas shuffling among all species of the contrast
(stratum_size = Inf) gave up to 12 % false positives for
FRic; it is not recommended. As a rule of thumb (not evaluated by
simulation), use 3 when samples have fewer than 10 species and 5 to 10
when they have more than 60.
p.adjust(p, method = "BH").FRic is the volume of the convex hull in the retained axes. It
depends only on extreme species and is not abundance-weighted: interpret
ΔFRic as a descriptor of the functional boundary, complementary to
ΔFDis. When a community has no more species than retained axes, FRic is
NA.
Only the positional shift (rΔC %) is normalised by the regional functional diameter and comparable across studies. ΔFDis and ΔFRic are absolute differences in the retained space.
\[\sum_{i=1}^{S} \text{Leverage}_i = \|\Delta C\|, \qquad \text{Leverage}_i = \Delta p_i \, (\mathbf{f}_i - \mathbf{r}) \cdot \hat{v}\]
where \(\Delta p_i\) is the change in relative abundance, \(\mathbf{f}_i\) the species’ coordinates, \(\hat{v}\) the unit vector of the shift and \(\mathbf{r}\) the reference point (by default the midpoint between the two centroids). The total does not depend on \(\mathbf{r}\); how it is shared among species does.