Function guide and demographic meaning

library(StablePopulation)

Aim of this guide

This vignette explains the main exported functions in StablePopulation from both a computational and a demographic perspective. It is intended as a companion to the workflow vignette. The workflow vignette shows one complete analysis from start to finish; this guide explains what each function is for, what question it answers, and what should not be inferred from its output.

The common demographic rule behind the constrained functions is

\[ R_0 = \sum_x l_x m_x = 1, \]

where lx is survivorship to age class x and mx is age-specific fertility. All functions use consecutive internal class indices 0, 1, 2, ..., n - 1.

We use the same small artificial example throughout.

age <- 0:5
mx <- c(0, 0, 0.30, 0.75, 0.60, 0.20)
lx_observed <- c(1.0000000, 0.8480619, 0.7023945, 0.5759026, 0.4689639, 0.3798816)

example_data <- data.frame(age = age, mx = mx, lx_observed = lx_observed)
knitr::kable(example_data, digits = 4)
age mx lx_observed
0 0.00 1.0000
1 0.00 0.8481
2 0.30 0.7024
3 0.75 0.5759
4 0.60 0.4690
5 0.20 0.3799

weibull_survival()

Question answered

For a given pair of Weibull parameters, what survivorship value is predicted at each age class?

Role in the package

weibull_survival() is the low-level mathematical function used by the higher level reconstruction routes. It evaluates

\[ l_x = \exp\left[-\left(\frac{x}{\alpha}\right)^\beta\right]. \]

lx_candidate <- weibull_survival(
  alpha = 4.5,
  beta = 1.2,
  age = age
)

knitr::kable(
  data.frame(age = age, lx_candidate = lx_candidate),
  digits = 4
)
age lx_candidate
0 1.0000
1 0.8483
2 0.6853
3 0.5408
4 0.4197
5 0.3215

Interpretation

This function does not use fertility and does not impose R0 = 1. It simply returns a survivorship curve for supplied values of alpha and beta.

calculate_population()

Question answered

Given alpha, beta, and fertility, what survivorship profile and total births are implied?

Role in the package

This historical helper returns the Weibull survivorship vector under the name population and the resulting sum births = sum(population * fertility_rates).

calculated <- calculate_population(
  alpha = 4.5,
  beta = 1.2,
  fertility_rates = mx
)

calculated$births
#> [1] 0.9272999

Interpretation

The returned population is cumulative survivorship lx, not the normalized stable age structure R. Use derive_demographic_profile() when R, D, and B are needed.

find_alphas()

Question answered

For one selected beta, which alpha makes the fertility schedule satisfy sum(lx * mx) = 1?

alpha_for_beta <- find_alphas(
  beta = 1.2,
  fertility_rates = mx
)

alpha_for_beta
#> [1] 4.971506

Interpretation

This is a root-finding step. It is useful for understanding the historical algorithm, but ordinary analyses should usually use reconstruct_population(), scan_beta(), or select_beta(), because those functions also return clearer diagnostics and output tables.

reconstruct_population()

Question answered

If beta is fixed in advance, what constrained Weibull profile is obtained?

reconstruction <- reconstruct_population(
  fertility_rates = mx,
  beta = 1.20
)

reconstruction
#> Stable Population Reconstruction (Weibull, R0 = 1)
#> ---------------------------------------------------
#>   alpha: 4.97151 
#>   beta:      1.2 
#>   R0:            1 
#>   Stable: TRUE 
#>   Age classes: 6 
#> 
#> Use $table for the full age-specific profile.
knitr::kable(reconstruction$table, digits = 4)
age fertility_rates lx lxmx
0 0.00 1.0000 0.0000
1 0.00 0.8642 0.0000
2 0.30 0.7151 0.2145
3 0.75 0.5796 0.4347
4 0.60 0.4629 0.2777
5 0.20 0.3654 0.0731

Interpretation

This is the fixed-beta route. It should be used when beta is already justified by an external argument or when a particular value is being examined as an explicit scenario. The function solves alpha under R0 = 1 and returns the reconstructed lx.

scan_beta()

Question answered

Which constrained candidate profiles are possible across a grid of beta values?

scan <- scan_beta(
  fertility_rates = mx,
  beta_values = seq(0.50, 1.80, by = 0.10)
)

