The goal of vital is to allow analysis of demographic data using tidy tools. It works with other tidyverse packages such as dplyr and ggplot2. It also works with the tidyverts packages, tsibble and fable.
vital objects
The basic data object is a vital, which is a
time-indexed tibble that contains vital statistics such as births,
deaths, population counts, and mortality and fertility rates.
We will use Norwegian data in the following examples. First, let’s remove the “Total” Sex category and collapse the upper ages into a final age group of 100+.
nor <- norway_mortality |>
filter(Sex != "Total") |>
collapse_ages(max_age = 100)
nor
#> # A vital: 25,048 x 7 [1Y]
#> # Key: Age x Sex [101 x 2]
#> Year Age OpenInterval Sex Population Deaths Mortality
#> <int> <int> <lgl> <chr> <dbl> <dbl> <dbl>
#> 1 1900 0 FALSE Female 30070 2376. 0.0778
#> 2 1900 1 FALSE Female 28960 842 0.0290
#> 3 1900 2 FALSE Female 28043 348 0.0123
#> 4 1900 3 FALSE Female 27019 216. 0.00786
#> 5 1900 4 FALSE Female 26854 168. 0.00624
#> 6 1900 5 FALSE Female 25569 140. 0.00538
#> 7 1900 6 FALSE Female 25534 108. 0.00422
#> 8 1900 7 FALSE Female 24314 93.5 0.00376
#> 9 1900 8 FALSE Female 24979 93.5 0.00380
#> 10 1900 9 FALSE Female 24428 90 0.00365
#> # ℹ 25,038 more rowsThis example contains data from 1900 to 2023. There are 101 age groups and 2 Sex categories. A vital must have a time “index” variable, and optionally other categorical variables known as “key” variables. Each row must have a unique combination of the index and key variables. Some columns are “vital” variables, such as “Age” and “Sex”.
We can use functions to see which variables are index, key or vital:
index_var(nor)
#> [1] "Year"
key_vars(nor)
#> [1] "Age" "Sex"
vital_vars(nor)
#> age sex deaths population
#> "Age" "Sex" "Deaths" "Population"Plots
There are autoplot() functions for plotting
vital objects. These produce rainbow plots (Hyndman and Shang 2010) where each line
represents data for one year, and the variable is plotted against
age.
nor |>
autoplot(Mortality) +
scale_y_log10()
We can use standard ggplot functions to modify the plot as desired. For example, here are population pyramids for all years.
nor |>
mutate(Population = if_else(Sex == "Female", -Population, Population)) |>
autoplot(Population) +
coord_flip() +
facet_grid(. ~ Sex, scales = "free_x")
Life tables and life expectancy
Life tables (Chiang 1984) can be
produced using the life_table() function. It will produce
life tables for each unique combination of the index and key variables
other than age.
# Life tables for males and females in Norway in 2000
nor |>
filter(Year == 2000) |>
life_table()
#> # A vital: 202 x 13 [?]
#> # Key: Age x Sex [101 x 2]
#> Year Age Sex mx qx lx dx Lx Tx ex rx nx
#> <int> <int> <chr> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 2000 0 Fema… 3.28e-3 3.27e-3 1 3.27e-3 0.997 81.4 81.4 0.997 1
#> 2 2000 1 Fema… 2.77e-4 2.77e-4 0.997 2.76e-4 0.997 80.4 80.6 1.000 1
#> 3 2000 2 Fema… 3.09e-4 3.09e-4 0.996 3.08e-4 0.996 79.4 79.7 1.000 1
#> 4 2000 3 Fema… 1.33e-4 1.33e-4 0.996 1.32e-4 0.996 78.4 78.7 1.000 1
#> 5 2000 4 Fema… 1.68e-4 1.68e-4 0.996 1.67e-4 0.996 77.4 77.7 1.000 1
#> 6 2000 5 Fema… 3.30e-5 3.30e-5 0.996 3.29e-5 0.996 76.4 76.7 1.000 1
#> 7 2000 6 Fema… 1.35e-4 1.35e-4 0.996 1.34e-4 0.996 75.4 75.7 1.000 1
#> 8 2000 7 Fema… 1.68e-4 1.68e-4 0.996 1.67e-4 0.996 74.4 74.7 1.000 1
#> 9 2000 8 Fema… 6.70e-5 6.70e-5 0.996 6.67e-5 0.995 73.4 73.7 1.000 1
#> 10 2000 9 Fema… 1.97e-4 1.97e-4 0.995 1.96e-4 0.995 72.4 72.7 1.000 1
#> # ℹ 192 more rows
#> # ℹ 1 more variable: ax <dbl>Life expectancy
(
with
by default) is computed using life_expectancy():
# Life expectancy for males and females in Norway
nor |>
life_expectancy() |>
ggplot(aes(x = Year, y = ex, color = Sex)) +
geom_line()
Smoothing
Several smoothing functions are provided:
smooth_spline(), smooth_mortality(),
smooth_fertility(), and smooth_loess(), each
smoothing across the age variable for each year. The methods used in
smooth_mortality() and smooth_fertility() are
described in Hyndman and Ullah (2007). The
smooth_mortality_law() function instead fits a parametric
mortality law (Gompertz by default) using the MortalityLaws package.
# Smoothed data
nor |>
filter(Year == 1967) |>
smooth_mortality(Mortality) |>
autoplot(Mortality) +
geom_line(aes(y = .smooth), col = "#0072B2") +
ylab("Mortality rate") +
scale_y_log10()
Lee-Carter models
Lee-Carter models (Lee and Carter 1992)
are estimated using the LC() function which must be called
within a model() function:
# Lee-Carter model
lc <- nor |>
model(lee_carter = LC(log(Mortality)))
lc
#> # A mable: 2 x 2
#> # Key: Sex [2]
#> Sex lee_carter
#> <chr> <model>
#> 1 Female <LC>
#> 2 Male <LC>Models are fitted for all combinations of key variables excluding
age. To see the details for a specific model, use the
report() function.
lc |>
filter(Sex == "Female") |>
report()
#> Series: Mortality
#> Model: LC
#> Transformation: log(Mortality)
#>
#> Options:
#> Adjust method: dt
#> Jump choice: fit
#>
#> Age functions
#> # A tibble: 101 × 3
#> Age ax bx
#> <int> <dbl> <dbl>
#> 1 0 -4.33 0.0151
#> 2 1 -6.16 0.0218
#> 3 2 -6.88 0.0197
#> 4 3 -7.20 0.0190
#> 5 4 -7.35 0.0177
#> # ℹ 96 more rows
#>
#> Time coefficients
#> # A tsibble: 124 x 2 [1Y]
#> Year kt
#> <int> <dbl>
#> 1 1900 118.
#> 2 1901 112.
#> 3 1902 105.
#> 4 1903 111.
#> 5 1904 109.
#> # ℹ 119 more rows
#>
#> Time series model: RW w/ drift
#>
#> Variance explained: 92.39%The results can be plotted.
autoplot(lc)
The components can be extracted.
age_components(lc)
#> # A tibble: 202 × 4
#> Sex Age ax bx
#> <chr> <int> <dbl> <dbl>
#> 1 Female 0 -4.33 0.0151
#> 2 Female 1 -6.16 0.0218
#> 3 Female 2 -6.88 0.0197
#> 4 Female 3 -7.20 0.0190
#> 5 Female 4 -7.35 0.0177
#> 6 Female 5 -7.53 0.0179
#> 7 Female 6 -7.63 0.0177
#> 8 Female 7 -7.73 0.0173
#> 9 Female 8 -7.75 0.0163
#> 10 Female 9 -7.83 0.0168
#> # ℹ 192 more rows
time_components(lc)
#> # A tsibble: 248 x 3 [1Y]
#> # Key: Sex [2]
#> Sex Year kt
#> <chr> <int> <dbl>
#> 1 Female 1900 118.
#> 2 Female 1901 112.
#> 3 Female 1902 105.
#> 4 Female 1903 111.
#> 5 Female 1904 109.
#> 6 Female 1905 113.
#> 7 Female 1906 104.
#> 8 Female 1907 108.
#> 9 Female 1908 108.
#> 10 Female 1909 102.
#> # ℹ 238 more rowsForecasts are obtained using the forecast() function
# Forecasts from Lee-Carter model
lc |>
forecast(h = 20)
#> # A vital fable: 4,040 x 6 [1Y]
#> # Key: Age x (Sex, .model) [101 x 2]
#> Sex .model Year Age Mortality .mean
#> <chr> <chr> <int> <int> <dist> <dbl>
#> 1 Female lee_carter 2024 0 t(N(-6.8, 0.0087)) 0.00113
#> 2 Female lee_carter 2025 0 t(N(-6.8, 0.017)) 0.00110
#> 3 Female lee_carter 2026 0 t(N(-6.9, 0.026)) 0.00106
#> 4 Female lee_carter 2027 0 t(N(-6.9, 0.035)) 0.00103
#> 5 Female lee_carter 2028 0 t(N(-6.9, 0.045)) 0.00100
#> 6 Female lee_carter 2029 0 t(N(-7, 0.054)) 0.000973
#> 7 Female lee_carter 2030 0 t(N(-7, 0.064)) 0.000944
#> 8 Female lee_carter 2031 0 t(N(-7, 0.073)) 0.000917
#> 9 Female lee_carter 2032 0 t(N(-7.1, 0.083)) 0.000890
#> 10 Female lee_carter 2033 0 t(N(-7.1, 0.093)) 0.000864
#> # ℹ 4,030 more rowsThe forecasts are returned as a distribution column (here transformed
normal because of the log transformation used in the model). The
.mean column gives the point forecasts equal to the mean of
the distribution column.
Functional data models
Functional data models (Hyndman and Ullah
2007) can be estimated in a similar way to Lee-Carter models, but
with an additional smoothing step, then modelling with LC
replaced by FDM.
# FDM model
fdm <- nor |>
smooth_mortality(Mortality) |>
model(hu = FDM(log(.smooth)))
fc_fdm <- fdm |>
forecast(h = 20)
autoplot(fc_fdm) +
scale_y_log10()
Functional data models have multiple principal components, rather than the single factor used in Lee-Carter models.
fdm |>
autoplot(show_order = 3)
By default, six factors are estimated using FDM(). Here
we have chosen to plot only the first three.
The components can be extracted.
age_components(fdm)
#> # A tibble: 202 × 9
#> Sex Age mean phi1 phi2 phi3 phi4 phi5 phi6
#> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Female 0 -4.34 0.133 0.257 0.0933 -0.0790 -0.0626 0.0496
#> 2 Female 1 -6.13 0.185 0.232 -0.00843 -0.243 -0.145 0.392
#> 3 Female 2 -6.82 0.167 0.192 -0.0932 -0.239 0.00395 0.0676
#> 4 Female 3 -7.15 0.159 0.187 -0.158 -0.252 0.0838 -0.0257
#> 5 Female 4 -7.36 0.156 0.171 -0.209 -0.256 0.110 -0.119
#> 6 Female 5 -7.50 0.153 0.145 -0.232 -0.240 0.120 -0.229
#> 7 Female 6 -7.60 0.150 0.113 -0.241 -0.167 0.0931 -0.246
#> 8 Female 7 -7.69 0.148 0.0761 -0.251 -0.0526 0.0317 -0.158
#> 9 Female 8 -7.75 0.146 0.0372 -0.262 0.0586 -0.0442 -0.0344
#> 10 Female 9 -7.78 0.144 -0.00131 -0.266 0.142 -0.120 0.0639
#> # ℹ 192 more rows
time_components(fdm)
#> # A tsibble: 248 x 9 [1Y]
#> # Key: Sex [2]
#> Sex Year mean beta1 beta2 beta3 beta4 beta5 beta6
#> <chr> <int> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl> <dbl>
#> 1 Female 1900 1 13.7 0.0205 -0.564 0.109 -0.0526 -0.313
#> 2 Female 1901 1 13.0 -0.195 -0.790 0.0667 -0.0868 -0.300
#> 3 Female 1902 1 12.7 -0.488 -0.541 -0.0327 -0.109 -0.497
#> 4 Female 1903 1 13.4 -0.426 -0.747 -0.00132 -0.0597 -0.160
#> 5 Female 1904 1 13.1 -0.483 -0.546 0.188 -0.198 -0.403
#> 6 Female 1905 1 13.5 -0.395 -0.640 -0.0218 0.0109 -0.166
#> 7 Female 1906 1 13.0 -0.881 -0.822 0.0824 -0.105 -0.235
#> 8 Female 1907 1 13.1 -0.500 -0.335 0.279 -0.0908 -0.237
#> 9 Female 1908 1 13.0 -0.417 -0.636 0.114 -0.153 -0.207
#> 10 Female 1909 1 12.3 -0.337 -0.753 0.0418 -0.0990 -0.368
#> # ℹ 238 more rowsCoherent functional data models
A coherent functional data model (Hyndman et al. 2013), is obtained by first computing the sex-products and sex-ratios of the smoothed mortality data. Then a functional data model is fitted to the smoothed data, forecasts are obtained, and the product/ratio transformation is reversed. The following code shows the steps.
fdm_coherent <- nor |>
smooth_mortality(Mortality) |>
make_pr(.smooth) |>
model(hby = FDM(log(.smooth), coherent = TRUE))
fc_coherent <- fdm_coherent |>
forecast(h = 20) |>
undo_pr(.smooth)
fc_coherent
#> # A vital fable: 4,040 x 6 [1Y]
#> # Key: Age x (Sex, .model) [101 x 2]
#> Year Age Sex .model .smooth .mean
#> <int> <int> <chr> <chr> <dist> <dbl>
#> 1 2024 0 Female hby sample[2000] 0.00202
#> 2 2024 1 Female hby sample[2000] 0.000274
#> 3 2024 2 Female hby sample[2000] 0.000163
#> 4 2024 3 Female hby sample[2000] 0.000126
#> 5 2024 4 Female hby sample[2000] 0.000109
#> 6 2024 5 Female hby sample[2000] 0.0000991
#> 7 2024 6 Female hby sample[2000] 0.0000938
#> 8 2024 7 Female hby sample[2000] 0.0000911
#> 9 2024 8 Female hby sample[2000] 0.0000923
#> 10 2024 9 Female hby sample[2000] 0.0000955
#> # ℹ 4,030 more rowsHere, make_pr() makes the product-ratios, while
undo_pr() undoes them.
The argument coherent = TRUE in FDM()
ensures that the time series models fitted to the coefficients of the
sex-ratios are stationary (ARFIMA models by default), so the forecast
ratios do not diverge. The coefficients of the geometric mean are
modelled with ARIMA models by default.
Other models
The FMEAN() and FNAIVE() functions provide
simple benchmark models. FMEAN() forecasts each age using
the mean of the observed values at that age, while FNAIVE()
uses a random walk from the last observation at each age. Here,
FNAIVE() warns that some young ages have zero mortality in
the final year, so the last positive rate at those ages is used
instead.
benchmarks <- nor |>
filter(Year > 1990) |>
model(mean = FMEAN(log(Mortality)), naive = FNAIVE(log(Mortality)))
#> Warning: Values are zero or missing in the final year for ages 10, 13. Using
#> the last finite value as the starting point for these ages.
benchmarks |>
forecast(h = 10)
#> # A vital fable: 4,040 x 6 [1Y]
#> # Key: Age x (Sex, .model) [101 x 4]
#> Sex .model Year Age Mortality .mean
#> <chr> <chr> <int> <int> <dist> <dbl>
#> 1 Female mean 2024 0 t(N(-5.9, 0.13)) 0.00285
#> 2 Female mean 2025 0 t(N(-5.9, 0.13)) 0.00285
#> 3 Female mean 2026 0 t(N(-5.9, 0.13)) 0.00285
#> 4 Female mean 2027 0 t(N(-5.9, 0.13)) 0.00285
#> 5 Female mean 2028 0 t(N(-5.9, 0.13)) 0.00285
#> 6 Female mean 2029 0 t(N(-5.9, 0.13)) 0.00285
#> 7 Female mean 2030 0 t(N(-5.9, 0.13)) 0.00285
#> 8 Female mean 2031 0 t(N(-5.9, 0.13)) 0.00285
#> 9 Female mean 2032 0 t(N(-5.9, 0.13)) 0.00285
#> 10 Female mean 2033 0 t(N(-5.9, 0.13)) 0.00285
#> # ℹ 4,030 more rowsGeneralized age-period-cohort (GAPC) models (Villegas et al. 2018) are fitted using the
StMoMo package. These include LC2() (an alternative
Lee-Carter model that allows zero rates), CBD(),
APC(), RH(), M7(),
PLAT(), and the general GAPC() function. They
are fitted to the deaths and population variables, and require annual
data. For example, the Cairns-Blake-Dowd model is designed for older
ages.

