Tables

Loading Tables

There are a variety of ways to load your own tables, reference online tables, or load bundled tables:

Bundled mort.SOA.org Tables

Comes with some tables built in via mort.SOA.org and by using you agree to their terms.

Note

Many mortality tables from mort.SOA.org have been tested to work, however not all mortality tables have been tested. Additionally, mort.SOA.org provides non-mortality rate tables which may not be properly parsed by this package. Try it and look at a few of the resulting values. If you encounter any issues, please file an issue.

Sample of some of the included table sets:

2017 Loaded CSO
2015 VBT
2001 VBT
2001 CSO
1980 CSO
1980 CET

The names of the bundled tables are the keys of MortalityTables.table_source_map, which maps each name to its mort.SOA.org id.

If you would like more tables added by default, please open a GitHub issue with the request.

Other mort.SOA.org Tables

Given a table id (for example 60029) or name, get_SOA_table (also called MortalityTables.table) loads the bundled copy of the table. Remember that not all tables have been tested, though the standard source format should mean compatibility with MortalityTables.jl.

aus_life_table_female = get_SOA_table(60029)
aus_life_table_female.ultimate[0]  # returns the attained age 0 rate of 0.10139

Load custom set of tables

Download the .xml aka the (XTbML format) version of the table from mort.SOA.org and place it in a directory of your choosing. Then call MortalityTables.read_tables(path_to_your_dir), which returns a Dict of the tables by name.

From CSV

If you have a CSV file that is from mort.SOA.org, or follows the same structure, then you can load and parse the table into a MortalityTable like so:

using CSV
using MortalityTables

path = "path/to/table.csv"
file = CSV.File(path,header=false) # SOA's CSV files have no true header
table = MortalityTable(file)

From XTbML

If you have a file using the XTbML format:

using MortalityTables
path = "path/to/table.xml"
table = MortalityTables.readXTbML(path)

Custom Tables

Say you have an ultimate vector and select matrix, and you want to leverage the MortalityTables package.

Here's an example, where we first construct the UltimateMortality and then combine it with the select rates to get a SelectMortality table.

using MortalityTables

# represents attained ages of 15 through 100
ult_vec = [0.005, 0.008, ...,0.805,1.00]
ult = UltimateMortality(ult_vec,start_age = 15)

We can now use the ultimate rates by themselves:

ult[15]               # 0.005, the rate at age 15
survival(ult, 15, 16) # 0.995

And join with the select rates, which for our example will start at age 0:

# issue age going down the rows, duration across
select_matrix = [ 0.001 0.002 ... 0.010;
                  0.002 0.003 ... 0.012;
                  ...
                ]
sel_start_age = 0
sel = SelectMortality(select_matrix,ult,start_age = 0)

sel[0][0]   # issue age 0, attained age 0 rate of 0.001
sel[0][100] # issue age 0, attained age 100 rate of 1.0

Lastly, to take the SelectMortality and UltimateMortality we just created, we can combine them into one stored object, along with a TableMetaData:

my_table = MortalityTable(
              sel,
              ult,
              metadata=TableMetaData(name="My Table", comments="Rates for Product XYZ")
              )
MortalityTables.readXTbML — Function
readXTbML(path)

Loads the XTbML (the SOA XML data format for mortality tables) stored at the given path and returns a MortalityTable.

The result is cached by path, so calling this twice with the same path returns the identical object.

source
MortalityTables.table — Function
table(id)
table(name)

Given the id or name of a mort.SOA.org table, grab it and return it as a MortalityTable.

Note

Not all tables have been tested to work.

source
MortalityTables.get_SOA_table — Function
get_SOA_table(id)
get_SOA_table(table_name)

Given the id or name of a mort.SOA.org table, grab it and return it as a MortalityTable.

Note

Not all tables have been tested to work.

source

Table Constructors

Use these to build your own MortalityTables.jl-compatible table:

MortalityTables.MortalityTable — Type
MortalityTable(ultimate)
MortalityTable(select, ultimate)
MortalityTable(select, ultimate; metadata::TableMetaData)

Constructs a container object which can hold either:

  • ultimate-only rates (an UltimateTable)
  • select and ultimate rates (a SelectUltimateTable)

Also pass a keyword argument metadata=TableMetaData(...) to store relevant information (source, notes, etc) about the table itself.

Examples