scan
#> Stable Population Scan (Weibull, R0 = 1)
#> -----------------------------------------
#>   Beta values scanned: 14 
#>   Numerically stable:  14 
#>   Admissible:          14 
#>   Age classes: 6 
#> 
#> Use $summary for all candidates, $admissible_summary for retained ones.
knitr::kable(head(scan$summary), digits = 4)
beta alpha R0 residual lx_terminal stable terminal_admissible admissible status
0.5 8.6742 1 0 0.4680 TRUE NA TRUE converged
0.6 7.3882 1 0 0.4533 TRUE NA TRUE converged
0.7 6.5908 1 0 0.4386 TRUE NA TRUE converged
0.8 6.0520 1 0 0.4239 TRUE NA TRUE converged
0.9 5.6653 1 0 0.4092 TRUE NA TRUE converged
1.0 5.3754 1 0 0.3945 TRUE NA TRUE converged

Interpretation

scan_beta() is the scenario route. It does not select a single empirical optimum. It returns a family of profiles, all solved under R0 = 1 when the solver converges.

A terminal survivorship window can be used when the analysis is framed around a final-class criterion.

terminal_scan <- scan_beta(
  fertility_rates = mx,
  beta_values = seq(0.50, 1.80, by = 0.10),
  terminal_window = c(0.10, 0.40)
)

knitr::kable(terminal_scan$admissible_summary, digits = 4)
beta alpha R0 residual lx_terminal stable terminal_admissible admissible status
6 1.0 5.3754 1 0 0.3945 TRUE TRUE TRUE converged
7 1.1 5.1505 1 0 0.3799 TRUE TRUE TRUE converged
8 1.2 4.9715 1 0 0.3654 TRUE TRUE TRUE converged
9 1.3 4.8259 1 0 0.3509 TRUE TRUE TRUE converged
10 1.4 4.7053 1 0 0.3366 TRUE TRUE TRUE converged
11 1.5 4.6040 1 0 0.3225 TRUE TRUE TRUE converged
12 1.6 4.5179 1 0 0.3085 TRUE TRUE TRUE converged
13 1.7 4.4439 1 0 0.2947 TRUE TRUE TRUE converged
14 1.8 4.3796 1 0 0.2810 TRUE TRUE TRUE converged

The terminal window filters scenarios. It does not estimate a true beta and it does not create a central result.

select_beta()

Question answered

When observed survivorship is available, which constrained candidate profile is closest to it?

selection <- select_beta(
  fertility_rates = mx,
  lx_observed = lx_observed,
  beta_values = seq(0.50, 1.80, by = 0.05)
)

selection
#> Stable Population Selection (Weibull, R0 = 1, best RMSE)
#> ---------------------------------------------------------
#>   Best beta:      1.1 
#>   Best alpha: 5.15055 
#>   R0:                 1 
#>   RMSE:       2.902e-08 
#>   Age classes: 6 
#>   Candidates evaluated: 27 
#> 
#> Use $best_profile for the selected profile, $results for all candidates.
selection$best_beta
#> [1] 1.1
selection$best_alpha
#> [1] 5.150545
selection$best_RMSE
#> [1] 2.901736e-08
knitr::kable(selection$best_profile, digits = 4)
age fertility_rates lx_observed lx_reconstructed residual squared_error lxmx
0 0.00 1.0000 1.0000 0 0 0.0000
1 0.00 0.8481 0.8481 0 0 0.0000
2 0.30 0.7024 0.7024 0 0 0.2107
3 0.75 0.5759 0.5759 0 0 0.4319
4 0.60 0.4690 0.4690 0 0 0.2814
5 0.20 0.3799 0.3799 0 0 0.0760

Interpretation

select_beta() scans a grid of beta values. For each beta, alpha is derived from the stability condition. The selected profile is the grid candidate with the lowest RMSE against lx_observed. This is therefore not a free two-parameter Weibull fit.

If the selected beta is exactly at the lower or upper boundary of the scanned range, the result should be treated as a diagnostic cue to extend the beta grid before interpreting the selected value.

selection$beta_at_boundary
#> [1] FALSE
selection$beta_boundary_note
#> [1] NA

normalize_fertility()

Question answered

