markovchain is now a genuinely optional dependency
(Suggests). Before, the S4 coercions to
markovchainList were registered at build time only
if markovchain happened to be installed then, so installing it later
left as(x, "markovchainList") broken while
rmdt() worked; they are now registered at load time
(.onLoad()), whichever order the packages were installed
in. Without markovchain, rmdt() stops with an informative
error instead of warning and returning NULL, and the
vignette chunk that needs it is skipped
(eval = requireNamespace("markovchain")); the
rmdt() example is guarded the same way. Tests in
test-optional-markovchain.R cover both states.
Minimum supported R raised from 4.1.0 to 4.4.0
(Depends); the CI full matrix now also checks R
4.4.
Portability of floating-point comparisons (found by the
macOS/aarch64 CI job, R 4.6.1, Apple clang 21): the mdt
validity check and .tableSanitizer() compared the total of
the decrement columns with lx for exact equality,
so a table whose decrements come from floating-point rates was accepted
or rejected depending on how the platform happened to round.
buildMdtFromIndependentRates() balanced to the last bit on
x86 but was rejected with
invalid class "mdt" object: Check the lx on aarch64, where
the compiler contracts a * b + c into a fused multiply-add.
Both comparisons now allow a relative tolerance of 1e-8
(.MDT_BALANCE_TOL); genuinely unbalanced tables are still
rejected, and integer-valued published tables are unaffected.
test-pxt-lifetable-native.R no longer requires the
native pxt() kernel to be bit for bit identical to
the former R code, which is not a portable property for the “linear” and
“hyperbolic” assumptions (same fused multiply-add: a few ulps of
difference). It now asserts that the degenerate cases agree exactly
(which entries are NaN, which are zero) and that the finite
values agree to a relative tolerance of 1e-12. The kernel’s own values
are unchanged.
_pkgdown.yml added to .Rbuildignore: it
was shipped in the tarball and raised a “Non-standard file/directory
found at top level” NOTE under
R CMD check --as-cran.
dxt() on
lifetable/actuarialtable objects is now
vectorised over x and t (recycled to a common
length) and looks ages up with match() instead of
which() per call; Tx() uses it instead of an
sapply() loop. Results are unchanged on every integer and
fractional case previously computed (checked bit for bit on SOA 2008 and
two other tables); fractional t reaching beyond the last
tabulated age used to return numeric(0) and now correctly
counts no further deaths.
New function independentRatesFromMdt(): extracts the
full Associated Single Decrement Table (ASDT) as a matrix of independent
rates \(q'^{(j)}_x\) for every
combination of age and decrement in an mdt object, under
the UDD assumption. This is the vectorized convenience wrapper around
qxt.prime.fromMdt().
New function buildMdtFromIndependentRates():
constructs an mdt object from a matrix of independent
(ASDT) rates, the inverse of independentRatesFromMdt().
Uses the UDD integration formula via qxt.fromQxprime() to
convert independent rates to absolute rates, then builds the
survivorship column recursively.
New plot() S4 method for mdt objects:
produces a ggplot2 visualisation with three views — stacked
area chart of decrement counts (default), stacked bar chart, or line
chart of decrement-specific probabilities. Requires ggplot2
(already in Imports).
The multiple-decrement vignette
(multiple_decrement_tables.Rmd) now documents the three new
functions with worked examples from Finan (2014) and a round-trip
verification (mdt → ASDT → mdt).
dxt(), pxt() and qxt() are
now S4 generics with methods for lifetable (hence
actuarialtable) and mdt, replacing the
hand-written dispatch on class(object). Signatures and
results are unchanged (checked bit for bit on lifetables, actuarial
tables and mdt objects across fractional assumptions and ages);
unsupported objects keep the historical error message, and the per-call
overhead is lower than before. New table classes can now register their
own methods.
Fixed decrement-specific probabilities on mdt
objects: pxt()/qxt(..., decrement = ) used an
optimised branch that rebuilt a pseudo-lx from the
cause-specific counts alone whenever the first row of the table
contained no zero cell (e.g. a table supplied from age 0), returning
NaN or values outside [0, 1] (qxt() gave -2 on
a simple 3-age table). They now compute
tq_x^(j) = td_x^(j) / l_x directly, vectorised over
x and t, with linear (UDD) interpolation for
fractional t; dxt() on an mdt
also interpolates fractional t instead of truncating
it.
dxt(), pxt() and qxt() on
mdt objects accept several decrements at once
(decrement = c("death", "disability")), returning the
probability/number of leaving the table because of any of them.
pxt() and qxt() now really default to
t = 1 as documented (previously a missing t
raised an error).
Axn.mdt() gains cause-dependent benefits
(benefits, one per covered decrement), several decrements
at once, a deferment m, cover on the total decrement when
decrement is missing, and vectorisation over
x/n/m. Its default term is now
n = omega + 1 - x - m, consistent with Axn():
the previous default omega - x - 1 silently dropped the
last tabulated age. The corresponding regression test, which asserted
the 2-year value 0.1364 for the 3-year term of Finan (2014) Problem
68.1, now checks the correct 3-year value (APV 4515.40).
New axn.mdt(): APV of an annuity payable while no
decrement has occurred, with deferment, k payments per year
and payments in advance or in arrears. Together with
Axn.mdt() it reproduces the worked examples of Finan (2014)
Sections 68-69 (Examples 68.1, 69.1, 69.2, 69.3), now in
test-mdt-s4-actuarial-long.R.
New mdtToLong(): converts an mdt into
an aggregated long (time, status, count) data set for competing-risks
analysis with survival::Surv; the Aalen-Johansen cumulative
incidence computed by survival on it coincides with
qxt(..., decrement = ). survival added to
Suggests.
The multiple-decrement vignette documents Axn.mdt()
with cause-dependent benefits, axn.mdt() (premiums and
reserves from Finan’s examples) and mdtToLong().
Internal performance: rLifeContingencies() and
rLifeContingenciesXyz() now compute the per-sample payoff
with vectorized C++ kernels (.fAxnCppVec(),
.faxnCppVec(), .fAxyznCppVec() and relatives)
instead of calling the scalar kernel element by element via
sapply()/apply(). The whole
deathsTimeX vector/matrix is processed in a single
.Call, which removes the per-element interop overhead that
dominated runtime for large n. The
parallel = TRUE argument now drives OpenMP inside the C++
loop rather than a parallel::makeCluster(), and is strictly
opt-in: it has an effect only if the user also sets
options(lifecontingencies.openmp = TRUE); the number of
threads comes from the new nthreads argument or
options(lifecontingencies.nthreads) (default 2, never
auto-detected). Without the option, or when the shared library was built
without OpenMP support, the sequential vectorized kernel is used. Return
values are numerically equivalent to the previous implementation
(checked on 116 configurations of
rLifeContingencies()/rLifeContingenciesXyz()
against the previous release, max abs. difference 9e-15;
test-sim-vector-kernels.R pins each kernel to its scalar
reference), and set.seed() + parallel = TRUE
produces the same realized sample as parallel = FALSE; the
only intended change is the multi-life fix below.
Makevars/Makevars.win now pass
$(SHLIB_OPENMP_CXXFLAGS).
Fixed rLifeContingenciesXyz() when the lives have
different issue ages: the vector x was added to the matrix
of simulated lifetimes recycled down the column-major storage, so ages
were swapped across lives (simulated Axyz for ages 40 and
60 deviated from Axyzn() by more than 10 standard errors;
equal ages were unaffected). Ages are now added column-wise; covered by
a new test.
Memory-safety hardening of the native code, found with an
AddressSanitizer/UBSan build: the multi-life kernels reject a matrix
with no columns (was a null dereference) and use 64-bit indices; the
annuity kernel no longer converts an unbounded number of payment dates
to an integer (undefined behaviour for n = Inf or very
large k) and uses an expm1 formulation that
stays accurate for tiny payment steps; .pxtCpp() no longer
reads out of bounds when omega >= length(lx) and no
longer casts huge/NaN ages to int, and uses
R_xlen_t sizes. New tests in
test-native-memory-safety.R.
New demographic functions on life tables: varxn()
and sdxn() (variance and standard deviation of the
curtate/complete future lifetime, the second-moment companions of
exn()), modalAge() (Lexis modal age at death,
with optional parabolic interpolation), and
median()/quantile() methods for
lifetable/actuarialtable objects (age-at-death
distribution, optionally conditional on survival to a given
age). Tx() and exn() gain
fxt and axOmega arguments controlling the
within-year distribution of deaths and the closure of the last, open age
interval (L_omega = axOmega * l_omega), so published
open-interval tables (e.g. NCHS,
L_omega = l_omega / m_omega) can be reproduced; the
defaults leave existing results unchanged. print() for a
lifetable forwards exType
(“curtate”/“complete”), fxt and axOmega and
returns the tabulated data.frame invisibly. New vignette
Demographic analysis with the lifecontingencies package and
tests in test-demographic-extras.R.
Internal performance: pxt() (and hence
qxt(), axn(), Axn(),
AExn(), IAxn(), Iaxn(),
DAxn(), axyzn(), Axyzn() and the
other functions built on it) now computes survival probabilities for
lifetable/actuarialtable objects with a native
kernel, .pxtLifetableCpp(), an exact port of the former R
code (name-based lookup, NA-to-zero replacement of the one-year ratios,
R_pow for ^). Return values are bit-for-bit
identical, degenerate NaN cases included, as checked against the
previous implementation on 577 outputs of the public functions and 2.4
million random pxt() evaluations;
test-pxt-lifetable-native.R keeps the former R code as
reference. Typical speed-ups: pxt() x12-x28,
axn()/Axn() x5-x8 (x20-x26 with monthly
payments), multiple-life annuities x3.5. The mdt branch of
pxt() is unchanged.
Fixed exyzt(..., type = "Tx") for a finite term
t: the complete expectation under UDD is
sum(kpxyz) + 0.5 * (1 - tpxyz); the previous code added a
fixed 0.5, overstating the result unless t covered the
whole lifespan (default t = Inf is unchanged).
Documentation of exn(), exyzt() and
rLife(): precise definitions of the curtate and complete
(temporary) expectation of life, their UDD relation, the meaning of
n, and the closed-interval treatment of omega; fixed the
Pr[K_x = t] formula (denominator is l_x) and
the description of the ex column of
print(lifetable) (curtate) in the introductory vignette.
New tests in test-exn-definitions.R and
test-published-life-expectancy.R (worked examples from
Finan’s MLC manual, SOA Exam MLC samples and Dickson-Hardy-Waters
Exercise 2.1; note that Finan Ex. 23.24 prints e_{80:3} = 1.64 while the
correct value is 1.94).
New regression tests against published national tables and worked examples: Spanish GRM-80 commutation table at 6%, German Destatis 2002/2004 commutation numbers and annuity values (men, women, joint life), Australian Life Tables 1932-34 monetary table at 3%, Irish Life Table No. 14 personal injury multipliers (Society of Actuaries in Ireland).
Removed fragile/dead links: the Travis-CI badge
(travis-ci.org, discontinued free-tier domain, redundant
with the existing GitHub Actions R-CMD-check badge) and the
Depsy badge (depsy.org, defunct project, broken TLS
certificate) in README.md; dropped the now-inaccessible
url field (redirects to a Google sign-in page) from the Tim
Riffe LifeTable bibliography entry in
vignettes/lifecontingenciesBiblio.bib, keeping the citation
itself. Also dropped the Google Books url fields from the
de2016assicurazioni and
willekens2014multistate bibliography entries in the same
file, keeping the citations (ISBN retained).
Added tests/_snaps and
tests/testthat/_snaps to .Rbuildignore so a
stray local empty snapshot directory no longer triggers the “Removed
empty directory” note during R CMD build.
Axn.mdt(),
setAs("lifetable","numeric",...),
setAs("actuarialtable","numeric",...) and
.lifetable_to_markovchain_list() now call the
already-vectorized
pxt()/qxt()/Axn() once instead of
looping over scalar calls. No change in return values (verified against
the previous implementation on multiple test tables, including
mdt and Markov-chain conversions)..github/workflows/ci.yml CI
workflow, which duplicated R-CMD-check.yaml but lacked
TinyTeX setup and could fail the vignette build.Exn() and AExn() are now vectorized:
x, n and i are recycled to a
common length and one value per element is returned (previously a vector
age raised an error).multiple_decrement_tables.Rmd:
the full multiple-decrement workflow (the mdt class,
decrement probabilities, Associated Single Decrement Tables, actuarial
applications, and a worked, fully reproducible example building a
cause-specific mdt object from NCHS’s “United States Life
Tables Eliminating Certain Causes of Death, 1999-2001”, Table 8,
including curation of a mutually exclusive cause-of-death partition and
graduation from 5-year bands to single years of age via a monotone
Hermite (Fritsch-Carlson) spline on lx, with an explicit
note that this is not a substitute for PCLM-based ungrouping). The main
vignette’s “Multiple Decrement Models” section is now a short
introduction pointing to it.