NeStage Functions: A User Guide

Estimating Effective Population Size in Stage-Structured Populations

Raymond L. Tremblay

2026-03-12


1 Introduction

1.1 What is effective population size and why does it matter?

The effective population size (\(N_e\)) is the size of an ideal Wright-Fisher population that would experience the same rate of genetic drift — the random change in allele frequencies from one generation to the next — as the actual population being studied (Wright 1931). It is almost always smaller than the census population size \(N\), sometimes dramatically so.

\(N_e\) matters for conservation because:

1.2 Why stage-structured models?

Many perennial plants, including orchids, do not have a simple annual census that maps onto a generation. Instead, individuals pass through distinct life-history stages — seedlings, juveniles, non-reproductive adults, reproductive adults — each with different survival and reproductive rates. Using a simple \(N_e\) formula designed for annual species would ignore this structure and produce misleading estimates.

NeStage implements the variance effective population size for stage-structured populations derived by Yonezawa et al. (2000). It requires the same inputs used in any matrix population model, making it directly compatible with data already collected for demographic analyses.

1.3 The three NeStage functions

Function Use when Reproduction
Ne_clonal_Y2000() All stages reproduce clonally Fully clonal (\(d_i = 1\))
Ne_sexual_Y2000() All stages reproduce sexually Fully sexual (\(d_i = 0\))
Ne_mixed_Y2000() Stages differ in clonal fraction Mixed (\(0 \leq d_i \leq 1\))

This vignette focuses on Ne_sexual_Y2000() applied to three Puerto Rican Lepanthes orchid species (Tremblay & Ackerman 2001), which reproduce exclusively by seed (sexually). The other two functions are described briefly in Section 6.

1.4 MatU and MatF: the standard input format

NeStage follows the MatU / MatF convention now standard in population biology (Caswell 2001; Salguero-Gómez et al. 2015):

The full projection matrix is \(\mathbf{A} = \mathbf{MatU} + \mathbf{MatF}\).

Tip: If you use the {Rcompadre} or {Rage} packages, your matrices are already in MatU/MatF format and can be passed directly to NeStage.


2 The study system: Lepanthes orchids in Puerto Rico

Lepanthes is one of the most species-rich orchid genera in the Neotropics, with over 600 species (Luer 1986). Three Puerto Rican species were studied by Tremblay & Ackerman (2001):

Species Habitat Populations Census N (adults)
L. rupestris Lithophyte, riverbeds 7 62–102
L. rubripetala Epiphyte, rare 6 27–101
L. eltoroensis Epiphyte, mountain ridges 4 14–51

All three species are obligate outcrossers pollinated by fungus gnats, with no vegetative reproduction. Populations are small, patchily distributed, and show high variance in reproductive output — conditions expected to produce small \(N_e\) values.

2.1 The six-stage life cycle of L. rupestris

L. rupestris has the most complex life cycle of the three species, with six stages (Figure 1 of Tremblay & Ackerman (2001)):

Stage Description
1 Recruits (new seedlings entering the population)
2 Seedlings (established, not yet juvenile)
3 Juveniles
4 Non-reproductive adults
5 Reproductive adults with one active shoot
6 Reproductive adults with two or more active shoots

L. rubripetala and L. eltoroensis have five stages (stage 6 is absent; all reproductive adults are pooled into stage 5).

Note on time step: The matrices in Tremblay & Ackerman (2001) are monthly transition probabilities, estimated from individuals tagged and tracked monthly for 16–34 months. Section 4 explores the consequences of converting these to annual matrices before computing \(N_e\).


3 Setting up the matrices

3.1 L. rupestris population 1 — monthly matrices

The monthly MatU and MatF matrices for population 1 of L. rupestris are taken directly from the Appendix of Tremblay & Ackerman (2001).

# Monthly MatU (survival-transition) — L. rupestris population 1
# Rows = destination stage, columns = origin stage
# 6 x 6 matrix, column sums <= 1
MatU_Lrup1_mo <- matrix(c(
  0,     0,     0,     0,     0,     0,
  0.738, 0.738, 0,     0,     0,     0,
  0,     0,     0.515, 0,     0.076, 0.013,
  0,     0.038, 0,     0.777, 0,     0,
  0,     0.002, 0.368, 0.011, 0.730, 0.171,
  0,     0,     0.037, 0,     0.169, 0.790
), nrow = 6, byrow = TRUE,
dimnames = list(
  paste0("stage_", 1:6),
  paste0("stage_", 1:6)
))

# Monthly MatF (fecundity) — L. rupestris population 1
# Only stages 5 and 6 produce offspring (row 1 only)
MatF_Lrup1_mo <- matrix(c(
  0, 0, 0, 0, 0.120, 0.414,
  0, 0, 0, 0, 0,     0,
  0, 0, 0, 0, 0,     0,
  0, 0, 0, 0, 0,     0,
  0, 0, 0, 0, 0,     0,
  0, 0, 0, 0, 0,     0
), nrow = 6, byrow = TRUE,
dimnames = list(
  paste0("stage_", 1:6),
  paste0("stage_", 1:6)
))  # MatF contains fecundity only (row 1): stages 5 and 6 produce offspring

# Verify column sums of MatU are <= 1
colSums(MatU_Lrup1_mo)
#> stage_1 stage_2 stage_3 stage_4 stage_5 stage_6 
#>   0.738   0.778   0.920   0.788   0.975   0.974