# first construct the underlying data
ult = UltimateMortality([x / 100 for x in 0:100]);
matrix = rand(50,10); # random(!) rates for issue ages 0 to 49, with a select period of 10 years
sel = SelectMortality(matrix,ult,start_age=0);

table = MortalityTable(sel,ult)

# can now get rates, indexed by attained age:

table.select[10] # the vector of rates for a risk issued select at age 10 

table.ultimate[99] # 0.99
source
MortalityTables.SelectMortality — Function
SelectMortality(select, ultimate; start_age=0)

Given a matrix of rates, where each row holds the select rates for one issue age, creates an OffsetArray that is indexed by issue age, containing a vector of rates indexed by attained age. The ultimate mortality vector is used for rates in the post-select period.

Give the optional keyword argument to start the indexing at an age other than zero.

Examples

julia> ult = UltimateMortality([x / 100 for x in 0:100]);

julia> matrix = rand(50,10); # represents random(!) mortality rates with a select period of 10 years

julia> sel = SelectMortality(matrix,ult,start_age=0);

julia> sel[0] # the mortality vector for a select life with issue age 0
 0.12858960119349439
 0.1172480189376135
 0.8237661916705163
 ⋮
 0.98
 0.99
 1.0

julia> sel[0][95] # the mortality rate for a life age 95, that was issued at age 0
0.95
source
MortalityTables.UltimateMortality — Function
UltimateMortality(vector; start_age=0)

Given a vector of rates, returns an OffsetArray that is indexed by attained age.

Any AbstractVector is accepted (a Vector, a range, a view, or a vector containing missing); the input is wrapped without copying.

Give the optional keyword argument to start the indexing at an age other than zero.

Examples

julia> m = UltimateMortality([0.1,0.3,0.6,1]);

julia> m[0]
0.1

julia> m = UltimateMortality([0.1,0.3,0.6,1], start_age = 18);

julia> m[18]
0.1
source
MortalityTables.mortality_vector — Function
mortality_vector(vec; start_age=0)

An alias for UltimateMortality: wraps vec in an OffsetArray indexed by attained age, starting at start_age, rather than always starting from 1. The package and JuliaActuary ecosystem assume that the rates are indexed by attained age, and this allows transformation of tables without a direct dependency on OffsetArrays.jl.

source

Table Attributes

Basic metadata about the table (automatically populated for some tables).

MortalityTables.TableMetaData — Type
TableMetaData(kwargs...)

Has the following fields, which default to nothing if not specified with a keyword:

  • name - a name for the table
  • id - if a mort.SOA.org sourced table, will be the identifying table ID
  • provider - Where the rates came from
  • reference - Source for more info on table
  • content_type
  • description
  • comments
  • source_path

When you call a MortalityTable interactively, it will nicely print this summary information.

Example content from mort.SOA.org:

  • Table Identity: 1076
  • Provider Domain: actuary.org
  • Provider Name: American Academy of Actuaries
  • Table Reference: Tillinghast, “American Council of Life Insurers: Preferred Version of 2001 CSO Mortality Tables”, ...
  • Content Type: CSO/CET
  • Table Name: 2001 CSO Super Preferred Select and Ultimate - Male Nonsmoker, ANB
  • Table Description: 2001 Commissioners Standard Ordinary (CSO) Super Preferred Select and Ultimate Table – Male Nonsmoker. Basis: Age Nearest Birthday. Minimum Select Age: 0. Maximum Select Age: 99. Minimum Ultimate Age: 16. Maximum Ultimate Age: 120
  • Comments: Study Data: A preferred version of the 2001 Commissioners Standard Ordinary (CSO) table ...

And the source_path would be: https://mort.soa.org/ViewTable.aspx?&TableIdentity=1076

Example usage:

julia-repl> TableMetaData(name="My Table Name")
TableMetaData("My Table Name", nothing, nothing, nothing, nothing, nothing, nothing, nothing)
source

Find the final age for which a table defines a rate.

MortalityTables.omega — Function
omega(x)
ω(x)

Returns the last index of the given vector. For mortality vectors this means the last attained age for which a rate is defined.

Note that omega can vary depending on the issue age for a select table, and that a select omega may differ from the table's ultimate omega.

For a parametric model (see ParametricMortality), omega is the last age at which its law is defined: Inf for a law defined at every age, m for Wittstein (where its formula ends), and n for VanderMaen and VanderMaen2 (whose hazard has a pole there). An age past omega is a DomainError. Survival need not reach zero at omega (for Wittstein it does not), so a projection that stops at omega truncates such a law. omega does not validate a law's parameters either: some parameters give a negative hazard at ages inside the domain.