The forecast() function gives only point forecasts for
GAPC models, unless simulate = TRUE is used to obtain
forecast distributions from simulated future paths.
cbd |>
forecast(h = 10, simulate = TRUE, times = 500) |>
filter(Sex == "Female", Age == 80) |>
mutate(pi = distributional::hilo(Mortality, 95))
#> # A vital fable: 10 x 7 [1Y]
#> # Key: Age x (Sex, .model) [1 x 1]
#> Sex .model Year Age Mortality .mean pi
#> <chr> <chr> <int> <int> <dist> <dbl> <hilo>
#> 1 Female cbd 2024 80 sample[500] 0.0367 [0.03503988, 0.03842375]95
#> 2 Female cbd 2025 80 sample[500] 0.0362 [0.03382494, 0.03866225]95
#> 3 Female cbd 2026 80 sample[500] 0.0357 [0.03289851, 0.03854067]95
#> 4 Female cbd 2027 80 sample[500] 0.0352 [0.03203992, 0.03848268]95
#> 5 Female cbd 2028 80 sample[500] 0.0347 [0.03090754, 0.03853392]95
#> 6 Female cbd 2029 80 sample[500] 0.0342 [0.03020974, 0.03853143]95
#> 7 Female cbd 2030 80 sample[500] 0.0338 [0.02935744, 0.03847232]95
#> 8 Female cbd 2031 80 sample[500] 0.0333 [0.02867820, 0.03852769]95
#> 9 Female cbd 2032 80 sample[500] 0.0328 [0.02787233, 0.03830655]95
#> 10 Female cbd 2033 80 sample[500] 0.0324 [0.02756522, 0.03784387]95