ascent: decomposing functional community restructuring

Rogelio R. Muñoz-Li & Flavia Alvarez-Denis

2026-10-17

Part I - Conceptual Framework

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 simulated data

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      5

Abundances 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.


Part II - Quality of the functional space

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.


Part III - Paired contrasts

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.


Part IV - Which species moved the community?

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.


Part V - Functional entities

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-04

Reading. 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.


Part VI - Many communities: a global test

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).


Part VII - Baseline topology

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.3435

Table 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()

Part VIII - Methodological notes

Choosing stratum_size

Null 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.

Known limits of the null models

Interpreting FRic

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.

Normalisation

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.

Leverage: additive decomposition

\[\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.