traitstrap-workflow

This vignette explains how to use the traitstrap package (Telford et al 2023). For more details on the methods see Maitner et al. (2021).

First of all, relax and turn on some music. We have prepared the traitstrap playlist for you!

The aim of traitstrap

Trait distributions can be used to infer the importance of community assembly processes and the role of climate drivers in shaping species and community responses to climate change. Community ecology has typically focused on the mean, however the higher moments (variance, skewness, and kurtosis) of trait distributions can reveal information about the various processes shaping species diversity.

Measuring trait distributions is often difficult and time-consuming as it requires information on measuring trait values of all individuals present. Sampling protocols often limit sampling to a non-representative subset of the community, or rely upon species-level average traits values calculated in other locations or across many locations.

Traditionally the moments of trait distributions have been estimated using weighting approaches that rely on the average traits of species weighted by some measure of abundance within the community. Such community-weighted trait moments methods assume that a species’ trait expression can be adequately represented by the mean, ignoring intraspecific trait variation.

To more accurately estimate local trait distributions, trait sampling should thus occur both across multiple individuals within each species, and across multiple locations or experimental treatments across the extent of the study in order to capture both inter- and intra-specific variability.

Traitstrap is an R package to estimate the moments of community trait distributions using a bootstrapping approach. Further, this package uses a hierarchical sampling design, which allows accounting for incomplete trait collections, traits from different spatial or temporal levels (e.g. local traits vs. databases), taxonomic hierarchies (e.g., species vs genus) and experimental designs (e.g., multiple sites, or treated vs. control sampling units).

The package has three main functions:

Note that for this tutorial we are calling the mean and the higher moments the happy moments :-)

The data

For this vignette we will use part of a vascular plant dataset from two sites near Longyearbyen on Svalbard. The data was collected during the Plant Functional Trait Course in 2018 and contains data on the plant community composition and functional traits. For more details see this GitHub repo

Note that some of the species names have been adapted.

Organize your data

To run traitstrap two datasets are required:

The datasets need to be organized in a tidy and long format and certain columns (see below) are required, but the naming of these columns are up to the user.

Let us have a look at these datasets in an example.

The community data should have information the abundance of species in the community. This dataset will be used to weigh the traits by species abundance. Note that abundance can also be cover, size, biomass, or something similar.

In this example the contains species names (e.g. Taxon), cover of each species per plot (e.g. Cover) and two columns with information about the hierarchy (i.e. Site and PlotID).

#> # A tibble: 110 × 4
#>    Taxon             Cover Site  PlotID
#>    <chr>             <dbl> <chr> <chr> 
#>  1 alopecurus ovatus   0.5 1     B     
#>  2 alopecurus ovatus   1   1     C     
#>  3 alopecurus ovatus   1   1     D     
#>  4 alopecurus ovatus   2   1     F     
#>  5 alopecurus ovatus   0.1 1     G     
#>  6 bistorta vigdis    10   1     A     
#>  7 bistorta vigdis    25   1     B     
#>  8 bistorta vigdis    10   1     C     
#>  9 bistorta vigdis     2   1     D     
#> 10 bistorta vigdis     1   1     F     
#> # ℹ 100 more rows

The trait data should contain information about traits and trait values for as many species and individuals in the community data as possible. The data should be organized in the same way as the community data and should have corresponding columns. In this example the trait data contains Taxon, Site and PlotID as well as Trait and Value.

#> # A tibble: 705 × 6
#>    Site  PlotID Taxon                   ID      Trait              Value
#>    <chr> <chr>  <chr>                   <chr>   <chr>              <dbl>
#>  1 1     A      saxifraga oppositifolia ANF4320 Plant_Height_cm 2       
#>  2 1     A      saxifraga oppositifolia ANF4320 Wet_Mass_g      0.000695
#>  3 1     A      saxifraga oppositifolia ANF4320 Thickness_mm    0.127   
#>  4 1     C      bistorta vigdis         ARQ4344 Plant_Height_cm 4.2     
#>  5 1     C      bistorta vigdis         ARQ4344 Wet_Mass_g      0.0105  
#>  6 1     C      bistorta vigdis         ARQ4344 Thickness_mm    0.154   
#>  7 2     C      festuca rubra           AMT1545 Plant_Height_cm 7       
#>  8 2     C      festuca rubra           AMT1545 Wet_Mass_g      0.00724 
#>  9 2     C      festuca rubra           AMT1545 Thickness_mm    0.257   
#> 10 1     C      bistorta vigdis         ARP2422 Plant_Height_cm 1       
#> # ℹ 695 more rows