ω is aliased to omega, but un-exported. To use, do using MortalityTables: ω when importing or call MortalityTables.ω()

Examples

julia> qs = UltimateMortality([0.1,0.3,0.6,1]);
julia> omega(qs)
3

julia> qs = UltimateMortality([0.1,0.3,0.6,1],start_age=10);
julia> omega(qs)
13
source

Rates, Survival and Decrement

To access the rates, index a table's rate vectors by attained age. Example:

julia> vbt2001 = MortalityTables.table("2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB")
MortalityTable (Insured Lives Mortality):
   Name:
       2001 VBT Residual Standard Select and Ultimate - Male Nonsmoker, ANB
   Fields:
       (:select, :ultimate, :metadata)
   Provider:
       Society of Actuaries
   mort.SOA.org ID:
       1118
   mort.SOA.org link:
       https://mort.soa.org/ViewTable.aspx?&TableIdentity=1118
   Description:
       2001 Valuation Basic Table (VBT) Residual Standard Select and Ultimate Table -  Male Nonsmoker.
       Basis: Age Nearest Birthday. 
       Minimum Select Age: 0. 
       Maximum Select Age: 99. 
       Minimum Ultimate Age: 25. 
       Maximum Ultimate Age: 120

The package revolves around easy-to-access vectors which are indexed by attained age:

julia> vbt2001.select[35]          # vector of rates for issue age 35
 0.00036
 0.00048
 ⋮
 0.94729
 1.0
 
julia> vbt2001.select[35][35]      # issue age 35, attained age 35
 0.00036
 
julia> vbt2001.select[35][50:end] # issue age 35, attained age 50 through end of table
0.00316
0.00345
 ⋮
0.94729
1.0

julia> vbt2001.ultimate[95]        # ultimate vectors only need to be called with the attained age
 0.24298

Docstrings

MortalityTables.survival — Function
survival(mortality_vector,to_age)
survival(mortality_vector,from_age,to_age)

Returns the survival through attained age to_age. The start of the calculation is either the start of the vector, or attainedage `fromage.fromageandtoage` need to be Integers. Add a DeathDistribution as the last argument to handle floating point and non-whole ages:

survival(mortality_vector,to_age,::DeathDistribution)
survival(mortality_vector,from_age,to_age,::DeathDistribution)

Survival from a fractional from_age is conditional on surviving to from_age: it equals survival(v, to_age, dd) / survival(v, from_age, dd) under the same assumption, so survival over consecutive intervals multiplies. Where the assumption gives zero survival to a fractional from_age (after a rate of one under ConstantForce or Balducci) there is nothing to condition on; the formulas are still evaluated as written and give finite values.

When to_age is before from_age, the result is the reverse factor 1 / survival(v, to_age, from_age): the number expected alive at the earlier age for each life alive at the later one, as used to project a population backward or to accumulate with the benefit of survivorship. It is not a probability (it can exceed one, and the corresponding decrement is negative), and it is an expected-value back-calculation rather than a reconstruction of realized deaths. It is defined where the forward survival is positive; a zero forward survival gives Inf. With it, survival composes over any three ages whose factors are positive and representable: survival(v, a, c) == survival(v, a, b) * survival(v, b, c), whatever their order. (A zero factor has no inverse: 0 * Inf is not one.) Ages outside the table (such as a negative to_age for a table starting at zero) are a BoundsError.

Results have the numeric type of the rates, promoted with the ages' type for fractional ages, including the exact one of an empty interval: a BigFloat table gives BigFloat survival, and a Float32 table gives Float32 survival at whole ages. A vector whose element type doesn't name the rates' number type (such as Any, Real, or Missing for a column with no rates) has Float64 identities, as in version 2: an empty interval gives 1.0, and a nonempty result takes the type its rates' arithmetic gives. Use a concretely typed vector for type-consistent and fast results.

Parametric models (see ParametricMortality) are continuous and need no fractional-age assumption, so they accept a trailing DeathDistribution and ignore it.

Examples

julia> qs = UltimateMortality([0.1,0.3,0.6,1]);
    
julia> survival(qs,0)
1.0
julia> survival(qs,1)
0.9

