prepare_selection_data()detect_family()selection_differential()analyze_linear_selection()analyze_nonlinear_selection()analyze_disruptive_selection()univariate_spline()plot_univariate_fitness()correlated_fitness_surface()plot_correlated_fitness()adaptive_landscape()plot_adaptive_landscape()selection_coefficients()selection_report()compare_fitness_surfaces_data()temporal_landscape() and
plot_temporal_landscape()canonical_analysis() and
plot_canonical_axes()This package measures natural selection on phenotypic traits the way
Lande & Arnold (1983) set out, and adds spline fitness functions,
fitness surfaces and adaptive landscapes on top of the same prepared
data. Svensson (2023) reviews the approach and the arguments about it;
Palacio et al. (2019) set out what an analysis should report, and
check_selection_assumptions() runs the checks they ask for
on the data and the models.
Key capabilities:
| Analysis Type | Description |
|---|---|
| Selection differentials (\(\mathbf{S}\)) | Total selection acting on traits |
| Linear selection gradients (\(\boldsymbol{\beta}\)) | Direct directional selection |
| Nonlinear selection gradients (\(\boldsymbol{\gamma}\)) | Stabilising/disruptive and correlational selection |
| Univariate correlated fitness functions | Flexible spline-based fitness functions at the individual level |
| Bivariate correlated fitness surfaces | Thin-plate spline (TPS) or GAM surfaces of individual fitness as a function of two traits |
| Adaptive landscapes | Mean fitness as a function of population mean phenotype (requires simulation) |
| Landscapes over time | One fitness function or surface per period, on trait axes shared by all periods |
| Canonical analysis | \(\boldsymbol{\gamma}\) rotated to its canonical axes, and the curvature along each axis tested |
| Peak comparison | The difference in fitted fitness between two peaks, or between a peak and the valley between them, with its standard error |
| Uncertainty | Bootstrap intervals for the gradients; standard error lines or bands on GAM surfaces |
| Binary and count fitness | Logistic regression for survival; Poisson or negative binomial regression for offspring counts (p-values only; the gradients stay OLS) |
Individual fitness is converted to relative fitness to enable comparisons across populations and generations:
\[w_i = \frac{W_i}{\bar{W}}\]
where \(W_i\) is the absolute fitness of individual \(i\) (an offspring count, or survival coded 0/1), \(\bar{W} = \frac{1}{n}\sum_{i=1}^n W_i\) is the mean fitness of the population, and \(w_i\) is the relative fitness of individual \(i\).
Fitness is made relative because selection acts on differences in
fitness relative to the mean, and dividing by mean fitness puts
gradients from different studies on one scale. Brodie and Janzen (1996)
showed that regressing absolute survival puts the gradients on the wrong
scale, which is why the package always relativises fitness before
fitting. File:
prepare_selection_data.R
Traits are standardised to mean 0 and variance 1 before analysis:
\[z_i = \frac{x_i - \bar{x}}{s_x}\]
where \(x_i\) is the original trait value, \(\bar{x}\) and \(s_x\) are the sample mean and standard deviation, and \(z_i\) is the standardised value.
Purpose: Standardisation puts all traits on the same scale, allowing direct comparison of selection gradients. A \(\beta = 0.5\) means that a one-standard-deviation increase in the trait increases relative fitness by 0.5 units.
Scale traits and relative fitness within the population and time
period in question. With data from several years or sites, pass the
column to group and both steps are done separately within
each level before the data are stacked. For the linear gradients scaling
within groups is the same as fitting a fixed effect for the group, and
for the quadratic ones close to it, so the pooled gradients gain power
from the extra individuals without mixing the groups’ scales, but the
pooled model cannot test whether selection differs between groups (a
year by trait interaction); fit the groups separately with
return_grouped = TRUE for that.
The gradients are the raw partial regression coefficients from models
fitted to these standardised traits, so don’t wrap the traits in
scale() in a model formula or use a modelling option that
standardises predictors; either would scale the coefficients a second
time. File: prepare_selection_data.R
The selection differential (\(S\)) measures the total selection acting on a trait, including both direct and indirect effects through correlated traits:
\[S = \text{Cov}(z, w) = \mathbb{E}[(z - \bar{z})(w - \bar{w})]\]
For multiple traits, the vector of selection differentials is:
\[\mathbf{S} = \text{Cov}(\mathbf{z}, w) = \begin{bmatrix} \text{Cov}(z_1, w) \\ \text{Cov}(z_2, w) \\ \vdots \\ \text{Cov}(z_m, w) \end{bmatrix}\]
It is the change in the trait mean within a generation (Arnold & Wade, 1984).
Interpretation: \(S\) represents the shift in trait mean
after one generation of selection. A positive \(S\) indicates the trait mean increases.
File: selection_differential.R
Linear selection gradients (\(\boldsymbol{\beta}\)) measure the direct directional selection on each trait, controlling for correlations with other traits:
\[w = \alpha + \boldsymbol{\beta}^T \mathbf{z} + \varepsilon\]
In matrix form:
\[\boldsymbol{\beta} = \mathbf{P}^{-1} \mathbf{S}\]
where \(\mathbf{P}\) is the phenotypic variance-covariance matrix.
For a model with two traits:
\[w = \alpha + \beta_1 z_1 + \beta_2 z_2 + \varepsilon\]
Interpretation: \(\beta_i\) is the partial regression coefficient: the change in relative fitness for a one-standard-deviation increase in trait \(i\), holding all other traits constant.
Tests. The gradients always come from this
least-squares fit on relative fitness. Its t-tests are only valid when
the residuals are roughly normal, which holds for continuous fitness but
not for survival or offspring counts. For binary fitness the p-values
therefore come from a logistic regression of the raw 0/1 outcome on the
same terms, and for counts from a Poisson regression of the raw counts,
replaced by a negative binomial one when the counts are overdispersed.
File: analyze_linear_selection.R
Quadratic selection gradients (\(\boldsymbol{\gamma}\)) measure nonlinear selection, including stabilising/disruptive selection (diagonal elements) and correlational selection (off-diagonal elements):
\[w = \alpha + \boldsymbol{\beta}^T \mathbf{z} + \frac{1}{2} \mathbf{z}^T \boldsymbol{\gamma} \mathbf{z} + \varepsilon\]
For two traits, this expands to:
\[w = \alpha + \beta_1 z_1 + \beta_2 z_2 + \frac{1}{2}\gamma_{11} z_1^2 + \frac{1}{2}\gamma_{22} z_2^2 + \gamma_{12} z_1 z_2 + \varepsilon\]
Important: The \(\frac{1}{2}\) factor in the quadratic terms follows Lande & Arnold’s convention, making \(\gamma_{ii}\) the second derivative of the fitness surface. In practice, most implementations (including this one) report \(2 \times\) the quadratic coefficient from regression, which equals \(\gamma_{ii}\).
Interpretation:
| \(\gamma\) value | Type | Interpretation |
|---|---|---|
| \(\gamma_{ii} < 0\) | Stabilising selection | Intermediate trait values have highest fitness |
| \(\gamma_{ii} > 0\) | Disruptive selection | Extreme trait values have highest fitness |
| \(\gamma_{ij} > 0\) | Positive correlational | Selection favours positive correlation between traits |
| \(\gamma_{ij} < 0\) | Negative correlational | Selection favours negative correlation between traits |
Canonical axes: with several traits the curvature is
spread over the \(\gamma_{ii}\) and the
\(\gamma_{ij}\) together. Canonical
analysis (Phillips & Arnold, 1989; Blows & Brooks, 2003) rotates
\(\boldsymbol{\gamma}\) to its
eigenvectors, combinations of the traits that each have a single
curvature, the eigenvalue \(\lambda\).
See canonical_analysis() in section 5.17.
File: analyze_nonlinear_selection.R,
analyze_disruptive_selection.R
Quadratic regression fixes the shape of the fitness function in advance, while a spline takes its shape from the data and can show, for example, two peaks that a quadratic would average away. These are still individual-level fitness functions; the population-level adaptive landscape is a separate construction (section 2.7). Cubic splines for one trait and thin-plate splines for two follow Schluter (1988) and Schluter and Nychka (1994); Brodie et al. (1995) is the guide to reading the surfaces.
Univariate spline model:
\[w = \alpha + s(z) + \varepsilon\]
where \(s(z)\) is a smooth function estimated from the data using penalised regression splines.
Multivariate thin-plate spline:
\[w = \alpha + f(z_1, z_2) + \varepsilon\]
where \(f(z_1, z_2)\) is a two-dimensional smooth surface showing how individual fitness varies across trait combinations. The univariate spline chooses its smoothing parameter by generalised cross-validation (Schluter 1988), which for survival and Poisson counts mgcv applies in its UBRE form; the surface uses REML. The surface is drawn only where there are data: grid points outside the convex hull of the observed trait pairs are blank, and a distance rule can also blank cells far from any individual, as Beausoleil et al. (2023) did for a community of Darwin’s finches. With a grouping column such as species, each group’s mean and the highest point of the surface within that group’s own range are marked on the plot.
GAM formulation: \[ w \sim \text{family}(\eta), \quad \eta = \alpha + s(z_1, z_2) \]
File: correlated_fitness_surface.R
Following Sewall Wright’s original concept, the adaptive landscape describes mean fitness as a function of population mean phenotype:
\[ \bar{W} = g(\bar{z}_1, \bar{z}_2) \]
where:
A fitness surface is fitted to individuals. The adaptive landscape is the mean fitness a population would have at a given mean phenotype, so it is computed by simulating such populations.
Calculation procedure:
For each candidate population mean \(\bar{\mathbf{z}}\):
1. Simulate individuals around the mean
\[ \mathbf{z}_{\text{ind}} \sim \mathrm{N}(\bar{\mathbf{z}}, \mathbf{P}) \]
where \(\mathbf{P}\) is the within-population phenotypic variance–covariance matrix.
2. Predict individual fitness
\[ w_{\text{ind}} = \hat{f}(\mathbf{z}_{\text{ind}}) \]
3. Compute mean fitness
\[ \bar{W}(\bar{\mathbf{z}}) = \frac{1}{N_{\text{sim}}} \sum_{j=1}^{N_{\text{sim}}} w_{\text{ind}, j} \]
The resulting surface:
\[ \bar{W} = g(\bar{z}_1, \bar{z}_2) \]
is the adaptive landscape.
Mathematical distinction
| Surface | Level | Formula | Interpretation |
|---|---|---|---|
| Correlated Fitness | Individual | \(w = f(\mathbf{z})\) | Selection acting on individuals now |
| Adaptive Landscape | Population | \(\bar{W} = g(\bar{\mathbf{z}})\) | Where selection would move the population mean |
File: adaptive_landscape.R
The two surfaces are compared with
compare_fitness_surfaces_data() (section 5.15). The same
construction works for a single trait, where the landscape is a curve of
mean fitness against the population mean, drawn alongside the fitness
function it was averaged from.
Beausoleil et al. (2019) fitted a separate fitness function to each
year of the finch data and found disruptive selection throughout,
stronger in some years than others and significant in three of the six
year-to-year intervals they tested. The package does the same with
temporal_landscape(): the fitness function (one trait) or
surface (two traits) and, if asked, the adaptive landscape are fitted
separately for each level of a time column, on traits standardised once
over all periods so that the periods share one axis. The result is one
row per period with the sample size, mean fitness, the degrees of
freedom of the smooth, the position of the highest fitness and, for one
trait, the number of interior peaks.
plot_temporal_landscape() draws the periods side by side
or, for one trait, as a heat map of fitness against trait and time.
File: temporal_landscape.R
| Script | Purpose | Key Functions | Mathematical Component |
|---|---|---|---|
prepare_selection_data.R |
Data preprocessing | Standardisation, relative fitness, NA handling | \(z_i = \frac{x_i - \bar{x}}{s_x}\), \(w_i = \frac{W_i}{\bar{W}}\) |
detect_family.R |
Fitness type detection | Binary, count or continuous | \(W_i \in \{0,1\}\), \(W_i \in \mathbb{N}\) or \(W_i \in \mathbb{R}^+\) |
selection_differential.R |
Selection differential | \(S = \text{Cov}(z, w)\) | \(S = \mathbb{E}[(z - \bar{z})(w - \bar{w})]\) |
selection_coefficients.R |
Main wrapper | Complete analysis | Gradient estimation |
analyze_linear_selection.R |
Linear gradients | Regression | \(w = \alpha + \boldsymbol{\beta}^T \mathbf{z} + \varepsilon\) |
analyze_nonlinear_selection.R |
Quadratic gradients | \(z^2\) + interactions | \(w = \alpha + \boldsymbol{\beta}^T\mathbf{z} + \frac{1}{2}\mathbf{z}^T\boldsymbol{\gamma}\mathbf{z} + \varepsilon\) |
analyze_disruptive_selection.R |
Single-trait quadratic | Stabilising/disruptive test | \(\gamma_{ii} = 2\beta_{ii}\) |
extract_results.R |
Result extraction | Coefficient parsing | \(\beta\), \(\gamma\) |
univariate_spline.R |
1D fitness | Cubic regression spline (GCV, UBRE for 0/1 and Poisson counts) | \(w = \alpha + s(z) + \varepsilon\) |
correlated_fitness_surface.R |
2D fitness | GAM / TPS | \(w = \alpha + f(z_1, z_2) + \varepsilon\) |
adaptive_landscape.R |
Adaptive landscape | Simulation | \(\bar{W} = g(\bar{z}_1, \bar{z}_2)\) |
compare_fitness_surfaces.R |
Comparison | Surface contrast | Individual vs population |
plot_correlated_fitness.R |
Visualisation | Contour plots | 2D fitness |
plot_univariate_fitness.R |
Visualisation | Line + 95% band | Wald interval or bootstrap ribbon |
plot_adaptive_landscape.R |
Visualisation | Population surface | \(\bar{W}(\bar{z})\) |
temporal_landscape.R |
Selection over time | One fit per period, summary table | \(w_t = \alpha_t + s_t(z)\) |
plot_temporal_landscape.R |
Visualisation | Panels or heat map by period | \(w_t(z)\) |
selection_report.R |
Results table | \(S\), \(\beta\), \(\gamma\), \(\gamma_{ij}\) together | Same individuals, same scale |
bootstrap_selection.R |
Uncertainty | Resampling with replacement | Bootstrap SE, percentile CI |
check_selection_assumptions.R |
Assumption checks | Normality, VIF, rows per term, residuals | Mardia’s tests, Shapiro-Wilk, Breusch-Pagan |
canonical_analysis.R |
Canonical analysis | Eigen-decomposition of \(\boldsymbol{\gamma}\), double regression, permutation test | \(\boldsymbol{\gamma} = \mathbf{M}\boldsymbol{\Lambda}\mathbf{M}^T\) |
peak_difference.R |
Peak comparison | Difference in fitted fitness between two points, and between each and the pass between them | Link-scale difference, SE from the model’s covariance |
run_app.R |
Interactive app | The whole workflow in a browser | Same calculations as the functions above |
Function dependencies. Drawn from workflow.dot with Graphviz.
Solid arrows show data passed from one function to the next. Blue dashed arrows carry the prepared data to the functions that use it directly; grey dashed arrows show a result being reused elsewhere.
mgcv, MASS, Matrix,
ggplot2 and dplyr install with the package;
the rest are needed only for the features listed.
| Package | Purpose |
|---|---|
mgcv |
Generalised additive models (GAMs) for flexible spline-based fitness estimation |
fields |
Thin-plate spline (TPS) interpolation for bivariate fitness surfaces |
MASS |
Multivariate normal simulation (mvrnorm) for adaptive
landscape generation |
Matrix |
Matrix operations and positive-definite corrections
(nearPD) |
car |
Type III ANOVA and variance inflation factor (VIF) diagnostics |
ggplot2 |
Visualisation of fitness functions and contour surfaces |
dplyr |
Data manipulation and transformation (filtering, summarising) |
ggrepel |
Improved text labelling in plots (avoids overlap) |
viridis |
Perceptually uniform colour scales for scientific visualisation |
patchwork |
Composing multiple plots into a single figure |
performance |
Heteroscedasticity, overdispersion and \(R^2\) in the assumption checks |
akima |
Putting the two surfaces on one grid to correlate them in
compare_fitness_surfaces_data() |
shiny |
The app, run_app() |
plotly |
The rotatable 3D landscape in the app |
Five datasets come with the package, each with a help page giving its
columns and source: bumpus, the house sparrows of Bumpus
(1899); crescent_pond_pupfish and
little_lake_pupfish, the pupfish of Martin (2016);
finch_yearly, the medium ground finches of Beausoleil et
al. (2019); and finch_community, the five finch groups of
Beausoleil et al. (2023). Most examples below use simulated data;
sections 5.16, 5.17 and 7.0 use finch_yearly and
bumpus.
run_app() runs the same workflow in a browser, on four
of these datasets (all but finch_yearly) or an uploaded
CSV, and writes out the R calls that repeat the analysis with its
settings.
prepare_selection_data()Purpose: Preprocesses raw data for selection analysis.
prepare_selection_data(
data, # Data frame
fitness_col, # Name of fitness column (character)
trait_cols, # Vector of trait column names
standardize = TRUE, # Standardise traits to mean 0, SD 1
group = NULL, # Standardise and relativise within this column (e.g. year)
add_relative = TRUE, # Add relative fitness column
na_action = c("warn", "drop", "none"), # How to handle NAs
name_relative = "relative_fitness" # Name for relative fitness column
)Returns: Modified data frame with:
standardize = TRUE)add_relative = TRUE)na_action = "drop")Example:
set.seed(42)
raw_data <- data.frame(
survival = rbinom(100, 1, 0.6),
size = rnorm(100, 10, 2),
colour = rnorm(100, 5, 1)
)
prepared <- prepare_selection_data(
data = raw_data,
fitness_col = "survival",
trait_cols = c("size", "colour"),
standardize = TRUE,
add_relative = TRUE,
na_action = "drop"
)
cat("size:", nrow(prepared), "\n")
#> size: 100
head(prepared)| survival | size | colour | relative_fitness |
|---|---|---|---|
| 0 | 0.3732348 | -0.0351522 | 0.000000 |
| 0 | -0.8154449 | -1.6538384 | 0.000000 |
| 1 | 1.7210532 | 1.2589300 | 1.851852 |
| 0 | 0.7182770 | -0.2847265 | 0.000000 |
| 0 | 0.1236614 | -0.4927875 | 0.000000 |
| 1 | 0.3244579 | -1.3181840 | 1.851852 |
detect_family()Purpose: Automatically determines whether fitness is binary, count or continuous.
Logic:
Returns: List with:
$type: “binary”, “count” or “continuous”$family: the GLM family the p-value model starts from
(binomial, Poisson or Gaussian)Example:
# Binary fitness
fitness_binary <- c(1, 0, 1, 1, 0, 1)
detect_family(fitness_binary)
#> $type
#> [1] "binary"
#>
#> $family
#>
#> Family: binomial
#> Link function: logit
#>
#>
#> $note
#> [1] "Binary fitness (0/1) detected. Use logistic GLM for p-values."
# Continuous fitness
fitness_cont <- c(2.3, 4.1, 3.2, 5.6, 1.8)
detect_family(fitness_cont)
#> $type
#> [1] "continuous"
#>
#> $family
#>
#> Family: gaussian
#> Link function: identity
#>
#>
#> $note
#> [1] "Continuous fitness detected. Using Gaussian family."selection_differential()Purpose: Calculates the selection differential (\(S\)) for a single trait.
selection_differential(
data,
fitness_col,
trait_col,
standardized = TRUE, # Whether trait is already standardised
use_relative = TRUE # Use relative fitness
)Formula: \(S = \mathrm{Cov}(z, w)\)
When traits are standardised (\(\bar{z} = 0\)), this simplifies to: \[S = \mathbb{E}(z \times w) = \frac{1}{n}\sum_{i=1}^n z_i w_i\]
Example:
# Calculate selection differential for size
S_size <- selection_differential(
data = prepared,
fitness_col = "relative_fitness",
trait_col = "size",
standardized = TRUE,
use_relative = TRUE
)
# Calculate for colour
S_colour <- selection_differential(
data = prepared,
fitness_col = "relative_fitness",
trait_col = "colour",
standardized = TRUE,
use_relative = TRUE
)
cat("Selection Differentials (S):\n")
#> Selection Differentials (S):
cat("Size :", round(S_size, 4), "\n")
#> Size : -0.1126
cat("Colour:", round(S_colour, 4), "\n")
#> Colour: -0.0078analyze_linear_selection()Purpose: Estimates linear selection gradients (\(\boldsymbol{\beta}\)), with variance inflation factors and Type III tests.
analyze_linear_selection(
data,
fitness_col,
trait_cols,
fitness_type # "binary", "count" or "continuous"
)Features:
car package available)Returns: List containing:
$model: the fitted lm for continuous fitness; for
binary and count fitness a list with $ols (the gradient
model) and $glm (the model the p-values come from)$summary: Model summary, or a list of the two
summaries$anova: Type III ANOVA table (if available)Example:
linear_results <- analyze_linear_selection(
data = prepared,
fitness_col = "relative_fitness",
trait_cols = c("size", "colour"),
fitness_type = "continuous"
)
# View coefficients
summary(linear_results$model)
#>
#> Call:
#> lm(formula = as.formula(paste(fitness_col, "~", rhs)), data = fit_data)
#>
#> Residuals:
#> Min 1Q Median 3Q Max
#> -1.3632 -0.9623 0.7068 0.8423 1.0484
#>
#> Coefficients:
#> Estimate Std. Error t value Pr(>|t|)
#> (Intercept) 1.0000000 0.0930049 10.752 <2e-16 ***
#> size -0.1137116 0.0936612 -1.214 0.228
#> colour -0.0006674 0.0936612 -0.007 0.994
#> ---
#> Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
#>
#> Residual standard error: 0.93 on 97 degrees of freedom
#> Multiple R-squared: 0.01504, Adjusted R-squared: -0.00527
#> F-statistic: 0.7405 on 2 and 97 DF, p-value: 0.4795analyze_nonlinear_selection()Purpose: Estimates quadratic (\(\gamma_{ii}\)) and correlational (\(\gamma_{ij}\)) selection gradients.
Model specification: Automatically constructs:
Diagnostics:
Example:
nonlinear_results <- analyze_nonlinear_selection(
data = prepared,
fitness_col = "relative_fitness",
trait_cols = c("size", "colour"),
fitness_type = "continuous"
)
# Extract quadratic term for size (multiply by 2 for gamma)
coef(summary(nonlinear_results$model))["I(size^2)", ]
#> Estimate Std. Error t value Pr(>|t|)
#> -0.09616280 0.06704955 -1.43420501 0.15483215analyze_disruptive_selection()Purpose: Specialised function for detecting disruptive or stabilising selection on a single trait.
analyze_disruptive_selection(
data,
fitness_col,
trait_col,
fitness_type = c("auto", "binary", "count", "continuous"),
standardize = TRUE,
group = NULL
)Models: \(\beta\)
comes from the linear-only model \(w \sim
z\) and \(\gamma = 2 b_2\) from
\(w \sim z + z^2\) (with the standard
error doubled as well), following Lande & Arnold (1983) and
Stinchcombe et al. (2008). Both are computed through
selection_coefficients() on the single trait.
Returns: Data frame with:
Example:
disruptive_test <- analyze_disruptive_selection(
data = prepared,
fitness_col = "relative_fitness",
trait_col = "size",
fitness_type = "continuous"
)
print(disruptive_test)
#> Term Type Beta_Coefficient Standard_Error P_Value Variance
#> 1 size Linear -0.1137538 0.09299531 0.2241794 0.008648127
#> 2 size² Quadratic -0.1971818 0.11967907 0.1026730 0.014323079univariate_spline()Purpose: Fits a flexible nonparametric fitness function using GAMs.
univariate_spline(
data,
fitness_col,
trait_col,
fitness_type = c("auto", "binary", "count", "continuous"),
group = NULL, # optional group fixed effect
relative_col = NULL, # pre-computed relative fitness column, if any
k = 10, # basis dimension
bs = "cr", # spline basis: "cr", "tp" or "ps"
smoothing = "GCV.Cp", # how the smoothing parameter is chosen: "GCV.Cp" (UBRE for 0/1 and Poisson counts), "REML" or "ML"
bootstrap = FALSE, # TRUE: percentile-bootstrap 95% ribbon (Schluter 1988)
n_boot = 1000,
count_family = "poisson" # "quasipoisson" or "nb" for overdispersed counts
)Technical details:
bs = "cr") with the
smoothing parameter chosen by generalised cross-validation, following
Schluter (1988); for survival and Poisson counts mgcv uses its UBRE
form, and a negative binomial fit uses REML. bs sets the
family of curves and smoothing the criterion, to match the
smoother another study used; the amount of smoothing is still estimated
from the data, though the criterion can change it a good deal. A warning
is given if mgcv’s check suggests k left the curve too
little room to bend; the check is skipped for survival, whose residuals
carry too little to test.bootstrap = TRUEExample:
# k is capped from the number of distinct trait values; the default is fine here
spline_fit <- univariate_spline(
data = prepared,
fitness_col = "survival",
trait_col = "size",
fitness_type = "binary",
bootstrap = TRUE
)
head(spline_fit$grid)| size | fit | lwr | upr |
|---|---|---|---|
| -3.190356 | 0.4562304 | 6.0e-07 | 0.9999762 |
| -3.159592 | 0.4591544 | 7.0e-07 | 0.9999706 |
| -3.128828 | 0.4620803 | 9.0e-07 | 0.9999639 |
| -3.098064 | 0.4650070 | 1.2e-06 | 0.9999555 |
| -3.067301 | 0.4679334 | 1.5e-06 | 0.9999452 |
| -3.036537 | 0.4708582 | 1.9e-06 | 0.9999341 |
plot_univariate_fitness()Purpose: Visualises univariate correlated fitness functions with confidence bands.
Returns: ggplot2 object with line plot and 95% confidence bands.
Example:
p <- plot_univariate_fitness(
spline_fit,
"size",
title = "Correlated Fitness Function: Body Size"
)
print(p)Figure 1. Correlated fitness function for body size
adaptive_landscape()Purpose: Calculates the Wrightian adaptive landscape, which is a population-level concept distinct from correlated fitness surfaces.
adaptive_landscape(
data, # Individual-level data frame
fitness_model, # $model from correlated_fitness_surface() or univariate_spline()
trait_cols, # One or two trait names
group_col = NULL, # Optional grouping column (e.g., "species")
population_variance = NULL,# Within-population variance-covariance matrix
simulation_n = 1000, # Number of individuals to simulate per grid point
grid_n = 50, # Grid resolution (grid_n × grid_n)
custom_range = NULL, # Optional custom range for population means
clamp = TRUE, # Hold thin-plate fitness within the range of the fitness type
support_warn = 0.25 # Share outside the data at the optimum that draws a note
)Calculation Steps:
The simulated populations spread beyond the data, more so towards the edge of the grid, and the fitness of those individuals is extrapolated. The function reports how far this goes, as the share of each simulated population falling outside the convex hull of the observed traits, and says so when the optimum rests on it.
Returns:
$grid: Population means with mean fitness
(.mean_fit); with one trait also the fitness function at
those means (.ind_fit)$optimum: Population mean with highest fitness$support: Share of simulated individuals outside the
data, over the grid (outside) and at the optimum
(at_optimum); per population mean in the grid’s
.outside$actual_population_means: Actual means (if group_col
provided)An optimum on the edge of the grid: this usually
means the true optimum lies outside the observed traits.
custom_range, a named list with one range per trait, moves
or widens the grid, but fitness out there is extrapolated, and
$support says how much of the simulated population that
involves.
With a single trait the grid is a line of population means, the model
is normally the $model of univariate_spline(),
and the result is a curve rather than a surface.
Example:
landscape <- adaptive_landscape(
data = prepared,
fitness_model = surface$model,
trait_cols = c("size", "colour"),
grid_n = 50,
simulation_n = 1000
)
landscape$optimum| size | colour | .mean_fit | .outside | |
|---|---|---|---|---|
| 1222 | -0.741549 | -0.0222991 | 0.5612512 | 0.143 |
plot_adaptive_landscape()Purpose: Creates a 2D contour map showing how mean fitness varies with population mean phenotypes, or, for a single trait, a curve of mean fitness against the population mean with the fitness function drawn dashed alongside it.
plot_adaptive_landscape(
landscape, # Output from adaptive_landscape()
trait_cols, # One or two trait column names
original_data = NULL, # Optional original data for overlaying points
group_col = NULL, # Optional grouping column for labels
bins = 12, # Number of contour bins
show_optimum = TRUE, # Show optimal population mean (gold diamond)
show_actual_means = TRUE, # Show actual population means (red points)
show_individual = TRUE, # One trait: also draw the fitness function
show_support = FALSE, # Mark where most of the simulated population leaves the data
support_level = 0.5 # The share outside the data at which that line is drawn
)
plot_adaptive_landscape_3d(
landscape, # Output from adaptive_landscape()
trait_cols, # Two trait column names
theta = -30, # Azimuthal rotation angle
phi = 30 # Colatitude tilt angle
)Example:
p <- plot_adaptive_landscape(
landscape = landscape,
trait_cols = c("size", "colour"),
bins = 12
)
print(p)Adaptive landscape showing mean fitness as function of population mean size and colour
plot_adaptive_landscape_3d(
landscape = landscape,
trait_cols = c("size", "colour"),
theta = -30,
phi = 30
)3D adaptive landscape showing the shape of mean fitness
selection_coefficients()Purpose: Main wrapper function that runs a complete selection analysis.
selection_coefficients(
data,
fitness_col,
trait_cols,
fitness_type = c("auto", "binary", "count", "continuous"),
standardize = TRUE,
group = NULL, # optional grouping column (e.g. year)
use_relative_for_fit = TRUE,
return_grouped = FALSE, # TRUE: one set of gradients per group
se_type = "ols" # "hc3": leave-one-out standard errors
)Workflow:
auto)Returns: Data frame with columns:
Term: Coefficient name (e.g., “size”, “size²”,
“size×colour”)Type: “Linear”, “Quadratic”, or “Correlational”Beta_Coefficient: Estimated selection gradientStandard_Error: Standard error of estimate, least
squares or HC3 (se_type)P_Value: Statistical significanceVariance: Square of standard errorse_type = "hc3" gives leave-one-out standard errors
(MacKinnon & White, 1985; Mitchell-Olds & Shaw, 1987; see
?selection_coefficients). They allow for residual spread
that changes with the traits, as it does when the model leaves out
curvature; in the package’s simulation they recovered most of the
coverage lost there, most of all with heavy-tailed traits. They did
nothing for the coverage lost to estimating a skewed trait’s SD, and
with survival and counts they covered a little less. For continuous
fitness the p-values follow them; for survival and counts the p-values
come from the GLM with either se_type.
Example:
results_binary <- selection_coefficients(
data = prepared,
fitness_col = "survival", # Binary 0/1
trait_cols = c("size", "colour"), # Only available traits
fitness_type = "binary",
standardize = TRUE
)
print(results_binary)
#> Term Type Beta_Coefficient Standard_Error P_Value
#> 1 size Linear -0.1137115929 0.09366118 0.22486021
#> 2 colour Linear -0.0006673983 0.09366118 0.99313501
#> 3 size² Quadratic -0.1923255937 0.13409909 0.14687155
#> 4 colour² Quadratic -0.3230714003 0.16086280 0.04719178
#> 5 size × colour Correlational 0.0051607761 0.10537314 0.91801711
#> Variance
#> 1 0.008772416
#> 2 0.008772416
#> 3 0.017982567
#> 4 0.025876841
#> 5 0.011103499The table is put together by three exported helpers,
extract_linear_coefficients(),
extract_quadratic_coefficients() and
extract_interaction_coefficients(). Each takes the trait
names and the model object from analyze_linear_selection()
or analyze_nonlinear_selection(), pulls the terms out by
name, doubles the quadratic estimates and skips any term the model does
not have. They are only needed if you fit the models yourself and want
the same table.
selection_report()selection_coefficients() gives the coefficient table and
selection_report() the differentials and gradients in one
standardised table:
| Term | Type | Estimate | Std_Error | P_Value |
|---|---|---|---|---|
| size | Differential | -0.1126163 | NA | NA |
| colour | Differential | -0.0077854 | NA | NA |
| size | Linear | -0.1137116 | 0.0936612 | 0.2248602 |
| colour | Linear | -0.0006674 | 0.0936612 | 0.9931350 |
| size² | Quadratic | -0.1923256 | 0.1340991 | 0.1468716 |
| colour² | Quadratic | -0.3230714 | 0.1608628 | 0.0471918 |
| size × colour | Correlational | 0.0051608 | 0.1053731 | 0.9180171 |
compare_fitness_surfaces_data()Purpose: Compares the individual-level fitness surface with the population-level adaptive landscape: their optima, the distance between them, summary statistics and the correlation between the two surfaces. The surface is selection on individuals and the landscape is where the population mean would move, so the two can peak in different places.
comparison <- compare_fitness_surfaces_data(
correlated_surface = surface,
adaptive_landscape = landscape,
trait_cols = c("size", "colour")
)
comparison$summary_stats| Surface | Fitness_Range_Min | Fitness_Range_Max | Fitness_Mean | Fitness_SD | N_Points |
|---|---|---|---|---|---|
| Correlated Fitness | 0.1706367 | 0.7006815 | 0.4908177 | 0.1185462 | 3600 |
| Adaptive Landscape | 0.0622175 | 0.5612512 | 0.3493286 | 0.1226478 | 2500 |
plots <- plot_fitness_surfaces_comparison(
comparison_data = comparison,
bins = 15
)
print(plots$overlay)Overlay comparison
plots <- plot_fitness_surfaces_comparison(
comparison_data = comparison,
bins = 15
)
print(plots$side_by_side)Side-by-side comparison
temporal_landscape() and
plot_temporal_landscape()Purpose: Fit the fitness function or surface, and optionally the adaptive landscape, separately for each period, to see how selection changes over time.
temporal_landscape(
data, # Traits standardised once, over all periods
fitness_col,
trait_cols, # One or two traits
time_col, # The period of each row, usually year
fitness_type = "auto",
min_n = 20, # Periods with fewer rows are skipped
landscape = TRUE, # Also the adaptive landscape per period
bootstrap = FALSE, # Band around each period's fitness function (one trait)
grid_n = 60, simulation_n = 300
)
plot_temporal_landscape(
tl,
type = "panels", # or "heatmap" for one trait
show_points = TRUE, show_landscape = TRUE, show_optimum = TRUE,
ncol = NULL # Panel columns
)Returns: a list with one fit and one landscape per
period, a summary table (n, mean fitness, trait means, the
smooth’s degrees of freedom, the position and height of the highest
fitness and whether it sits at the edge of the data, the number of
interior peaks for one trait, and the landscape optimum), the fitted
values of every period stacked in grid, for one trait the
fitness functions on a common grid in heat (blank where a
period has no individuals), and the periods skipped for having fewer
than min_n rows.
Standardise the traits once over all periods before calling; nothing
is restandardised per period, since the periods share one trait axis.
The number of peaks depends on the basis size k and the
smoothing criterion, as any spline feature does, so test disruptive
selection with the quadratic gradient on the birds between the peaks, as
Beausoleil et al. (2019) did.
Example: medium ground finches at El Garrapatero, one row per bird per year it was seen, survival (seen again in any later year) against beak size (first principal component of the three beak medians), from the public data of Beausoleil et al. (2019). The splines here use REML; the “Finches year by year” article compares it with UBRE.
finch_prep <- prepare_selection_data(finch_yearly, "survived", "beak_pc1")
years <- temporal_landscape(finch_prep, "survived", "beak_pc1", "year", smoothing = "REML", simulation_n = 200)| time | n | mean_fitness | edf | optimum_beak_pc1 | peaks |
|---|---|---|---|---|---|
| 2004 | 110 | 0.3454545 | 1.000082 | -1.834495 | 0 |
| 2005 | 185 | 0.2756757 | 1.000645 | -1.708023 | 0 |
| 2006 | 233 | 0.1802575 | 1.000176 | -2.045747 | 0 |
| 2007 | 61 | 0.3442623 | 1.016109 | -1.162755 | 0 |
| 2008 | 127 | 0.3070866 | 1.000084 | -2.103969 | 0 |
| 2009 | 196 | 0.1938776 | 3.165374 | 2.841979 | 1 |
| 2010 | 175 | 0.1885714 | 1.000047 | -2.103969 | 0 |
Survival against beak size, one panel per year. Solid: fitness function. Dashed: adaptive landscape. Diamond: highest survival.
The same fitness functions as a heat map, blank where a year has no birds.
With REML the functions are close to straight lines on the logit
scale in every year but 2009, when survival dips past a small bump for
the small morph and then rises steeply towards large beaks; Beausoleil
et al. (2019) found their deepest valley in the 2009 to 2010 interval.
With the default UBRE criterion 2005, 2006 and 2008 each show two or
three peaks instead. Their test was the quadratic gradient on the birds
between the peaks, which is analyze_disruptive_selection()
in section 5.6 run on that subset.
canonical_analysis() and
plot_canonical_axes()Purpose: Rotate the matrix of quadratic and correlational gradients to its canonical axes and read the curvature along each.
canonical_analysis(
data, fitness_col, trait_cols,
fitness_type = "auto",
standardize = TRUE,
group = NULL, # Standardise and relativise within groups, as the gradients do
bootstrap = FALSE, # Percentile intervals for the eigenvalues
n_boot = 200,
conf = 0.95,
test = "permutation", # p-values by shuffling fitness (Reynolds et al. 2010); "double_regression" for the double-regression tests
n_perm = 999
)Returns: the gamma matrix, \(\beta\), the loadings M with
one column per axis, and an axes table with each eigenvalue
\(\lambda\), its standard error from
the double regression of Bisgaard and Ankenman (1996), a p-value from
the permutation test of Reynolds et al. (2010), and the directional
selection along the axis, \(\theta =
\mathbf{M}^\top\boldsymbol{\beta}\). The prepared data come back
with the canonical scores m1, m2, … added.
set.seed(1)
ca <- canonical_analysis(bumpus, "survival", c("total_length", "weight", "humerus"))
ca
#> Canonical analysis of gamma for total_length, weight, humerus on 136 individuals
#>
#> Loadings (columns are the canonical axes):
#> m1 m2 m3
#> total_length -0.256 -0.056 0.965
#> weight 0.590 0.782 0.202
#> humerus 0.766 -0.621 0.167
#>
#> Curvature along each axis (negative is consistent with stabilising selection, positive with disruptive):
#> axis lambda se p_value theta
#> m1 0.0331 0.1138 0.968 0.281
#> m2 -0.0248 0.2509 0.905 -0.507
#> m3 -0.1511 0.0988 0.469 -0.282
#>
#> p-values from 999 permutations of fitness (Reynolds et al. 2010), which allow for the
#> axes coming from these data. The standard errors treat the axes as known.A negative \(\lambda\) is curvature
along that axis, consistent with stabilising selection, and a positive
one with disruptive selection, exactly as for \(\gamma_{ii}\). The double-regression tests
treat the axes as known when they were estimated from the same data,
which makes them much too liberal (Reynolds et al., 2010). The p-values
come from shuffling fitness among individuals and repeating the analysis
each time; set a seed first to get the same p-values again. Sampling
error in \(\boldsymbol{\gamma}\) also
spreads its eigenvalues, so the largest curvatures are overestimated,
more so with many traits and few individuals (Reynolds et al., 2010),
and the estimated axes lean towards directions of phenotype with little
variance (Morrissey, 2014). bootstrap = TRUE gives
percentile intervals, with each resample’s axes matched to the original
ones, and plot_canonical_axes() draws the fitness surface
along any two axes, or the fitness function along one, which is where to
check whether a curved axis has a peak inside the data.
Fitness surface along the first and last canonical axes
The p-values test whether each gradient differs from zero. Treat them as a guide to how much the data say, not as the size of selection: a gradient of 0.05 with p < 0.001 is well estimated but weak, and a gradient of 0.4 with p = 0.08 may matter more. Report the estimate and its standard error or bootstrap interval alongside the p-value.
Linear Selection (\(\beta\)):
| \(\beta\) sign | Interpretation |
|---|---|
| Positive (+) | Selection favours larger trait values |
| Negative (-) | Selection favours smaller trait values |
| Zero (0) | No directional selection |
Quadratic Selection (\(\gamma\)):
| \(\gamma\) sign | Interpretation | Shape |
|---|---|---|
| Negative (-) | Stabilising selection | ∩-shaped |
| Positive (+) | Disruptive selection | ∪-shaped |
| Zero (0) | No nonlinear selection | Linear |
Correlational Selection (\(\gamma_{ij}\)):
| \(\gamma_{ij}\) sign | Interpretation | Shape of the surface |
|---|---|---|
| Positive (+) | Selection favours positive correlation (both traits high or both low) | A ridge running from low-low to high-high; the two off-diagonal corners are valleys |
| Negative (-) | Selection favours negative correlation (one high, one low) | A ridge running from high-low to low-high |
| Zero (0) | No correlational selection on the pair | Contours are symmetric about both axes |
In Brodie’s (1992) garter snakes, neither colour pattern nor escape behaviour was under strong selection on its own, but striped snakes that fled in a straight line and unstriped snakes that reversed direction both survived better than the mismatched combinations. Selection was on the combination, which shows as a ridge across the fitness surface with no peak on either axis. The sign of \(\gamma_{ij}\) depends on how the traits are scored. With stripes scored high and reversals counted, the favoured pairs are high-low and low-high, so the gradient is negative.
For standardised traits (\(\mu=0, \sigma=1\)):
| \(|\beta|\) | Effect size |
|---|---|
| < 0.1 | Weak selection |
| 0.1 - 0.3 | Moderate selection |
| > 0.3 | Strong selection |
These bands are relative to the published record: across the studies collated by Kingsolver et al. (2001) the median \(|\beta|\) was 0.16 and the median \(|\gamma|\) was 0.10, so a gradient above 0.3 is stronger than most selection measured in the wild. Gradients standardised by the trait mean instead of its standard deviation put traits with different variances on one scale, and Hereford et al. (2004) collate the record on that scale; the package reports variance-standardised gradients only.
Scenario 1: β_size = 0.25, p = 0.001
→ "Body size experiences significant positive directional selection.
A one-standard-deviation increase in size increases relative fitness by 0.25 units."
Scenario 2: γ_size = -0.15, p = 0.02
→ "Fitness curves downwards with body size, which is consistent with
stabilising selection if the fitness function peaks inside the data."
Scenario 3: γ_size x beak = 0.32, p = 0.004
→ "Significant positive correlational selection between size and beak depth.
Selection favours individuals that are either both large or both small."
check_selection_assumptions() puts the checks behind a
Lande and Arnold analysis in one table: Mardia’s tests of multivariate
normality of the traits, which is what lets the gradients be read as the
slope and curvature of the fitness surface (Lande & Arnold, 1983;
Morrissey & Sakrejda, 2013; they equal \(\mathbf{P}^{-1}\mathbf{S}\) by least
squares whatever the distribution), Shapiro-Wilk for each trait, the
largest VIF, the individuals (for survival, the rarer outcome) per
quadratic term, and for the gradient models residual normality and
heteroscedasticity, separation, or the dispersion ratio. With the
performance package installed it adds that package’s
heteroscedasticity and overdispersion tests and an R squared. Report the
table with the gradients, as Palacio et al. (2019) ask.
| check | statistic | p_value | note |
|---|---|---|---|
| Multivariate normality: Mardia skewness | 1.0454836 | 0.0064042 | skewed |
| Multivariate normality: Mardia kurtosis | 15.0033644 | 0.9971423 | no evidence against normality |
| Normality of total_length (Shapiro-Wilk) | 0.9782702 | 0.0285053 | not normal |
| Normality of weight (Shapiro-Wilk) | 0.9696585 | 0.0039519 | not normal |
| Normality of humerus (Shapiro-Wilk) | 0.9804520 | 0.0479984 | not normal |
| Collinearity: largest VIF | 1.7503118 | NA | below 5 |
| Rarer outcome per quadratic term | 7.1111111 | NA | 64 of the rarer outcome for 9 terms; treat gamma with care |
| Logistic model: separation | 1.3448491 | NA | no sign of separation |
| Model fit: R2_Tjur (performance) | 0.2399886 | NA |
| Analysis | Minimum n | Recommended n |
|---|---|---|
| Linear selection (continuous) | 10 | > 30 |
| Linear selection (binary) | 20 per group | > 50 per group |
| Nonlinear selection | 20 | > 100 |
| Bivariate TPS | 30 | > 200 |
The nonlinear model grows quickly with the number of traits: \(m\) traits give \(m\) linear, \(m\) quadratic and \(m(m-1)/2\) correlational terms, so nine traits mean 54 terms. Allow about ten individuals per term, and for binary fitness about ten survivors (or deaths, whichever is rarer) per term; below that the quadratic and correlational estimates are mostly noise, even when their p-values are small. Choose fewer traits on biological grounds rather than fitting them all.
Symptoms:
Solutions:
prepare_selection_data() checks the fitness and trait
columns for NA. The default,
na_action = "warn", keeps every row and warns with the
count. The models drop incomplete rows themselves, and traits and
relative fitness are standardised on the rows that are analysed, so the
rows that fall out do not shift the means. Rows with a missing group
label are kept and treated as their own group.
na_action = "drop" removes incomplete rows up front and
prints how many went, which keeps the sample size explicit and is the
better choice when the same data go on to the splines and surfaces.
# Missing values per column, and incomplete rows
colSums(is.na(data[, c(fitness_col, trait_cols)]))
sum(!complete.cases(data[, c(fitness_col, trait_cols)]))
prepared <- prepare_selection_data(data, fitness_col, trait_cols, na_action = "drop")Imputing traits is possible but changes what is being estimated. With more than a few percent of rows incomplete, report the count and analyse the complete cases.
High correlations between traits can inflate standard errors:
# Check correlations
cor(data[, trait_cols])
# Calculate VIF
if (requireNamespace("car", quietly = TRUE)) {
vif_vals <- car::vif(model)
# VIF > 5 is flagged by the package; > 10 is serious
}Solutions:
univariate_spline() warns “Reducing k from 10 to …” when
a trait has fewer distinct values than the basis asks for, which happens
with small groups, single years or coarsely measured traits. The curve
is then less flexible than the default. To set the basis size yourself,
in the spline or the surface:
uni <- univariate_spline(prepared, "survival", "size", k = 5)
surface <- correlated_fitness_surface(prepared, "survival", c("size", "colour"), k = 15)Lande, R., & Arnold, S. J. (1983). The measurement of selection on correlated characters. Evolution, 37(6), 1210-1226.
Stinchcombe, J. R., Agrawal, A. F., Hohenlohe, P. A., Arnold, S. J., & Blows, M. W. (2008). Estimating nonlinear selection gradients using quadratic regression coefficients: double or nothing? Evolution, 62(9), 2435-2440.
Schluter, D. (1988). Estimating the form of natural selection on a quantitative trait. Evolution, 42(5), 849-861.
Brodie, E. D. (1992). Correlational selection for color pattern and antipredator behavior in the garter snake Thamnophis ordinoides. Evolution, 46(5), 1284-1298.
Kingsolver, J. G., Hoekstra, H. E., Hoekstra, J. M., Berrigan, D., Vignieri, S. N., Hill, C. E., Hoang, A., Gibert, P., & Beerli, P. (2001). The strength of phenotypic selection in natural populations. The American Naturalist, 157(3), 245-261.
Wood, S. N. (2017). Generalized Additive Models: An Introduction with R (2nd ed.). Chapman and Hall/CRC.
Brodie, E. D., III, & Janzen, F. J. (1996). On the assignment of fitness values in statistical analyses of selection. Evolution, 50(1), 437-442.
Brodie, E. D., III, Moore, A. J., & Janzen, F. J. (1995). Visualizing and quantifying natural selection. Trends in Ecology & Evolution, 10(8), 313-318.
Schluter, D., & Nychka, D. (1994). Exploring fitness surfaces. The American Naturalist, 143(4), 597-616.
Palacio, F. X., Ordano, M., & Benitez-Vieyra, S. (2019). Measuring natural selection on multivariate phenotypic traits: a protocol for verifiable and reproducible analyses of natural selection. Israel Journal of Ecology & Evolution, 65, 130-136. doi:10.1163/22244662-20191064
Svensson, E. I. (2023). Phenotypic selection in natural populations: what have we learned in 40 years? Evolution, 77(7), 1493-1504.
Mardia, K. V. (1970). Measures of multivariate skewness and kurtosis with applications. Biometrika, 57(3), 519-530.
Beausoleil, M.-O., Carrión, P. L., Podos, J., Camacho, C., Rabadán-González, J., Richard, R., Lalla, K., Raeymaekers, J. A. M., Knutie, S. A., De León, L. F., Chaves, J. A., Clayton, D. H., Koop, J. A. H., Sharpe, D. M. T., Gotanda, K. M., Huber, S. K., Barrett, R. D. H., & Hendry, A. P. (2023). The fitness landscape of a community of Darwin’s finches. Evolution, 77(12), 2533-2546.
Beausoleil, M.-O., Frishkoff, L. O., M’Gonigle, L. K., Raeymaekers, J. A. M., Knutie, S. A., De León, L. F., Huber, S. K., Chaves, J. A., Clayton, D. H., Koop, J. A. H., Podos, J., Sharpe, D. M. T., Hendry, A. P., & Barrett, R. D. H. (2019). Temporally varying disruptive selection in the medium ground finch (Geospiza fortis). Proceedings of the Royal Society B, 286(1916), 20192290.
Arnold, S. J., & Wade, M. J. (1984). On the measurement of natural and sexual selection: theory. Evolution, 38(4), 709-719.
Hereford, J., Hansen, T. F., & Houle, D. (2004). Comparing strengths of directional selection: how strong is strong? Evolution, 58(10), 2133-2143.
Morrissey, M. B., & Sakrejda, K. (2013). Unification of regression-based methods for the analysis of natural selection. Evolution, 67(7), 2094-2100.
Phillips, P. C., & Arnold, S. J. (1989). Visualizing multivariate selection. Evolution, 43(6), 1209-1222.
Blows, M. W., & Brooks, R. (2003). Measuring nonlinear selection. The American Naturalist, 162(6), 815-820.
Bisgaard, S., & Ankenman, B. (1996). Standard errors for the eigenvalues in second-order response surface models. Technometrics, 38(3), 238-246.
Reynolds, R. J., Childers, D. K., & Pajewski, N. M. (2010). The distribution and hypothesis testing of eigenvalues from the canonical analysis of the gamma matrix of quadratic and correlational selection gradients. Evolution, 64(4), 1076-1085.
Morrissey, M. B. (2014). In search of the best methods for multivariate selection analysis. Methods in Ecology and Evolution, 5(10), 1095-1109.
Bumpus, H. C. (1899). The elimination of the unfit as illustrated by the introduced sparrow, Passer domesticus. Biological Lectures, Marine Biological Laboratory, Woods Hole, 209-226.
Martin, C. H. (2016). Context dependence in complex adaptive landscapes: frequency and trait-dependent selection surfaces within an adaptive radiation of Caribbean pupfishes. Evolution, 70(6), 1265-1282.
MacKinnon, J. G., & White, H. (1985). Some heteroskedasticity-consistent covariance matrix estimators with improved finite sample properties. Journal of Econometrics, 29(3), 305-325.
Mitchell-Olds, T., & Shaw, R. G. (1987). Regression analysis of natural selection: statistical inference and biological interpretation. Evolution, 41(6), 1149-1161.