Trait filling

The trait_fill() function uses a hierarchical sampling design, which allows to account for incomplete trait collections, traits from different spatial or temporal levels (i.e. local traits vs. databases), different taxonomic resolution and/or experimental design.

The first two mandatory arguments in the function are the two datasets: comm and traits

The next four arguments are also mandatory and refer to specific columns in the trait or community dataset:

All the other arguments are not mandatory.

With scale_hierarchy you can define the levels at which the traits have been collected and the order of trait filling starting with the highest level (e.g. global database, regional, site, plot). In the example below we have the levels Site and PlotID, starting with the highest level.

The trait_fill() function will choose if available a trait value from the lowest level, i.e. species X from plot A in site 1. If no trait value is available from that level (plot A, site 1), it will other groups in the same level and choose a trait value from species X from plot B or C at site 1. If there is no trait available, it will move up the hierarchy to the next level and choose trait values from species X from other sites (site 2, 3, etc.).

The argument min_n_in_samples allows users to define the minimum number in samples that are chosen at each level. If the minimum number is not reached (i.e. there are only 3 trait values at a specific level), trait values from the next higher level will be filled, to avoid sampling the same individual several times, which could result in unrealistic variances. The default minimum number of samples is 5.

In the other_col argument other grouping variables in the community dataset can be defined and will be kept after the trait filling.

trait_filling <- trait_fill(
  # input data (mandatory)
  comm = community,
  traits = trait,

  # specifies columns in your data (mandatory)
  abundance_col = "Cover",
  taxon_col = "Taxon",
  trait_col = "Trait",
  value_col = "Value",

  # specifies sampling hierarchy
  scale_hierarchy = c("Site", "PlotID"),

  # min number of samples
  min_n_in_sample = 9
)

trait_filling
#> # A tibble: 4,011 × 13
#> # Groups:   global, Site, PlotID, Trait [42]
#>    Taxon   Cover Site  PlotID global sum_abun ID    Trait Value n_sample  weight
#>    <chr>   <dbl> <chr> <chr>  <chr>     <dbl> <chr> <chr> <dbl>    <int>   <dbl>
#>  1 dryas …   0.1 1     A      global     38.2 ATC4… Plan…   1.3       17 0.00588
#>  2 dryas …   0.1 1     A      global     38.2 ADC9… Plan…   1.4       17 0.00588
#>  3 dryas …   0.1 1     A      global     38.2 AVU2… Plan…   1.5       17 0.00588
#>  4 dryas …   0.1 1     A      global     38.2 AHU1… Plan…   2         17 0.00588
#>  5 dryas …   0.1 1     A      global     38.2 AHS4… Plan…   2.5       17 0.00588
#>  6 dryas …   0.1 1     A      global     38.2 AHR0… Plan…   2.4       17 0.00588
#>  7 dryas …   0.1 1     A      global     38.2 ABE6… Plan…   0.6       17 0.00588
#>  8 dryas …   0.1 1     A      global     38.2 BUP1… Plan…   2.2       17 0.00588
#>  9 dryas …   0.1 1     A      global     38.2 ADE9… Plan…   1         17 0.00588
#> 10 dryas …   0.1 1     A      global     38.2 ACH6… Plan…   3         17 0.00588
#> # ℹ 4,001 more rows
#> # ℹ 2 more variables: level <ord>, max_n_in_sample <int>

Traitstrap also allows to include taxonomy and experimental design in the trait filling step.

With the argument taxon_col the taxonomic hierarchy for sampling can be defined. This means if traits for a specific species are not available, trait values from the same genus will be used. For this a list of the taxonomic hierarchy has to be defined (e.g. “Taxon”, “Genus”). Note that traits from species of the same genus can have very different traits and it might not be meaningful to use these traits. Therefore, you should always check the trait distributions for the same genus before using taxonomic trait filling.

The argument treatment_col allows to incorporate an experimental design where traits are filled from the same experimental treatment or the first factor level, which is assumed to be the control. Therefore, it is important to order the levels of a treatment in the right order, i.e. the first level has to be the control. The filling step can be defined at certain level using the treatment_level argument. Depending on the experimental design it might make sense to fill traits at a certain level, e.g. block or site.

Here is an example how to include taxonomy and experimental design in the trait filling function (code not run).