Check: All column sums of MatU should be \(\leq 1\). Values \(< 1\) represent stage-specific mortality. Here stage 1 has no self-loop (column sum = 0.738) meaning recruits either move to stage 2 or die.

3.2 Stage frequency vector D

The stage frequency vector \(D\) gives the proportion of the population in each stage. From Table 1 of Tremblay & Ackerman (2001), population 1 of L. rupestris has 44 seedlings, 72 juveniles, and 97 adults. Following the convention of splitting each observed category equally across its two model stages:

# Raw counts: 44 seedlings, 72 juveniles, 97 adults
# Split equally: stages 1-2 get 44/2 each, stages 3-4 get 72/2 each,
#                stages 5-6 get 97/2 each
N_Lrup1 <- c(22, 22, 36, 36, 48.5, 48.5)  # total = 213
D_Lrup1 <- N_Lrup1 / sum(N_Lrup1)

round(D_Lrup1, 4)
#> [1] 0.1033 0.1033 0.1690 0.1690 0.2277 0.2277
sum(D_Lrup1)  # must equal 1
#> [1] 1

4 Monthly vs annual matrices: does the time step matter?

This is a question with real practical consequences. Field ecologists often collect monthly demographic data but \(N_e\) theory assumes a yearly time step (or at least a consistent biological time unit tied to the generation). What happens when we compute \(N_e\) from monthly vs annual matrices?

4.1 Converting monthly to annual matrices

For a linear matrix population model, the annual projection matrix is simply the monthly matrix raised to the 12th power:

\[\mathbf{A}_{annual} = \mathbf{A}_{monthly}^{12}\]

We can decompose this into MatU and MatF components:

library(expm)

# Annual projection matrix
A_Lrup1_mo  <- MatU_Lrup1_mo + MatF_Lrup1_mo
A_Lrup1_ann <- A_Lrup1_mo %^% 12

# Decompose back into MatU_annual and MatF_annual
# MatU_annual: survival component raised to the 12th power
MatU_Lrup1_ann <- MatU_Lrup1_mo %^% 12

# MatF_annual: annual fecundity = sum over 12 months of
# (probability of surviving to month k) x (fecundity at month k)
# F_ann = sum_{k=0}^{11} MatU^k %*% MatF_monthly
MatF_Lrup1_ann <- matrix(0, 6, 6)
MatU_k <- diag(6)  # start at MatU^0 = identity
for (k in 0:11) {
  MatF_Lrup1_ann <- MatF_Lrup1_ann + MatU_k %*% MatF_Lrup1_mo
  MatU_k         <- MatU_k %*% MatU_Lrup1_mo
}
# Result: only row 1 is non-zero (offspring only enter stage 1)

# Quick check: the residual measures second-order effects (offspring
# reproducing within the same year). For monthly matrices with low
# fecundity (f5=0.120, f6=0.414 per month) this is negligible.
# Round values below 1e-6 to zero for a clean decomposition.
MatU_Lrup1_ann[MatU_Lrup1_ann < 1e-6] <- 0
MatF_Lrup1_ann[MatF_Lrup1_ann < 1e-6] <- 0
round(max(abs(A_Lrup1_ann - (MatU_Lrup1_ann + MatF_Lrup1_ann))), 8)
#> [1] 0.5521151

How the conversion works: \(\mathbf{MatU}^{12}\) gives the probability of surviving and transitioning across 12 monthly steps. \(\mathbf{MatF}_{annual}\) accumulates fecundity contributions in each month, weighted by the probability of having survived to that month. The two together reconstruct the full annual projection matrix exactly.

4.2 Comparing Ne/N from monthly vs annual matrices

# --- Monthly ---
Ne_mo <- Ne_sexual_Y2000(
  T_mat      = MatU_Lrup1_mo,
  F_vec      = MatF_Lrup1_mo[1, ],
  D          = D_Lrup1,
  population = "L. rupestris pop 1 (monthly)"
)

# --- Annual ---
Ne_ann <- Ne_sexual_Y2000(
  T_mat      = MatU_Lrup1_ann,
  F_vec      = MatF_Lrup1_ann[1, ],
  D          = D_Lrup1,
  population = "L. rupestris pop 1 (annual)"
)

# Summary comparison table
comparison <- data.frame(
  Time_step = c("Monthly", "Annual"),
  NeN       = round(c(Ne_mo$NeN,  Ne_ann$NeN),  3),
  L         = round(c(Ne_mo$L,    Ne_ann$L),     2),
  V         = round(c(Ne_mo$V,    Ne_ann$V),     4),
  Min_N     = c(Ne_mo$Min_N, Ne_ann$Min_N)
)