How should a fertility schedule be rescaled if the analysis is deliberately expressed relative to R0 = 1 for a reference survivorship profile?

normalised <- normalize_fertility(
  fertility_rates = mx,
  lx_reference = lx_observed
)

normalised
#> Normalized Fertility Schedule
#> -----------------------------
#>   R0 (reference):       1 
#>   Scaling factor:       1 
#>   Check R0 (after normalization):         1 
#>   Age classes: 6 
#> 
#> Use $fertility_rates_normalized for the adjusted schedule.

Interpretation

Normalisation is explicit. It multiplies every fertility value by the same factor and preserves the age pattern of fertility. Neither select_beta() nor run_reconstruction_excel() normalises fertility silently.

derive_demographic_profile()

Question answered

Once lx has been reconstructed, what stable age structure and mortality profiles follow from it?

demographic_profile <- derive_demographic_profile(
  lx = selection$best_lx,
  fertility_rates = mx
)

demographic_profile
#> Stable Population Demographic Profile
#> --------------------------------------
#>   Age classes: 6 
#>   R0:         1 
#>   Stable R0: TRUE 
#>   sum(R):         1 
#>   sum(D): 0.25155938  (expected: 0.25155938 )
#>   sum(D_relative):         1 
#> 
#> Use $table for the full profile with R, D, D_relative and B.
knitr::kable(demographic_profile$table, digits = 4)
age fertility_rates lx stable_structure mortality_profile mortality_profile_relative conditional_survival lxmx
0 0.00 1.0000 0.2516 0.0382 0.1519 0.8481 0.0000
1 0.00 0.8481 0.2133 0.0366 0.1457 0.8282 0.0000
2 0.30 0.7024 0.1767 0.0318 0.1265 0.8199 0.2107
3 0.75 0.5759 0.1449 0.0269 0.1069 0.8143 0.4319
4 0.60 0.4690 0.1180 0.0224 0.0891 0.8100 0.2814
5 0.20 0.3799 0.0956 0.0956 0.3799 NA 0.0760

Interpretation

This function creates the demographic bridge from survivorship to downstream eco-demographic quantities:

  • R or stable_structure: normalized stable age structure;
  • D or mortality_profile: raw exit-by-death profile;
  • D_relative: separately normalized death profile;
  • B or conditional_survival: survival between consecutive classes.

The raw D and the relative D are not the same object. This distinction is important when later ecological layers combine mortality with body mass or species abundance.

fit_weibull_free()

Question answered

What two-parameter Weibull curve best describes the observed survivorship, without imposing the stable-population constraint?

free_fit <- fit_weibull_free(
  lx_observed = lx_observed,
  fertility_rates = mx
)

free_fit
#> Free Weibull Fit (unconstrained, descriptive reference)
#> -------------------------------------------------------
#>   alpha:     5.15055 
#>   beta:          1.1 
#>   RMSE:      8.666e-08 
#>   Converged: TRUE 
#>   R0 (implied):       1 
#>   Optimization starts: 12 ( 12 converged )
#> 
#> Use $profile for the fitted profile, $attempts for all starts.
free_fit$R0_fitted
#> [1] 1

Interpretation

This is a descriptive reference fit. Both alpha and beta are estimated freely. If fertility is supplied, the implied R0 is reported after the fit. It should not be confused with the constrained route used by select_beta().

run_reconstruction_excel() and run_analysis()

Question answered

How can the same reconstruction routes be applied to Excel workbooks?

run_reconstruction_excel() is the recommended external-workbook interface. In mode = "auto", each sheet is routed according to the available columns:

Input columns Route used
fertility plus observed survivorship select
fertility plus one fixed beta value fixed
fertility only scan

run_analysis() is the historical source-tree workflow retained for compatibility with version 1.0.3. New analyses should normally use run_reconstruction_excel().

Practical route summary

Scientific situation Recommended function
One beta is already justified reconstruct_population()
Observed survivorship is available select_beta()
Only fertility is available scan_beta()
Fertility must be explicitly rescaled normalize_fertility() first
A fitted curve is needed only as a descriptive reference fit_weibull_free()
Stable structure and death profiles are needed derive_demographic_profile()
Data are stored in Excel sheets run_reconstruction_excel()