trait_filling2 <- trait_fill(
  comm = community,
  traits = trait,
  abundance_col = "Cover",

  # defining taxonomic hierarchy
  taxon_col = c("Taxon", "Genus"),
  trait_col = "Trait",
  value_col = "Value",
  scale_hierarchy = c("Site", "PlotID"),
  min_n_in_sample = 3,

  # specifying experimental design
  treatment_col = "Treatment",
  treatment_level = "Site",
)

Nonparametric bootstrapping

The output of the trait filling function is then used to do a nonparametric bootstrapping using the trait_np_bootstrap() function.

Nonparametric bootstrapping is a resampling method to estimate the trait moments. The traits are re-sampled in proportion to their weight in the community (e.g. by the abundance of the species).

The trait values across all individuals in a community are resampled n times (sample_size; the default is 200) to incorporate the full spectrum of trait variation, generating n number (nrep; the default is 100) of trait distributions.

From these trait distributions the happy moments are estimated: mean, variance, skewness and kurtosis.

This function also allows to extract raw distributions by setting the argument raw = TRUE. The raw data can be useful for visualizing the trait distributions. If the raw data is extracted, nrep is forced to 1 to avoid memory issues.

# run nonparametric bootstrapping
np_bootstrapped_moments <- trait_np_bootstrap(
  trait_filling, 
  nrep = 50
)

np_bootstrapped_moments
#> # A tibble: 2,100 × 8
#> # Groups:   global, Site, PlotID [14]
#>    global Site  PlotID Trait             mean  variance skewness kurtosis
#>    <chr>  <chr> <chr>  <chr>            <dbl>     <dbl>    <dbl>    <dbl>
#>  1 global 1     A      Plant_Height_cm 1.62   2.50         3.25    12.6  
#>  2 global 1     A      Thickness_mm    0.212  0.00351      1.27     2.26 
#>  3 global 1     A      Wet_Mass_g      0.0116 0.0000516    0.524   -0.944
#>  4 global 1     B      Plant_Height_cm 1.53   2.66         3.32    12.9  
#>  5 global 1     B      Thickness_mm    0.203  0.00171      0.139   -0.855
#>  6 global 1     B      Wet_Mass_g      0.0117 0.0000420    0.567   -0.941
#>  7 global 1     C      Plant_Height_cm 1.71   3.84         2.70     7.22 
#>  8 global 1     C      Thickness_mm    0.202  0.00277      2.04     9.08 
#>  9 global 1     C      Wet_Mass_g      0.0130 0.000213     5.50    41.9  
#> 10 global 1     D      Plant_Height_cm 2.04   7.97         2.42     5.19 
#> # ℹ 2,090 more rows

One advantage of using a bootstrapping approach, is that we get much more than a mean trait value. We can also estimate the variance and other moments of these trait distributions. In traitstrap happy moments can be summarized and the confidence intervals calculated using the trait_summarise_boot_moments() function. The input variable for this function is the output from the nonparametric bootstrapping function (or the parametric bootstrapping function, see below).

The confidence interval can be calculated parametrically, using the mean and standard deviation, or nonparametrically using quantiles. The default is using the mean and standard deviation (parametric = TRUE) with one standard deviation around each trait moment (sd_mult = 1). For the nonparametric approach the default is a 0.95 confidence level.

# summarizes bootstrapping output
sum_boot_moment <- trait_summarise_boot_moments(
  np_bootstrapped_moments
)

sum_boot_moment
#> # A tibble: 42 × 17
#> # Groups:   global, Site, PlotID [14]
#>    global Site  PlotID Trait           n   mean ci_low_mean ci_high_mean     var
#>    <chr>  <chr> <chr>  <chr>       <int>  <dbl>       <dbl>        <dbl>   <dbl>
#>  1 global 1     A      Plant_Heig…    50 1.61        1.49         1.73   3.16e+0
#>  2 global 1     A      Thickness_…    50 0.214       0.210        0.218  3.48e-3
#>  3 global 1     A      Wet_Mass_g     50 0.0109      0.0104       0.0114 4.81e-5
#>  4 global 1     B      Plant_Heig…    50 1.62        1.50         1.75   3.08e+0
#>  5 global 1     B      Thickness_…    50 0.203       0.200        0.206  1.70e-3
#>  6 global 1     B      Wet_Mass_g     50 0.0122      0.0117       0.0126 4.68e-5
#>  7 global 1     C      Plant_Heig…    50 1.70        1.57         1.82   3.97e+0
#>  8 global 1     C      Thickness_…    50 0.205       0.201        0.208  2.76e-3
#>  9 global 1     C      Wet_Mass_g     50 0.0118      0.0110       0.0126 1.24e-4
#> 10 global 1     D      Plant_Heig…    50 1.82        1.61         2.03   5.93e+0
#> # ℹ 32 more rows
#> # ℹ 8 more variables: ci_low_var <dbl>, ci_high_var <dbl>, skew <dbl>,
#> #   ci_low_skew <dbl>, ci_high_skew <dbl>, kurt <dbl>, ci_low_kurt <dbl>,
#> #   ci_high_kurt <dbl>