julia> survival(qs,1,1)
1.0
julia> survival(qs,1,2)
0.7

julia> survival(qs,0.5,UniformDeaths())
0.95
source
MortalityTables.decrement — Function
decrement(mortality_vector,to_age)
decrement(mortality_vector,from_age,to_age)

Returns the cumulative decrement through attained age to_age. The start of the calculation is either the start of the vector, or attainedage `fromage.fromageandtoage` need to be Integers. Add a DeathDistribution as the last argument to handle floating point and non-whole ages:

decrement(mortality_vector,to_age,::DeathDistribution)
decrement(mortality_vector,from_age,to_age,::DeathDistribution)

Examples

julia> qs = UltimateMortality([0.1,0.3,0.6,1]);
    
julia> decrement(qs,0)
0.0
julia> decrement(qs,1)
0.1

julia> decrement(qs,1,1)
0.0
julia> decrement(qs,1,2)
0.3

julia> decrement(qs,0.5,UniformDeaths())
0.05

The decrement is accumulated directly ($d \leftarrow q + d(1 - q)$) rather than computed as one minus the survival product, so a small decrement keeps its precision. A reversed interval gives the negative decrement 1 - survival(v, from_age, to_age) of the reverse factor, computed as -d / S from the forward decrement d and forward survival S, so it keeps its precision both for a small decrement and for a small forward survival.

A type that defines only survival gets decrement as 1 - survival; define decrement too where that complement would lose precision.

source

Life Expectancy

curtate_life_expectancy gives the expected number of whole years lived after an age; complete_life_expectancy gives the expected remaining lifetime, with survival within each year of age following a fractional year assumption (see below).

Docstrings

MortalityTables.curtate_life_expectancy — Function
curtate_life_expectancy(rates, age)
curtate_life_expectancy(law, age; rtol, atol)

The curtate expectation of life at age: the expected number of whole years lived after age, $e_x = \sum_{k \ge 1} {}_kp_x$, where ${}_kp_x$ is survival(m, age, age + k).

For a vector of rates, age is a whole age in the table. The sum runs to the table's last age, where the expectancy is zero, even if the table does not end with a rate of one. An age outside the table is a BoundsError.

For a parametric law, age is any real age up to omega(law). An age past omega is a DomainError.

  • With a finite omega, the sum runs to $k = \lfloor \omega - x \rfloor$. Survival left at omega counts as death there, so the expectancy at omega is zero.
  • With omega = Inf, the first K terms are summed and the rest is estimated from the integral of survival $S(t) = {}_tp_x$. Survival does not increase (the hazard is not negative), so the rest lies between $\int_{K+1}^\infty S$ and $\int_K^\infty S$, an interval no wider than $S(K)$. The estimate uses its midpoint, integrated with QuadGK. K grows until $S(K)/2$ plus QuadGK's error estimate is at most max(atol, rtol * abs(estimate)). This is an estimated accuracy, not a certified bound. If 100,000 terms are not enough, it throws an ArgumentError.

rtol defaults to sqrt(eps(T)), where T is the type of the survival values, and atol to 0. They apply only to a law with omega = Inf, and must be finite and nonnegative (either may be zero), or an ArgumentError names the one that is not.

See also complete_life_expectancy.

Examples

julia> qs = UltimateMortality([0.1, 0.3, 0.6, 1]);

julia> curtate_life_expectancy(qs, 0) # 0.9 + 0.9 * 0.7 + 0.9 * 0.7 * 0.4
1.782

julia> curtate_life_expectancy(qs, 3)
0.0
source
MortalityTables.complete_life_expectancy — Function
complete_life_expectancy(rates, age, dist = UniformDeaths())
complete_life_expectancy(law, age, dist = UniformDeaths(); rtol, atol, maxevals)

The complete expectation of life at age: the expected remaining lifetime, $\mathring{e}_x = \int_0^{\omega - x} {}_tp_x \, dt$, where ${}_tp_x$ is survival(m, age, age + t).

For a vector of rates, survival within each year of age follows the DeathDistribution dist, and survival is integrated one year of age at a time. A fractional age conditions on survival to age, as survival does. The integral runs to the table's last age, where the expectancy is zero. An age outside the table is a BoundsError.

