StablePopulation reconstructs age-specific survivorship
profiles under a stable and stationary population assumption. The
central demographic condition is
\[ R_0 = \sum_x l_x m_x = 1, \]
where \(l_x\) is survivorship to age
class \(x\) and \(m_x\) is age-specific fertility. The
package uses consecutive internal class indices
0, 1, 2, ..., n - 1. An external age column can be kept as
a label in Excel workflows, but it does not redefine the internal time
scale.
This vignette gives a complete minimal workflow:
select_beta();R, D,
D_relative, and B;The data below are deliberately small and artificial. They are intended for reproducibility and for understanding the workflow, not as a biological case study.
A minimal analysis needs an age-specific fertility schedule. If observed survivorship is available, it can be used to select the best constrained candidate among a grid of Weibull shape values.
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
)
input_data <- data.frame(
age = age,
mx = mx,
lx_observed = lx_observed
)
knitr::kable(input_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 |
The observed net replacement value for these data is:
In this toy example, the observed schedule is already very close to
the stable and stationary condition. In empirical applications, the
observed value may be different from one. StablePopulation
never rescales fertility silently; if normalisation is a methodological
decision, it should be done explicitly with
normalize_fertility() before the reconstruction.
normalised <- normalize_fertility(
fertility_rates = mx,
lx_reference = lx_observed
)
normalised$R0_reference
#> [1] 1
normalised$check_R0
#> [1] 1The normalised fertility vector is then available as
normalised$fertility_rates_normalized. In the rest of this
vignette we keep the original mx, because the toy data
already satisfy the reference condition.
select_beta()When both fertility and observed survivorship are available, use
select_beta(). For each candidate beta, the
package solves the corresponding Weibull scale parameter
alpha under \(R_0 = 1\),
reconstructs \(l_x\), and calculates
the RMSE against the observed profile.
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.The selected constrained candidate is stored in the main elements of the result:
selection$best_beta
#> [1] 1.1
selection$best_alpha
#> [1] 5.150545
selection$best_RMSE
#> [1] 2.901736e-08The age-by-age comparison between the observed and reconstructed
profiles is in best_profile.
| 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 |
The complete grid is retained in results, so the
selected value can be checked against the neighbouring candidates.
selected_neighbourhood <- selection$results[
selection$results$beta >= selection$best_beta - 0.10 &
selection$results$beta <= selection$best_beta + 0.10,
c("beta", "alpha", "R0", "RMSE", "selected")
]
knitr::kable(selected_neighbourhood, digits = 6)| beta | alpha | R0 | RMSE | selected | |
|---|---|---|---|---|---|
| 11 | 1.00 | 5.375388 | 1 | 0.011204 | FALSE |
| 12 | 1.05 | 5.256243 | 1 | 0.005534 | FALSE |
| 13 | 1.10 | 5.150545 | 1 | 0.000000 | TRUE |
| 14 | 1.15 | 5.056195 | 1 | 0.005402 | FALSE |
| 15 | 1.20 | 4.971506 | 1 | 0.010677 | FALSE |
If the selected beta is the first or last value in the
scanned range, select_beta() issues a warning and records a
boundary note in the returned object. This is a diagnostic cue: it does
not mean that the result is invalid, but it does mean that the scanned
range should be extended deliberately before interpreting the selected
value as an internal optimum.
The reconstructed survivorship profile can be converted into the demographic quantities used by downstream population and ecological interpretation.
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.The resulting table contains:
lx: survivorship;stable_structure: the normalised stable age structure
\(R_x\);mortality_profile: the raw exit-by-death profile \(D_x\);mortality_profile_relative: the separately normalised
version of D;conditional_survival: survival between consecutive
classes \(B_x\);lxmx: the contribution of each class to \(R_0\).| 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 |
These checks are useful when explaining the output:
demographic_profile$checks
#> $stable_structure_sum
#> [1] 1
#>
#> $mortality_profile_sum
#> [1] 0.2515594
#>
#> $expected_mortality_profile_sum
#> [1] 0.2515594
#>
#> $mortality_profile_relative_sum
#> [1] 1stable_structure sums to one.
mortality_profile_relative also sums to one, because it is
explicitly normalised. The raw mortality_profile is
different: its total equals the first value of the stable age
structure.
fit_weibull_free() provides a descriptive two-parameter
Weibull fit to the observed survivorship profile. It does not impose
\(R_0 = 1\). If fertility is supplied,
the implied \(R_0\) is reported after
fitting.
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.The comparison is useful, but it answers a different question.
select_beta() selects among constrained candidates
compatible with the stable-population condition;
fit_weibull_free() estimates both Weibull parameters freely
and only then reports the demographic consequence.
| age | lx_observed | lx_fitted | residual | squared_error | fertility_rates | lxmx |
|---|---|---|---|---|---|---|
| 0 | 1.0000 | 1.0000 | 0 | 0 | 0.00 | 0.0000 |
| 1 | 0.8481 | 0.8481 | 0 | 0 | 0.00 | 0.0000 |
| 2 | 0.7024 | 0.7024 | 0 | 0 | 0.30 | 0.2107 |
| 3 | 0.5759 | 0.5759 | 0 | 0 | 0.75 | 0.4319 |
| 4 | 0.4690 | 0.4690 | 0 | 0 | 0.60 | 0.2814 |
| 5 | 0.3799 | 0.3799 | 0 | 0 | 0.20 | 0.0760 |
scan_beta()When observed survivorship is unavailable, the package should not
pretend that a single empirical optimum has been selected. In that case,
use scan_beta() to build a family of constrained candidate
profiles. A terminal-survivorship window can be used to retain scenarios
whose final class remains within a stated interval.
scan <- scan_beta(
fertility_rates = mx,
beta_values = seq(0.50, 1.80, by = 0.05),
terminal_window = c(0.35, 0.39)
)
scan
#> Stable Population Scan (Weibull, R0 = 1)
#> -----------------------------------------
#> Beta values scanned: 27
#> Numerically stable: 27
#> Admissible: 6
#> Terminal window: [0.35, 0.39]
#> Age classes: 6
#>
#> Use $summary for all candidates, $admissible_summary for retained ones.The retained candidates are stored in
admissible_summary.
| beta | alpha | R0 | residual | lx_terminal | stable | terminal_admissible | admissible | status | |
|---|---|---|---|---|---|---|---|---|---|
| 12 | 1.05 | 5.256243 | 1 | 0 | 0.387176 | TRUE | TRUE | TRUE | converged |
| 13 | 1.10 | 5.150545 | 1 | 0 | 0.379882 | TRUE | TRUE | TRUE | converged |
| 14 | 1.15 | 5.056195 | 1 | 0 | 0.372608 | TRUE | TRUE | TRUE | converged |
| 15 | 1.20 | 4.971506 | 1 | 0 | 0.365357 | TRUE | TRUE | TRUE | converged |
| 16 | 1.25 | 4.895107 | 1 | 0 | 0.358131 | TRUE | TRUE | TRUE | converged |
| 17 | 1.30 | 4.825871 | 1 | 0 | 0.350933 | TRUE | TRUE | TRUE | converged |
When a terminal window is active,
terminal_extremes$first and
terminal_extremes$last are the first and last admissible
candidates in ascending beta order. They are scenario endpoints, not
automatically minima or maxima of all downstream quantities.
If a value of beta has already been justified
externally, the direct route is
reconstruct_population().
fixed <- reconstruct_population(
fertility_rates = mx,
beta = 1.10
)
fixed
#> Stable Population Reconstruction (Weibull, R0 = 1)
#> ---------------------------------------------------
#> alpha: 5.15055
#> beta: 1.1
#> R0: 1
#> Stable: TRUE
#> Age classes: 6
#>
#> Use $table for the full age-specific profile.
knitr::kable(fixed$table, digits = 4)| age | fertility_rates | lx | lxmx |
|---|---|---|---|
| 0 | 0.00 | 1.0000 | 0.0000 |
| 1 | 0.00 | 0.8481 | 0.0000 |
| 2 | 0.30 | 0.7024 | 0.2107 |
| 3 | 0.75 | 0.5759 | 0.4319 |
| 4 | 0.60 | 0.4690 | 0.2814 |
| 5 | 0.20 | 0.3799 | 0.0760 |
This route solves alpha under \(R_0 = 1\) for the chosen beta.
It is useful for sensitivity analysis, for reproducing a predefined
scenario, or for using a shape value selected outside the current
dataset.
The same reconstruction logic can be applied to external workbooks
with run_reconstruction_excel(). A typical input sheet
contains columns such as:
| Age | mx | lx | Beta |
|---|---|---|---|
| 0 | 0.00 | 1.0000 | 1.1 |
| 1 | 0.00 | 0.8481 | NA |
| 2 | 0.30 | 0.7024 | NA |
| 3 | 0.75 | 0.5759 | NA |
| 4 | 0.60 | 0.4690 | NA |
| 5 | 0.20 | 0.3799 | NA |
The function can then be called as:
run_reconstruction_excel(
input_file = "demography.xlsx",
output_file = "demography_results.xlsx",
mode = "auto",
beta_values = seq(0.05, 3.00, by = 0.05),
terminal_window = c(0.0001, 0.05),
output_detail = "standard"
)In mode = "auto", the route is inferred from the
available columns. For example, observed survivorship supports the
selection route, while a single fixed value in a Beta
column supports the fixed-beta route when observed survivorship is
absent. The Excel workflow uses the fertility values supplied in the
workbook; explicit normalisation should therefore be done before saving
the input if the analysis is intended to use a rescaled fertility
schedule.
The package separates three analytical situations:
| Situation | Function | Interpretation |
|---|---|---|
| Fertility plus observed survivorship | select_beta() |
Selects the constrained candidate with the smallest RMSE. |
| Fertility without observed survivorship | scan_beta() |
Retains a family of constrained scenarios. |
| A fixed beta is already justified | reconstruct_population() |
Reconstructs one constrained profile for that beta. |
fit_weibull_free() is useful as a descriptive reference,
but it is not the same model as the constrained StablePopulation
reconstruction. The distinction is important when reporting methods,
comparing results, or linking the package to downstream ecological
analyses.