Parametric bootstrapping

Traitstrap also offers the option to run a parametric bootstrapping.

The trait_fit_distributions() function fits parametric distributions for each species-by-trait combination at the finest scale of the user-supplied hierarchy. This function takes as input:

Either a single distribution type can be used for all traits, or traits can be assigned specific distributions types by supplying the function with a named vector of traits (e.g. c(height = "normal", mass = "lognormal")).

Currently the function supports normal, log-normal, and beta (values between 0 and 1) distributions.

The function returns a dataframe containing fitted distribution parameters.

# fit distributions
fitted_distributions <- trait_fit_distributions(
  filled_traits = trait_filling, 
  distribution_type = "lognormal"
)

fitted_distributions
#> # A tibble: 303 × 15
#> # Groups:   global, Site, PlotID, Trait, Taxon, Cover, n_sample [303]
#>    global Site  PlotID Trait Taxon Cover n_sample distribution_type  parm1 parm2
#>    <chr>  <chr> <chr>  <chr> <chr> <dbl>    <int> <chr>              <dbl> <dbl>
#>  1 global 1     A      Plan… bist…  10         17 lognormal          0.156 0.587
#>  2 global 1     A      Plan… drya…   0.1       17 lognormal          0.434 0.810
#>  3 global 1     A      Plan… fest…   1         11 lognormal          1.61  0.377
#>  4 global 1     A      Plan… luzu…   0.5       10 lognormal          1.45  0.287
#>  5 global 1     A      Plan… luzu…   1         14 lognormal          0.405 0.501
#>  6 global 1     A      Plan… sali…  20         22 lognormal         -0.320 1.08 
#>  7 global 1     A      Plan… saxi…   2          6 lognormal          1.13  0.469
#>  8 global 1     A      Plan… saxi…   2          2 lognormal          0.693 0    
#>  9 global 1     A      Plan… sile…   1          3 lognormal         -0.221 0.277
#> 10 global 1     A      Thic… bist…  10         17 lognormal         -1.55  0.194
#> # ℹ 293 more rows
#> # ℹ 5 more variables: sd1 <lgl>, sd2 <lgl>, ks <dbl>, cvm <dbl>, ad <dbl>
# fit several types of distributions
fitted_distributions <- trait_fit_distributions(
  filled_traits = trait_filling,
  distribution_type = c(Plant_Height_cm = "normal", 
                           Wet_Mass_g = "lognormal")
)

fitted_distributions

The trait_parametric_bootstrap() function is a parametric analogue of the trait_np_bootstrap() function. It takes in fitted trait distributions produced by trait_fit_distributions and randomly samples from among the fitted distributions proportionally to species abundances in the community.

As with trait_np_bootstrap(), the number of samples per replicated draw are specified with the parameter sample_size, and the number of replicated draws is specified by the parameter nrep. The argument raw allows to extract raw distributions (see above).

# run parametric bootstrapping
p_bootstrapped_moments <- trait_parametric_bootstrap(
  fitted_distributions = fitted_distributions,
  nrep = 50
)

p_bootstrapped_moments
#> # A tibble: 2,100 × 8
#> # Groups:   global, Site, PlotID, Trait [42]
#>    global Site  PlotID Trait             mean  variance skewness kurtosis
#>    <chr>  <chr> <chr>  <chr>            <dbl>     <dbl>    <dbl>    <dbl>
#>  1 global 1     A      Plant_Height_cm 1.57   2.24         1.96    4.19  
#>  2 global 1     A      Thickness_mm    0.211  0.00316      1.29    3.34  
#>  3 global 1     A      Wet_Mass_g      0.0105 0.0000416    0.802   0.0631
#>  4 global 1     B      Plant_Height_cm 1.70   2.58         1.47    1.41  
#>  5 global 1     B      Thickness_mm    0.204  0.00159      0.303  -0.432 
#>  6 global 1     B      Wet_Mass_g      0.0130 0.0000454    0.649   0.228 
#>  7 global 1     C      Plant_Height_cm 1.47   1.99         1.84    3.20  
#>  8 global 1     C      Thickness_mm    0.208  0.00305      2.89   14.9   
#>  9 global 1     C      Wet_Mass_g      0.0112 0.0000537    2.38    9.65  
#> 10 global 1     D      Plant_Height_cm 1.61   3.72         2.15    4.57  
#> # ℹ 2,090 more rows