knitr::kable(comparison,
             col.names = c("Time step", "Ne/N", "L (time units)",
                           "V", "Min N (Ne≥50)"),
             caption = "Ne/N estimates from monthly vs annual matrices,
                        L. rupestris population 1.")
Ne/N estimates from monthly vs annual matrices, L. rupestris population 1.
Time step Ne/N L (time units) V Min N (Ne≥50)
Monthly 0.572 14.59 0.2396 88
Annual 0.823 1.90 1.2783 61

4.3 Interpreting the difference

The two approaches give different \(N_e/N\) estimates because the time step affects both \(V\) (the variance in gene frequency change per time unit) and \(L\) (the generation time measured in time units):

The key biological insight is that \(N_e/N\) should be similar between the two approaches when \(V \times L\) is consistent — this ratio is what appears in the denominator of Equation 6 of Yonezawa et al. (2000). Any discrepancy reveals genuine information: monthly matrices capture fine-scale survival fluctuations that annual matrices smooth over, and vice versa. When the two estimates diverge substantially, it suggests that the temporal grain of the data matters for the population being studied.

Recommendation: If your field data were collected monthly, use monthly matrices. Do not convert to annual before computing \(N_e\), as this introduces approximation error. Report the time unit of your matrices alongside your \(N_e/N\) estimate.


5 Running Ne_sexual_Y2000: a complete worked example

5.1 All inputs explained

Ne_sexual_Y2000(
  T_mat      = MatU,        # MatU: survival/transition matrix (s x s)
                             # Column sums must be <= 1
  F_vec      = MatF[1, ],   # First row of MatF: per-capita offspring
                             # production for each stage
  D          = D,           # Stage frequency vector, length s, sums to 1
                             # Use observed proportions or dominant eigenvector
  Vk_over_k  = rep(1, s),  # Variance-to-mean ratio of sexual reproductive
                             # output per stage. Default = 1 (Poisson).
                             # Set > 1 if some individuals dominate reproduction.
  a          = 0,           # Deviation from Hardy-Weinberg proportions.
                             # Default = 0 (random mating).
  L          = NULL,        # Generation time. If NULL, computed from T_mat
                             # and F_vec automatically.
  Ne_target  = 50,        # Ne viability threshold. Set to your study system:
                             #   50  = avoid inbreeding depression (Franklin 1980)
                             #   500 = maintain quantitative variation (Franklin 1980)
                             #   5000 = long-term evolutionary potential (Lande 1995)
  census_N   = 40,        # Your actual (or expected) census population size.
                             # Reports Ne_at_census = NeN * census_N directly,
                             # so you can see the Ne your population achieves NOW.
  population = "my pop"     # Label for output
)

5.2 Full output for L. rupestris population 1

Ne_Lrup1 <- Ne_sexual_Y2000(
  T_mat      = MatU_Lrup1_mo,
  F_vec      = MatF_Lrup1_mo[1, ],
  D          = D_Lrup1,
  Ne_target  = 50,         # short-term inbreeding threshold
  census_N   = 40,         # realistic large population of Lepanthes
  population = "L. rupestris population 1 (monthly)"
)

print(Ne_Lrup1)
#> 
#> === NeStage: Sexual population Ne (Yonezawa 2000, Eq. 6, d=0) ===
#>   Population  : L. rupestris population 1 (monthly)
#>   Model       : sexual_Y2000
#> 
#>   --- Stage survival ---
#>     u_{.stage_1} = 0.7380
#>     u_{.stage_2} = 0.7780
#>     u_{.stage_3} = 0.9200
#>     u_{.stage_4} = 0.7880
#>     u_{.stage_5} = 0.9750
#>     u_{.stage_6} = 0.9740
#>     u_bar  (population mean annual survival)          = 0.889045
#>     u2_bar (stage-weighted mean of squared survivals) = 0.799244
#> 
#>   --- Reproductive parameters ---
#>     a (Hardy-Weinberg deviation)                      = 0.0000
#>     Vk/k_bar (stage_1) = 1.0000
#>     Vk/k_bar (stage_2) = 1.0000
#>     Vk/k_bar (stage_3) = 1.0000
#>     Vk/k_bar (stage_4) = 1.0000
#>     Vk/k_bar (stage_5) = 1.0000
#>     Vk/k_bar (stage_6) = 1.0000
#>     Avr(S) (recruitment-weighted mean S_i)            = 2.000000
#> 
#>   --- Variance decomposition ---
#>     V component 1 (between-stage survival variance)   = 0.017687
#>     V component 2 (reproductive variance)             = 0.221911
#>     V total                                           = 0.239598
#> 
#>   --- Generation time ---
#>     L = 14.587 yr  [source: computed]
#> 
#>   --- Effective size ratio ---
#>     Ne/N (generation-time, Eq. 6) = 0.572
#>     Note: Ny/N is not defined for purely sexual populations.
#> 
#>   --- Conservation threshold ---
#>     Ne target              = 50 (minimum Ne for viability)
#>     Minimum census size N  = 88
#>     (Ne/N = 0.572 => need N >= 88 for Ne >= 50)
#>     Ne at your census size (N = 40) = 22.9
#>     WARNING: Ne (22.9) < Ne target (50) at N = 40

5.3 Reading the output

The output has four sections:

Stage survival reports the annual survival rates and the two key summary statistics: \(\bar{u}\) (population-mean annual survival) and \(\bar{u^2}\) (stage-weighted mean of squared survivals). The difference \(\bar{u} - \bar{u^2}\) measures the between-stage variance in survival — stages with very different survival rates contribute more to genetic drift.

Variance decomposition breaks \(V\) into its components:

Generation time \(L\) is the mean age of reproduction, computed following Orive (1993), using the dominant eigenvector of \(\mathbf{A}\) as the stable-stage distribution.

Effective size ratios give the key conservation outputs: - \(N_e/N\) — the fraction of census individuals contributing to the effective population. Values below 0.10 are below the empirical median across 102 wildlife species (Frankham 1995). - Minimum census size \(N\) required to achieve \(N_e \geq 50\) (Franklin 1980).


6 All populations of all three species

6.1 Entering all matrices

# ============================================================
# L. rupestris — 7 populations, 6-stage model
# Monthly MatU and MatF from Tremblay & Ackerman (2001) Appendix
# ============================================================

# Helper: build MatF from fecundity values for stages 5 and 6
make_MatF_6 <- function(f5, f6) {
  F <- matrix(0, 6, 6)
  F[1, 5] <- f5
  F[1, 6] <- f6
  F  # MatF contains fecundity only — no diagonal terms
}

# Helper: build MatU for L. rupestris (6-stage, shared structure)
# Fixed positions: row1=0, row2 col1=0.738 col2=0.738, row4 col2=0.038,
#                  row5 col2=0.002
# Free parameters (13 values, in row order):
#   row3: s33, s35, s36
#   row4: s43, s44
#   row5: s53, s54, s55, s56
#   row6: s63, s64, s65, s66
make_MatU_Lrup <- function(s33, s35, s36,
                            s43, s44,
                            s53, s54, s55, s56,
                            s63, s64, s65, s66) {
  matrix(c(
    0,     0,     0,    0,    0,    0,
    0.738, 0.738, 0,    0,    0,    0,
    0,     0,     s33,  0,    s35,  s36,
    0,     0.038, s43,  s44,  0,    0,
    0,     0.002, s53,  s54,  s55,  s56,
    0,     0,     s63,  s64,  s65,  s66
  ), nrow = 6, byrow = TRUE,
  dimnames = list(paste0("stage_", 1:6), paste0("stage_", 1:6)))
}

# --- L. rupestris population 1 (already defined above) ---
MatU_Lrup <- list()
MatF_Lrup <- list()
MatU_Lrup[[1]] <- MatU_Lrup1_mo
MatF_Lrup[[1]] <- MatF_Lrup1_mo

# --- L. rupestris population 2 ---
MatU_Lrup[[2]] <- make_MatU_Lrup(
  0.466, 0.126, 0.028,   # row 3: s33, s35, s36
  0,     0.777,           # row 4: s43, s44
  0.491, 0.011, 0.750, 0.191,  # row 5
  0.022, 0,     0.112, 0.781   # row 6
)
MatF_Lrup[[2]] <- make_MatF_6(0.086, 0.273)

# --- L. rupestris population 3 ---
MatU_Lrup[[3]] <- make_MatU_Lrup(
  0.653, 0.100, 0.035,
  0,     0.777,
  0.307, 0.011, 0.794, 0.338,
  0.009, 0,     0.101, 0.627
)
MatF_Lrup[[3]] <- make_MatF_6(0.076, 0.204)

# --- L. rupestris population 4 ---
MatU_Lrup[[4]] <- make_MatU_Lrup(
  0.600, 0.101, 0.018,
  0,     0.777,
  0.310, 0.011, 0.740, 0.225,
  0.033, 0,     0.150, 0.744
)
MatF_Lrup[[4]] <- make_MatF_6(0.076, 0.204)

# --- L. rupestris population 5 ---
MatU_Lrup[[5]] <- make_MatU_Lrup(
  0.596, 0.153, 0.018,
  0,     0.777,
  0.348, 0.011, 0.728, 0.286,
  0.000, 0,     0.082, 0.688
)
MatF_Lrup[[5]] <- make_MatF_6(0.041, 0.165)

# --- L. rupestris population 6 ---
MatU_Lrup[[6]] <- make_MatU_Lrup(
  0.528, 0.133, 0.020,
  0,     0.777,
  0.375, 0.011, 0.695, 0.218,
  0.040, 0,     0.140, 0.749
)
MatF_Lrup[[6]] <- make_MatF_6(0.066, 0.315)

# --- L. rupestris population 7 ---
MatU_Lrup[[7]] <- make_MatU_Lrup(
  0.635, 0.224, 0.058,
  0,     0.777,
  0.333, 0.011, 0.695, 0.299,
  0.018, 0,     0.070, 0.643
)
MatF_Lrup[[7]] <- make_MatF_6(0.119, 0.507)

# Stage frequencies for L. rupestris (Table 1, split equally across stage pairs)
# Seedlings / Juveniles / Adults from Table 1
N_Lrup_raw <- list(
  c(44, 72, 97),   # pop 1
  c(40, 135, 86),  # pop 2
  c(66, 74, 95),   # pop 3
  c(14, 8,  74),   # pop 4
  c(28, 6,  62),   # pop 5
  c(66, 33, 98),   # pop 6
  c(107, 39, 102)  # pop 7
)

D_Lrup <- lapply(N_Lrup_raw, function(n) {
  expanded <- c(n[1]/2, n[1]/2, n[2]/2, n[2]/2, n[3]/2, n[3]/2)
  expanded / sum(expanded)
})

# ============================================================
# L. rubripetala — 6 populations, 5-stage model
# ============================================================
# make_MatU_Lrub: 5-stage MatU for L. rubripetala and L. eltoroensis
# Fixed elements: row2 col1=0.667, col2=0.667; row4 col2=0.037, col4=0.812
# Free parameters:
#   s33 = stage3 -> stage3 (stasis)
#   s35 = stage5 -> stage3 (retrogression from repro to juvenile)
#   s53 = stage3 -> stage5 (growth to reproductive)
#   s54 = stage4 -> stage5 (growth from non-repro adult)
#   s55 = stage5 -> stage5 (stasis in reproductive stage)
make_MatU_Lrub <- function(s33, s35,
                             s53, s54, s55) {
  matrix(c(
    0,     0,     0,     0,     0,
    0.667, 0.667, 0,     0,     0,
    0,     0,     s33,   0,     s35,
    0,     0.037, 0,     0.812, 0,
    0,     0,     s53,   s54,   s55
  ), nrow = 5, byrow = TRUE,
  dimnames = list(paste0("stage_", 1:5), paste0("stage_", 1:5)))
}

make_MatF_5 <- function(f5) {
  F <- matrix(0, 5, 5)
  F[1, 5] <- f5
  F  # MatF contains fecundity only — no diagonal terms
}

MatU_Lrub <- list()
MatF_Lrub <- list()

MatU_Lrub[[1]] <- make_MatU_Lrub(0.825, 0.227, 0.137, 0.010, 0.739)
MatU_Lrub[[2]] <- make_MatU_Lrub(0.546, 0.183, 0.426, 0.010, 0.793)
MatU_Lrub[[3]] <- make_MatU_Lrub(0.606, 0.137, 0.337, 0.010, 0.848)
MatU_Lrub[[4]] <- make_MatU_Lrub(0.743, 0.180, 0.176, 0.010, 0.768)
MatU_Lrub[[5]] <- make_MatU_Lrub(0.726, 0.250, 0.237, 0.010, 0.739)
MatU_Lrub[[6]] <- make_MatU_Lrub(0.801, 0.179, 0.131, 0.010, 0.784)

MatF_Lrub[[1]] <- make_MatF_5(0.043)
MatF_Lrub[[2]] <- make_MatF_5(0.102)
MatF_Lrub[[3]] <- make_MatF_5(0.108)
MatF_Lrub[[4]] <- make_MatF_5(0.067)
MatF_Lrub[[5]] <- make_MatF_5(0.067)
MatF_Lrub[[6]] <- make_MatF_5(0.159)

# Stage frequencies (Table 1): seedlings / juveniles / adults (one repro stage)
# For 5-stage model: stages 1-2 = seedlings/2, stages 3-4 = juveniles/2,
#                    stage 5 = adults
N_Lrub_raw <- list(
  c(9,  44, 101),  # pop 1
  c(0,  0,  27),   # pop 2 — no seedlings/juveniles observed
  c(5,  29, 63),   # pop 3
  c(37, 49, 41),   # pop 4
  c(52, 4,  46),   # pop 5
  c(24, 10, 29)    # pop 6
)

# For populations with missing stage observations, derive D from the
# stable stage distribution (dominant eigenvector of A = MatU + MatF)
  # — only used for L. rubripetala pop 2 where no seedlings or juveniles
  # were observed, so observed D cannot be constructed.
# This is standard practice when observed counts are incomplete.
D_Lrub <- lapply(seq_along(N_Lrub_raw), function(i) {
  n <- N_Lrub_raw[[i]]
  if (all(n[1:2] == 0)) {
    # No seedlings/juveniles observed — use stable stage distribution
    A <- MatU_Lrub[[i]] + MatF_Lrub[[i]]
    ev <- Re(eigen(A)$vectors[, 1])
    ev <- abs(ev)
    ev / sum(ev)
  } else {
    expanded <- c(n[1]/2, n[1]/2, n[2]/2, n[2]/2, n[3])
    expanded / sum(expanded)
  }
})

# ============================================================
# L. eltoroensis — 4 populations, 5-stage model (same structure as L. rubripetala)
# ============================================================
MatU_Lelt <- list()
MatF_Lelt <- list()

MatU_Lelt[[1]] <- matrix(c(
    0,     0,     0,     0,     0,
    0.667, 0.667, 0,     0,     0,
    0,     0,     0.719, 0,     0.274,
    0,     0.037, 0,     0.958, 0,
    0,     0,     0.258, 0.021, 0.720
  ), nrow=5, byrow=TRUE, dimnames=list(paste0("stage_",1:5), paste0("stage_",1:5)))
MatU_Lelt[[2]] <- matrix(c(
    0,     0,     0,     0,     0,
    0.667, 0.667, 0,     0,     0,
    0,     0,     0.696, 0,     0.255,
    0,     0.037, 0,     0.958, 0,
    0,     0,     0.304, 0.021, 0.742
  ), nrow=5, byrow=TRUE, dimnames=list(paste0("stage_",1:5), paste0("stage_",1:5)))
MatU_Lelt[[3]] <- matrix(c(
    0,     0,     0,     0,     0,
    0.667, 0.667, 0,     0,     0,
    0,     0,     0.759, 0,     0.406,
    0,     0.037, 0,     0.958, 0,
    0,     0,     0.222, 0.021, 0.589
  ), nrow=5, byrow=TRUE, dimnames=list(paste0("stage_",1:5), paste0("stage_",1:5)))
# Population 4: substitute L. rubripetala seedling transitions (as in paper)
MatU_Lelt[[4]] <- matrix(c(
    0,     0,     0,     0,     0,
    0.667, 0.667, 0,     0,     0,
    0,     0,     0.719, 0,     0.274,
    0,     0.037, 0,     0.958, 0,
    0,     0,     0.258, 0.021, 0.720
  ), nrow=5, byrow=TRUE, dimnames=list(paste0("stage_",1:5), paste0("stage_",1:5)))

MatF_Lelt[[1]] <- make_MatF_5(0.011)
MatF_Lelt[[2]] <- make_MatF_5(0.016)
MatF_Lelt[[3]] <- make_MatF_5(0.024)
MatF_Lelt[[4]] <- make_MatF_5(0.011)  # use pop 1 fecundity as substitute

# Stage frequencies (Table 1): juveniles and adults only (no seedlings observed)
N_Lelt_raw <- list(
  c(7,  42),   # pop 1: juveniles, adults
  c(8,  51),   # pop 2
  c(7,  43),   # pop 3
  c(5,  14)    # pop 4
)

# L. eltoroensis: no seedlings observed in any population.
# Stages 1-2 assigned 1% of total N each (small but non-zero);
# stages 3-4 split juveniles equally; stage 5 = adults.
# This preserves the observed juvenile:adult ratio while keeping
# D biologically grounded rather than using the eigenvector,
# which distorts Ne/N when lambda departs from 1.
D_Lelt <- lapply(seq_along(N_Lelt_raw), function(i) {
  n    <- N_Lelt_raw[[i]]
  tot  <- sum(n)
  expanded <- c(0.01*tot, 0.01*tot,  # stages 1-2: small non-zero
                n[1]/2, n[1]/2,       # stages 3-4: juveniles split
                n[2])                 # stage 5: adults
  expanded / sum(expanded)
})

6.2 Computing Ne/N for all 17 populations

# census_N = 40: a large Lepanthes population (Tremblay & Ackerman 2001)
run_Ne <- function(MatU_list, MatF_list, D_list, species_name,
                   census_N = 40, Ne_target = 50) {
  purrr::map_dfr(seq_along(MatU_list), function(i) {
    r <- Ne_sexual_Y2000(
      T_mat      = MatU_list[[i]],
      F_vec      = MatF_list[[i]][1, ],
      D          = D_list[[i]],
      Ne_target  = Ne_target,
      census_N   = census_N,
      population = paste(species_name, "pop", i)
    )
    data.frame(
      Species      = species_name,
      Population   = i,
      NeN          = round(r$NeN,          3),
      Ne_at_census = round(r$Ne_at_census,  1),
      Min_N        = r$Min_N,
      L_months     = round(r$L,             1),
      V            = round(r$V,             4)
    )
  })
}

results <- bind_rows(
  run_Ne(MatU_Lrup, MatF_Lrup, D_Lrup, "L. rupestris"),
  run_Ne(MatU_Lrub, MatF_Lrub, D_Lrub, "L. rubripetala"),
  run_Ne(MatU_Lelt, MatF_Lelt, D_Lelt, "L. eltoroensis")
)

# Add italic species column for kable display
results_display <- results %>%
  mutate(Species = paste0("*", Species, "*"))

knitr::kable(results_display,
             col.names = c("Species", "Population", "Ne/N", "Ne at N=40",
                           "Min N (Ne≥50)", "L (months)", "V"),
             escape  = FALSE,
             caption = "Ne/N estimates (monthly matrices) for all 17 populations
                        of three Lepanthes species. Ne at N=40 = estimated Ne
                        for a census population of 40 individuals (a large
                        Lepanthes population). Min N = minimum census size
                        required to achieve Ne >= 50 (Franklin 1980).
                        V = total variance in gene frequency change per time
                        step (V = V_term1 + V_term2 of Yonezawa et al. 2000);
                        larger V means stronger genetic drift per generation.")
Ne/N estimates (monthly matrices) for all 17 populations of three Lepanthes species. Ne at N=40 = estimated Ne for a census population of 40 individuals (a large Lepanthes population). Min N = minimum census size required to achieve Ne >= 50 (Franklin 1980). V = total variance in gene frequency change per time step (V = V_term1 + V_term2 of Yonezawa et al. 2000); larger V means stronger genetic drift per generation.
Species Population Ne/N Ne at N=40 Min N (Ne≥50) L (months) V
L. rupestris 1 0.572 22.9 88 14.6 0.2396
L. rupestris 2 0.180 7.2 279 50.5 0.2205
L. rupestris 3 0.159 6.4 314 52.5 0.2391
L. rupestris 4 0.581 23.3 87 27.5 0.1253
L. rupestris 5 0.722 28.9 70 13.3 0.2078
L. rupestris 6 0.504 20.2 100 15.8 0.2514
L. rupestris 7 0.159 6.4 314 45.5 0.2759
L. rubripetala 1 0.930 37.2 54 13.8 0.1554
L. rubripetala 2 0.547 21.9 92 19.6 0.1868
L. rubripetala 3 0.792 31.7 64 18.6 0.1357
L. rubripetala 4 0.769 30.8 66 7.6 0.3409
L. rubripetala 5 0.250 10.0 201 20.8 0.3842
L. rubripetala 6 0.636 25.5 79 9.1 0.3465
L. eltoroensis 1 1.797 71.9 28 34.5 0.0323
L. eltoroensis 2 0.450 18.0 112 185.4 0.0240
L. eltoroensis 3 1.850 74.0 28 36.1 0.0300
L. eltoroensis 4 1.609 64.4 32 34.5 0.0360

6.3 Comparing to Orive (1993) estimates

Tremblay & Ackerman (2001) used Orive’s (1993) coalescence model (Table 7 of the paper). The table below computes the Ne/N range per species directly from the NeStage results and compares it to Orive’s estimates:

species_summary <- results %>%
  group_by(Species) %>%
  summarise(
    N_pops   = n(),
    NeN_min  = round(min(NeN),      3),
    NeN_max  = round(max(NeN),      3),
    NeN_mean = round(mean(NeN),     3),
    L_mean   = round(mean(L_months),1),
    .groups  = "drop"
  ) %>%
  mutate(
    NeN_range_NeStage = paste0(NeN_min, "–", NeN_max),
    NeN_range_Orive   = c("0.227–0.360",
                           "0.339–0.445",
                           "0.453–0.473"),
    Species = paste0("*", Species, "*")
  ) %>%
  select(Species, N_pops, NeN_range_NeStage, NeN_range_Orive, NeN_mean, L_mean)

knitr::kable(species_summary,
             col.names = c("Species", "Populations",
                           "Ne/N range (NeStage)", "Ne/N range (Orive 1993)",
                           "Mean Ne/N", "Mean L (months)"),
             escape  = FALSE,
             caption = "Species-level summary of Ne/N estimates from NeStage
                        (Yonezawa 2000 variance Ne, monthly matrices) compared
                        to the coalescence Ne of Orive (1993) reported in
                        Tremblay & Ackerman (2001) Table 7.")
Species-level summary of Ne/N estimates from NeStage (Yonezawa 2000 variance Ne, monthly matrices) compared to the coalescence Ne of Orive (1993) reported in Tremblay & Ackerman (2001) Table 7.
Species Populations Ne/N range (NeStage) Ne/N range (Orive 1993) Mean Ne/N Mean L (months)
L. eltoroensis 4 0.45–1.85 0.227–0.360 1.427 72.6
L. rubripetala 6 0.25–0.93 0.339–0.445 0.654 14.9
L. rupestris 7 0.159–0.722 0.453–0.473 0.411 31.4

The two approaches measure subtly different quantities:

Both measure genetic drift but from different angles. Agreement between the two is reassuring; divergence indicates that mating structure (Orive) or reproductive variance (Yonezawa) is driving the discrepancy.


7 Summary and conservation implications

# Summary plot: Ne/N by species and population
ggplot(results_display, aes(x = factor(Population), y = NeN,
                     color = Species, group = Species)) +
  geom_line(linewidth = 0.8) +
  geom_point(size = 3) +
  geom_hline(yintercept = 0.10, linetype = "dashed",
             color = "grey40", linewidth = 0.7) +
  annotate("text", x = 0.7, y = 0.105,
           label = "Frankham (1995)\nmedian Ne/N = 0.10",
           hjust = 0, size = 3, color = "grey40") +
  scale_color_manual(
    values = c("#2166ac", "#1b7837", "#d6604d"),
    labels = c(
      expression(italic("L. eltoroensis")),
      expression(italic("L. rubripetala")),
      expression(italic("L. rupestris"))
    )
  ) +
  labs(
    title    = expression(paste(N[e]/N, " across populations of three ",
                                italic("Lepanthes"), " species")),
    subtitle = "Monthly transition matrices | Yonezawa (2000) variance Ne",
    x        = "Population",
    y        = expression(N[e]/N),
    color    = "Species",
    caption  = "Dashed line = empirical median across 102 wildlife species (Frankham 1995)."
  ) +
  theme_classic(base_size = 12) +
  theme(
    plot.title    = element_text(face = "bold"),
    legend.text   = element_text(face = "italic"),
    panel.grid.major.y = element_line(color = "grey92", linewidth = 0.4)
  )

# How many populations have Ne < 50 (Wright drift threshold)?
results_display %>%
  mutate(
    Drift_risk = ifelse(Ne_at_census < 50, "HIGH (Ne < 50)", "lower")
  ) %>%
  select(Species, Population, NeN, Ne_at_census, Drift_risk) %>%
  knitr::kable(
    col.names = c("Species", "Pop", "Ne/N", "Estimated Ne", "Drift risk"),
    escape    = FALSE,
    caption   = "Estimated Ne assuming a census size of N = 40, representative
                 of a large Lepanthes population (Tremblay & Ackerman 2001).
                 Ne < 50 indicates populations where genetic drift dominates
                 over natural selection (Wright 1931; Franklin 1980)."
  )
Estimated Ne assuming a census size of N = 40, representative of a large Lepanthes population (Tremblay & Ackerman 2001). Ne < 50 indicates populations where genetic drift dominates over natural selection (Wright 1931; Franklin 1980).
Species Pop Ne/N Estimated Ne Drift risk
L. rupestris 1 0.572 22.9 HIGH (Ne < 50)
L. rupestris 2 0.180 7.2 HIGH (Ne < 50)
L. rupestris 3 0.159 6.4 HIGH (Ne < 50)
L. rupestris 4 0.581 23.3 HIGH (Ne < 50)
L. rupestris 5 0.722 28.9 HIGH (Ne < 50)
L. rupestris 6 0.504 20.2 HIGH (Ne < 50)
L. rupestris 7 0.159 6.4 HIGH (Ne < 50)
L. rubripetala 1 0.930 37.2 HIGH (Ne < 50)
L. rubripetala 2 0.547 21.9 HIGH (Ne < 50)
L. rubripetala 3 0.792 31.7 HIGH (Ne < 50)
L. rubripetala 4 0.769 30.8 HIGH (Ne < 50)
L. rubripetala 5 0.250 10.0 HIGH (Ne < 50)
L. rubripetala 6 0.636 25.5 HIGH (Ne < 50)
L. eltoroensis 1 1.797 71.9 lower
L. eltoroensis 2 0.450 18.0 HIGH (Ne < 50)
L. eltoroensis 3 1.850 74.0 lower
L. eltoroensis 4 1.609 64.4 lower

7.1 Key conservation messages

  1. All three species have very small effective population sizes. Even with the most optimistic Ne/N estimates, census populations of 50–100 individuals — typical for these species — yield Ne well below 50.

  2. The minimum census size for long-term conservation (\(N_e \geq 5{,}000\)) is far larger than any observed population. This confirms the finding of Tremblay & Ackerman (2001) that these orchid populations face substantial genetic vulnerability.

  3. Ne/N varies substantially among populations and species. L. rupestris shows greater among-population variation than the other two species, likely reflecting habitat heterogeneity along the Luquillo river system.

  4. Ne/N estimates from monthly vs annual matrices may differ. When reporting Ne/N, always state the time step of your matrices. Monthly matrices are preferred for species with fine-scale demographic variation.


8 Brief overview of the other two functions

8.1 Ne_clonal_Y2000

Use this function when all reproduction is clonal (\(d_i = 1\) for all stages). This implements Equations 10–11 of Yonezawa et al. (2000):

\[N_e = \frac{N}{(1 - \bar{u^2}) \cdot L}, \quad N_y = \frac{N}{1 - \bar{u^2}}\]

The clonal model is simpler than the general Equation 6 because sexual reproductive variance does not enter. It is the appropriate model for populations maintained entirely by vegetative reproduction (e.g., many ferns, some grasses, clonal shrubs).

Ne_clonal <- Ne_clonal_Y2000(
  T_mat      = MatU,
  F_vec      = MatF[1, ],
  D          = D,
  show_Ny    = TRUE,       # also compute annual effective size
  population = "fully clonal population"
)

8.2 Ne_mixed_Y2000

Use this function when stages differ in their mix of sexual and clonal reproduction (\(0 \leq d_i \leq 1\)). This implements the full Equation 6 of Yonezawa et al. (2000), including the clonal reproductive variance term \(Avr(A\delta)\).

An important property: under Poisson defaults (\(V_c/\bar{c} = V_k/\bar{k} = 1\), \(a = 0\)), the clonal fraction \(d_i\) has no effect on \(N_e/N\). The clonal fraction only influences \(N_e\) when clonal output is more variable than sexual output (\(V_c/\bar{c} > V_k/\bar{k}\)), which is biologically realistic for many clonal plants.

Ne_mixed <- Ne_mixed_Y2000(
  T_mat      = MatU,
  F_vec      = MatF[1, ],
  D          = D,
  d          = c(0.0, 0.0, 0.5),  # stage 3 is 50% clonal
  Vc_over_c  = c(1, 1, 2),        # stage 3 has super-Poisson clonal variance
  population = "mixed orchid"
)

9 Session information

sessionInfo()
#> R version 4.5.2 (2025-10-31)
#> Platform: x86_64-apple-darwin20
#> Running under: macOS Monterey 12.7.6
#> 
#> Matrix products: default
#> BLAS:   /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRblas.0.dylib 
#> LAPACK: /Library/Frameworks/R.framework/Versions/4.5-x86_64/Resources/lib/libRlapack.dylib;  LAPACK version 3.12.1
#> 
#> locale:
#> [1] C/en_US.UTF-8/en_US.UTF-8/C/en_US.UTF-8/en_US.UTF-8
#> 
#> time zone: America/Puerto_Rico
#> tzcode source: internal
#> 
#> attached base packages:
#> [1] stats     graphics  grDevices utils     datasets  methods   base     
#> 
#> other attached packages:
#> [1] knitr_1.51    purrr_1.2.1   tidyr_1.3.2   dplyr_1.2.0   ggplot2_4.0.2
#> [6] expm_1.0-0    Matrix_1.7-4  NeStage_0.8.0
#> 
#> loaded via a namespace (and not attached):
#>  [1] vctrs_0.7.1        cli_3.6.5          rlang_1.1.7        xfun_0.56         
#>  [5] otel_0.2.0         generics_0.1.4     S7_0.2.1           jsonlite_2.0.0    
#>  [9] labeling_0.4.3     glue_1.8.0         htmltools_0.5.9    sass_0.4.10       
#> [13] scales_1.4.0       rmarkdown_2.30     grid_4.5.2         tibble_3.3.1      
#> [17] evaluate_1.0.5     jquerylib_0.1.4    fastmap_1.2.0      yaml_2.3.12       
#> [21] lifecycle_1.0.5    compiler_4.5.2     RColorBrewer_1.1-3 pkgconfig_2.0.3   
#> [25] rstudioapi_0.18.0  farver_2.1.2       lattice_0.22-9     digest_0.6.39     
#> [29] R6_2.6.1           tidyselect_1.2.1   pillar_1.11.1      magrittr_2.0.4    
#> [33] bslib_0.10.0       withr_3.0.2        tools_4.5.2        gtable_0.3.6      
#> [37] cachem_1.1.0

10 References

mirror server hosted at Truenetwork, Russian Federation.