Model 4: Skew-Normal Niche (Azzalini & Capitanio 1999)
Source:vignettes/model_skew_normal.Rmd
model_skew_normal.Rmd1 Ecological Motivation
Many species exhibit asymmetric environmental tolerances: they may tolerate higher temperatures much better than lower temperatures (or vice versa), so the niche boundary is not symmetric around the optimum. The multivariate normal model (Models 1–3) forces ellipsoidal symmetry, which can misfit such species.
The skew-normal model extends the Gaussian niche with a skewness parameter vector that allows each environmental axis to have an asymmetric tail. The density is the multivariate skew-normal distribution of Azzalini and Capitanio (1999).
2 Mathematical Specification
2.1 Density
The -dimensional skew-normal density is
where:
- is the -variate normal density with covariance ,
- is the standard univariate normal CDF,
- is the skewness vector,
- are the marginal standard deviations.
When , and the density reduces to , recovering the symmetric Gaussian niche.
2.2 The Skewness Mechanism
Define the standardised skew score
The factor modulates the Gaussian density:
- When (the point is in the direction favoured by ), and the density is close to .
- When (the point is in the opposite direction), and the density is suppressed.
This creates an asymmetric niche that is elongated in the direction of and truncated in the opposite direction.
3 Parameterisation
The unconstrained parameter vector has length :
| Block | Length | Constraint | Meaning |
|---|---|---|---|
| Unconstrained | Niche centroid | ||
| via | Marginal standard deviations | ||
| Unconstrained | C-vine partial correlations | ||
| Unconstrained | Skewness parameters |
Note the block is appended after the C-vine block, at position .
4 Implementation Call Chain
optimize_niche(likelihood = "skew_normal")
└─ niche_obj.cpp::create_niche_obj_ptr()
└─ loglik_niche_math_skew_normal_eigen() [C++, lines 543–583]
├─ build_L_cov() [theta → mu, sigma, L_cov]
├─ alpha = Map(theta + 2*p + n_v, p) [line 559]
├─ Mahalanobis q via forward solve [lines 565–566]
├─ log_det = 2 * sum log L_cov(k,k) [lines 568–570]
├─ sum_log_phi = sum_log_pnorm(diff, sigma, alpha) [line 572]
├─ prior_penalty_value(has_alpha=true) [lines 578–579]
└─ neg_log = 0.5*n*log_det + 0.5*sum_q - sum_log_phi + penalty
4.1 Key Code (loglik_niche_math_cpp.cpp, lines 543–583)
The C++ kernel:
- Reconstructs
from
via
build_L_cov()(line 557). - Maps the block from (line 559).
- Computes Mahalanobis distances via forward solve (lines 565–566).
- Computes from the Cholesky diagonal (lines 568–570).
- Evaluates
via
sum_log_pnorm()(line 572), which internally callsskew_z()(lines 520–527) to compute and thenR::pnorm(..., log_p=1)(line 515) for numerically stable . - Adds the ridge penalty via
prior_penalty_value()(lines 578–579). - Returns the guarded negative log-likelihood (line 581).
4.2 Helper Functions
| Function | Lines | Purpose |
|---|---|---|
log_pnorm(z) |
514–516 | Stable
via R::pnorm
|
skew_z(diff, sigma, alpha) |
520–527 | Vectorised |
sum_log_pnorm(diff, sigma, alpha) |
530–537 |
4.3 R Entry Point
optimize_niche(env_occ, likelihood = "skew_normal",
prior_log_sigma_lambda = 1.0,
prior_alpha_lambda = 1.0)5 When To Use
- When ecological knowledge or exploratory analysis suggests asymmetric environmental tolerances (e.g. thermal tolerance asymmetry in ectotherms).
- As a model-selection candidate: fit both
"presence_only"(symmetric) and"skew_normal"(asymmetric), then compare via AIC/BIC. - When the sample size is large enough to estimate additional parameters reliably ().
6 Limitations
Additional parameters. The skewness vector adds parameters. For environmental variables, the total parameter count goes from to . With small , the extra parameters may not be well-identified.
Unimodal skewness. The skew-normal is still unimodal; it cannot capture bimodal niches.
No background correction. This is a presence-only model. For background-corrected skew-normal, use
"skew_normal_weighted"(Model 5).Near-singular . When is very large, can be numerically challenging. The implementation uses
R::pnormwithlog_p = TRUE, which is stable for down to about .