The output of trait_parametric_bootstrap can be summarized using trait_summarize_boot_moments() (see above).

Extracting raw distributions

In traitstrap both the parametric and nonparametric bootstrapping functions allow returning raw trait distributions.

# run nonparametric bootstrapping
raw_dist_np <- trait_np_bootstrap(
  filled_traits = trait_filling,
  raw = TRUE
)

raw_dist_np
#> # A tibble: 8,400 × 13
#> # Groups:   global, Site, PlotID, Trait [42]
#>    Taxon    Cover Site  PlotID global sum_abun ID    Trait Value n_sample weight
#>    <chr>    <dbl> <chr> <chr>  <chr>     <dbl> <chr> <chr> <dbl>    <int>  <dbl>
#>  1 salix p…    20 1     A      global     38.2 AXW1… Plan…   0.6       22  0.909
#>  2 salix p…    20 1     A      global     38.2 AMQ6… Plan…   0.7       22  0.909
#>  3 bistort…    10 1     A      global     38.2 AUA5… Plan…   0.8       17  0.588
#>  4 salix p…    20 1     A      global     38.2 AQT8… Plan…   0.3       22  0.909
#>  5 salix p…    20 1     A      global     38.2 ASL0… Plan…   1.6       22  0.909
#>  6 salix p…    20 1     A      global     38.2 ATU7… Plan…   0.9       22  0.909
#>  7 bistort…    10 1     A      global     38.2 BRP6… Plan…   0.7       17  0.588
#>  8 bistort…    10 1     A      global     38.2 ASE0… Plan…   2.3       17  0.588
#>  9 salix p…    20 1     A      global     38.2 AQT8… Plan…   0.3       22  0.909
#> 10 silene …     1 1     A      global     38.2 AUE6… Plan…   0.5        3  0.333
#> # ℹ 8,390 more rows
#> # ℹ 2 more variables: level <ord>, max_n_in_sample <int>

The raw data can be useful for visualizing the trait distributions.

Use colour and facets to separate between the different traits, hierarchies and treatments.

ggplot(raw_dist_np, aes(x = log(Value), fill = Site)) +
  geom_density(alpha = 0.4) +
  scale_fill_manual(values = col_palettes$GrandBudapest1) +
  labs(x = "log(trait value)") +
  facet_wrap(facets = vars(Trait), scales = "free")

Multivariate analyses

While the core functionality of traitstrap was designed with univariate distributions in mind, it can also be used for multivariate questions as well using the function trait_multivariate_bootstrap(). This function allows users to supply any appropriate function for analyses of multivariate data, including those included in popular packages for functional diversity calculations. Here, we demonstrate how to combine trait_multivariate_bootstrap() with the function dbFD() from the FD package.

We first use the trait_fill() function similar to the univariate case. Note that we are also using the argument complete_only to specify that we are only interested in individuals that have all traits and the argument leaf_id, which tells the function which column contains the unique individual ID. Also, we are subsetting the data to make it run faster.

library(FD)
#> Loading required package: ade4
#> Loading required package: ape
#> 
#> Attaching package: 'ape'
#> The following object is masked from 'package:dplyr':
#> 
#>     where
#> Loading required package: geometry
#> Loading required package: vegan
#> Loading required package: permute
#> Loading required package: lattice
#> This is vegan 2.6-4

multivariate_traits <- trait_fill(
  comm = community |>
    # to make the example faster, we'll only use a subset of plots
    filter(
      Site == "1",
      PlotID %in% c("A", "B")
    ),
  traits = trait,
  scale_hierarchy = c("Site", "PlotID"),
  taxon_col = "Taxon",
  value_col = "Value",
  trait_col = "Trait",
  abundance_col = "Cover",
  complete_only = TRUE,
  leaf_id = "ID"
)

Then we are using the trait_multivariate_bootstrap() function to do the actual bootstrapping. This function needs the output from the trait filling as filled_traits, number bootstrapped of replications nrep, bootstrap size sample_size and id. The last argument is fun, that takes any function to apply to the data. In this case we are using diversity functions from the dbFD package.