For a parametric law, survival is integrated with QuadGK up to omega(law). dist is accepted and ignored, since a law is continuous. With a finite omega, survival left at omega counts as death there, so the expectancy at omega is zero. An age past omega is a DomainError. rtol, atol and maxevals go to QuadGK, with its defaults: atol = 0, rtol the square root of the machine epsilon of the integration interval's type when atol is zero, and maxevals = 10^7. The tolerances must be finite and nonnegative and maxevals a positive integer, or an ArgumentError names the one that is not. The integral is returned only if it and QuadGK's error estimate are finite and the estimate is at most max(atol, rtol * abs(integral)); the estimate is QuadGK's, not a proof of accuracy. Otherwise an ArgumentError reports that the integration did not converge: the expectancy may be infinite, as for a law whose survival falls too slowly, or the integral may need a larger maxevals.

See also curtate_life_expectancy.

Examples

julia> qs = UltimateMortality([0.1, 0.3, 0.6, 1]);

julia> complete_life_expectancy(qs, 0) # 0.95 + 0.9 * 0.85 + 0.9 * 0.7 * 0.7
2.156

julia> complete_life_expectancy(qs, 0.5, ConstantForce())
1.7198453593325438
source

Fractional Year Assumptions

When evaluating survival over partial years when you are given full year mortality rates, you must make an assumption over how those deaths are distributed throughout the year. Three assumptions are provided as options and are based on formulas from the 2016 Experience Study Calculations paper from the SOA, specifically pages 40-44.

The three assumptions are:

  • UniformDeaths() which assumes an increasing force of mortality throughout the year.
  • ConstantForce() which assumes a level force of mortality throughout the year.
  • Balducci() which assumes a decreasing force of mortality over the year. It seems [to

be for making it easier](https://www.soa.org/globalassets/assets/library/research/actuarial-research-clearing-house/1978-89/1988/arch-1/arch88v17.pdf) to calculate successive months by hand rather than any theoretical basis.

MortalityTables.DeathDistribution — Type
DeathDistribution

An abstract type used to form an assumption of how deaths occur throughout a year. See Balducci(), UniformDeaths(), and ConstantForce() for concrete assumption types.

source
MortalityTables.UniformDeaths — Type
UniformDeaths()

A DeathDistribution type that assumes deaths are spread uniformly over the year of age (UDD), which gives an increasing force of mortality over the year.

source

Select-period deterioration (Dukes-MacDonald)

The Dukes-MacDonald methods estimate how the mortality of a select cohort deteriorates when excess lapses remove the healthier lives. Three variants are provided, following Doll (2003). Each has two methods: one taking the point-in-scale and select rates directly, and one taking a SelectUltimateTable together with the issue and attained ages.

These functions are not exported; call them as MortalityTables.dukes_macdonald1 etc.

MortalityTables.dukes_macdonald1 — Function

dukesmacdonald1(pointinscale, selectrate, baselapses, totallapses, effectiveness)

dukesmacdonald1(selectultimate, issueage, attainedage, baselapses, total_lapses, effectiveness)

Two methods are available, one takes the pointinscale and selectrate directly. See https://www.soa.org/globalassets/assets/library/newsletters/product-development-news/2003/july/pdn-2003-iss56-doll-a.pdf for information on terminology. The other method takes a SelectUltimateTable from MortalityTables.jl and calculates the pointinscale and selectrate from the table.

source
MortalityTables.dukes_macdonald2 — Function

dukesmacdonald2(pointinscale, selectrate, baselapses, totallapses, effectiveness)

dukesmacdonald2(selectultimate, issueage, attainedage, baselapses, total_lapses, effectiveness)

Two methods are available, one takes the pointinscale and selectrate directly. See https://www.soa.org/globalassets/assets/library/newsletters/product-development-news/2003/july/pdn-2003-iss56-doll-a.pdf for information on terminology. The other method takes a SelectUltimateTable from MortalityTables.jl and calculates the pointinscale and selectrate from the table.

source
MortalityTables.dukes_macdonald3 — Function

dukesmacdonald3(pointinscale, selectrate, baselapses, totallapses, effectiveness)

dukesmacdonald3(selectultimate, issueage, attainedage, baselapses, total_lapses, effectiveness)

Two methods are available, one takes the pointinscale and selectrate directly. See https://www.soa.org/globalassets/assets/library/newsletters/product-development-news/2003/july/pdn-2003-iss56-doll-a.pdf for information on terminology. The other method takes a SelectUltimateTable from MortalityTables.jl and calculates the pointinscale and selectrate from the table.

source