Note that we are using a low number of replicates (nrep) to make it run faster.

The dbFD() function returns a list as output, and we need to wrangle the data a bit to get it into a more useful format. We will extract Rao’s Quadratic Entropy, since that performs well with bootstrapping.

boot_multi <- trait_multivariate_bootstrap(
  filled_traits = multivariate_traits,
  nrep = 5, # number of reps is set low for demo purposes
  sample_size = 200,
  id = "ID",
  fun = function(x) {
    dbFD(
      x = x,
      calc.FRic = FALSE,
      calc.FDiv = FALSE,
      calc.CWM = FALSE,
      stand.x = FALSE,
      scale.RaoQ = FALSE
    )
  }
)

# data wrangling
raoq_est <- boot_multi |>
  mutate(result = map(result, as.data.frame)) |>
  unnest(result)

Let’s plot RaoQ:

ggplot(data = raoq_est, mapping = aes(x = RaoQ, fill = PlotID)) +
  geom_density(alpha = 0.5) +
  scale_fill_manual(values = col_palettes$GrandBudapest1) +
  xlim(c(0, 6))

Next, we’ll show how we can use a similar method to combine trait_multivariate_bootstrap with the functions TPDs() and REND() from the TPD package.

For each replicated site, we convert the distribution into a trait probability distribution and then derive functional diversity metrics.

library(TPD)

boot_tpd <- trait_multivariate_bootstrap(multivariate_traits,
  id = "ID",
  nrep = 5, # Note that the number of reps is set low for demo purposes
  fun = function(x) {
    TPDs(
      species = rep(1, 200),
      traits = x
    ) |>
      REND(TPDs = _)
  }
)

# wrangling data
tpd <- boot_tpd |>
  mutate(result = map(result, as.data.frame)) |>
  unnest(result) |>
  pivot_longer(
    cols = species.FRichness:species.FDivergence,
    values_to = "value",
    names_to = "metric"
  ) |>
  mutate(metric = str_remove(metric, "species\\."))


# and make plot
ggplot(data = tpd) +
  geom_violin(
    mapping = aes(y = value, x = PlotID, fill = PlotID),
    alpha = 0.5,
    draw_quantiles = c(0.025, 0.975)
  ) +
  scale_fill_manual(values = col_palettes$GrandBudapest1) +
  facet_wrap(facets = vars(metric), scales = "free")

Check your data

Traitstrap has a couple of functions to check the data.

The coverage_plot() function shows the trait coverage of the community for each level. Basically, this function summarizes from which level the traits are filled, and how much coverage of the community is reached.

Based on simulations, we recommend to collect traits for at least 80% of the community cover (Maitner et al. in prep).

# show coverage plot
autoplot(trait_filling) +
  scale_fill_manual(values = col_palettes$GrandBudapest1) +
  theme(axis.text.x = element_text(size = 8, angle = 90, vjust = 0.5))

Another important information is to know of which taxa traits are missing. This can be useful if the data sampling is not finished and you want to know which species should be sampled. The function also tells you the maximal abundance of each missing species, and gives you useful information if the missing species are abundant or rare.

Traitstrap has a function trait_missing() which gives you a table with all missing values.

# list missing traits
trait_missing(
  filled_trait = trait_filling,
  comm = community
)
#> # A tibble: 20 × 4
#> # Groups:   Taxon, max_abun [20]
#>    Taxon                      max_abun     n n_traits
#>    <chr>                         <dbl> <int>    <int>
#>  1 alopecurus ovatus               2       5        3
#>  2 bistorta vigdis                25      14        3
#>  3 calamagrostis neglecta         60       4        3
#>  4 cassiope tetragona              5       3        3
#>  5 dryas octopetala               20       9        3
#>  6 enquistetum scirpoides          2       6        3
#>  7 festuca rubra                   1       7        3
#>  8 juncus biglumis                 0.5     2        0
#>  9 luzula confusa                  5      10        3
#> 10 luzula nivalis                  5       8        3
#> 11 maitneranthes hieracifolia      0.5     2        0
#> 12 oxyria tanyna                   2       5        3
#> 13 poa pratensis                   1       6        3
#> 14 salix polaris                  43      14        3
#> 15 saxifraga hirculus              2       3        3
#> 16 saxifraga oppositifolia         2       1        3
#> 17 silene acaudis                  1       1        3
#> 18 stelfordaria humifusa           0.5     1        0
#> 19 stellaria longipes              0.1     4        0
#> 20 trisetum spicatum               3       5        3