Metadata-Version: 2.4
Name: pylifecontingencies
Version: 0.5.0
Summary: Life contingencies and actuarial mathematics in Python, with dynamic life tables and mortality forecasting
Project-URL: Homepage, https://github.com/filipeclduarte/pylifecontingencies
Project-URL: Repository, https://github.com/filipeclduarte/pylifecontingencies
Project-URL: Issues, https://github.com/filipeclduarte/pylifecontingencies/issues
Author: Filipe Duarte
License: GPL-2.0
License-File: LICENSE
Keywords: actuarial,life contingencies,life tables,mortality,survival analysis
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Financial and Insurance Industry
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: GNU General Public License v2 (GPLv2)
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Topic :: Office/Business :: Financial
Classifier: Topic :: Scientific/Engineering :: Mathematics
Requires-Python: >=3.10
Requires-Dist: matplotlib>=3.7
Requires-Dist: numpy>=1.24
Requires-Dist: pandas>=2.0
Requires-Dist: pyarrow>=14.0
Requires-Dist: scipy>=1.11
Requires-Dist: statsmodels>=0.14
Provides-Extra: dev
Requires-Dist: build>=1.2; extra == 'dev'
Requires-Dist: pytest-cov; extra == 'dev'
Requires-Dist: pytest>=7.4; extra == 'dev'
Requires-Dist: rpy2>=3.5; extra == 'dev'
Requires-Dist: twine>=5.0; extra == 'dev'
Provides-Extra: docs
Requires-Dist: mkdocs-material>=9.0.0; extra == 'docs'
Requires-Dist: mkdocs-static-i18n>=1.2.0; extra == 'docs'
Requires-Dist: mkdocs>=1.6.0; extra == 'docs'
Requires-Dist: mkdocstrings[python]>=0.25.0; extra == 'docs'
Provides-Extra: soa
Requires-Dist: pymort>=2.0; extra == 'soa'
Description-Content-Type: text/markdown

# pylifecontingencies

A native Python port of the R [`lifecontingencies`](https://github.com/spedygiorgio/lifecontingencies) package, extended with **dynamic life tables and mortality-rate forecasting** (Lee-Carter, CBD M5).

No R runtime required. Pure NumPy + pandas at install time; `rpy2` is only used in the validation test suite.

---

## What it includes

- Single-life actuarial present values: annuities, insurances, endowments, increasing/decreasing benefits, premiums, and reserves
- Mortality and demographic utilities: `pxt`, `qxt`, `mxt`, `Lxt`, `Tx`, `exn`
- Dynamic mortality forecasting: Lee-Carter, CBD M5, projected life tables, stochastic scenario support
- Monte Carlo PV simulation via `StochasticResult` — k-thly payments, deferral, `hist`/`plot` visualisation, `var`/`tvar` risk metrics, pandas export
- Parametric mortality graduation with `GompertzMakeham` and `HeligmanPollard`
- Bundled data: 100+ mortality tables via `load_table()` / `list_tables()` — SOA ILT, BR-EMS series (2010–2021), AT, UP, RP, GAM, CSO, IBGE, and many historical tables; plus R-sourced parquets (`soa08`, `AM92Lt`, `demoUsa`, etc.)

## Installation

Core package:

```bash
pip install pylifecontingencies
```

With SOA XTbML support:

```bash
pip install "pylifecontingencies[soa]"
```

For local development and release checks:

```bash
pip install -e ".[dev]"
```

---

## Quick examples

### Static actuarial values

```python
from pylifecontingencies import load_table, ActuarialTable, axn, Axn, AExn

lt = load_table("soa_ilt")
at = ActuarialTable(lt, interest=0.06)

axn(at, x=40)          # whole-life annuity-due
Axn(at, x=40, n=20)    # 20-year term insurance
AExn(at, x=40, n=20)   # 20-year endowment insurance
```

### Bundled R tables

```python
from pylifecontingencies import load_table, list_columns

list_columns("demoUsa")
lt_usa = load_table("demoUsa", column="USSS2007M")
lt_soa = load_table("soa08")
```

### BR-EMS tables

```python
from pylifecontingencies import load_table, ActuarialTable, axn, qxt

# BR-EMS Sobrevivencia 2021, male
lt_br = load_table("br_emssb_2021_m")
at_br = ActuarialTable(lt_br, interest=0.03)

qxt(lt_br, x=40, t=10)   # 10-year death probability at age 40
axn(at_br, x=65)         # whole-life annuity-due at age 65
```

### Stochastic simulation

```python
from pylifecontingencies import simulate_pv

# Annual term insurance
r = simulate_pv(at, x=40, n=20, benefit="term", n_sim=50_000, random_state=42)
r.mean, r.std, r.quantile(0.95)

# Monthly annuity (k=12 payments per year)
r_monthly = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, n_sim=50_000)

# 10-year deferred whole-life annuity (pension starting at age 50)
r_deferred = simulate_pv(at, x=40, benefit="annuity", m=10, n_sim=50_000)

# Visualisation
r.hist()          # histogram with mean and 95 % CI lines
r.plot()          # empirical CDF

# Risk metrics (Solvency II)
r.var(0.995)      # Value at Risk at 99.5 %
r.tvar(0.995)     # Tail VaR (Expected Shortfall) at 99.5 %

# Export samples to pandas for custom analysis
df = r.to_dataframe()   # DataFrame with column "pv"
df["pv"].describe()
```

### Mortality-law fitting

```python
import numpy as np
from pylifecontingencies import available_mortality_laws, fit_mortality_law

fit = fit_mortality_law(lt, "gompertz_makeham", ages=np.arange(40, 90))
fit.params_dict, fit.rmse
available_mortality_laws()
```

---

## Development and release

GitHub Actions workflows live under `.github/workflows/`:

- `ci.yml` runs the main pytest matrix on Python 3.10, 3.11, and 3.12, plus coverage and distribution builds.
- `r-parity.yml` installs R + `lifecontingencies` and runs the parity tests that compare Python results against the R package.
- `publish.yml` builds and publishes to PyPI on tags matching `v*` after the test suite passes.

For publishing, configure GitHub Trusted Publishing for the PyPI project, then create a version tag such as:

```bash
git tag v0.1.0
git push origin v0.1.0
```

---

## Quick start — static life table

```python
from pylifecontingencies import load_table, ActuarialTable
from pylifecontingencies import axn, Axn, Exn, AExn, IAxn, DAxn, exn

# Load a bundled table
lt = load_table("soa_ilt")            # SOA Illustrative Life Table
at = ActuarialTable(lt, interest=0.06)

# Whole-life annuity-due at age 40
axn(at, x=40)                         # ä_40

# 20-year term insurance
Axn(at, x=40, n=20)                   # A^1_{40:20|}

# 20-year endowment insurance
AExn(at, x=40, n=20)                  # A_{40:20|}

# 20-year pure endowment
Exn(at, x=40, n=20)                   # _20 E_40

# Increasing whole-life insurance
IAxn(at, x=40)                        # (IA)_40

# 20-year decreasing term
DAxn(at, x=40, n=20)                  # (DA)^1_{40:20|}

# Curtate future lifetime expectation
exn(at, x=40)                         # e_40
```

### Semi-annual payments (k=2)

```python
axn(at, x=40, n=20, k=2)             # ä^(2)_{40:20|} via UDD
Axn(at, x=40, n=20, k=2)             # A^(2)_{40:20|} via UDD
```

---

## Quick start — dynamic life tables

```python
from pylifecontingencies.dynamic import MortalityRates, LeeCarter, ProjectedLifeTable

# Build a rates surface from a DataFrame (ages as index, years as columns, values = log(mx))
rates = MortalityRates.from_dataframe(df_log_mx)

# Fit Lee-Carter
lc = LeeCarter().fit(rates)
print(lc.ax)   # age-specific levels
print(lc.bx)   # age-specific sensitivities
print(lc.kt)   # period index

# Forecast 50 years ahead with 95% bootstrap prediction interval
forecast = lc.forecast(horizon=50, n_bootstrap=500, ci=0.95)

# Build a cohort life table for someone born in 1985
cohort_lt = ProjectedLifeTable(forecast, birth_year=1985).to_life_table()
at_cohort = ActuarialTable(cohort_lt, interest=0.03)
axn(at_cohort, x=40)   # cohort-true annuity at 40
```

---

## Stochastic PV simulation

```python
from pylifecontingencies import load_table, ActuarialTable, simulate_pv

lt = load_table("soa_ilt")
at = ActuarialTable(lt, interest=0.03)

# Annual term insurance — PV distribution
r = simulate_pv(at, x=40, n=20, benefit="term", n_sim=50_000, random_state=42)
print(r.mean, r.std)
print(r.quantile(0.95))

# Monthly annuity (k=12 payments per year, UDD)
r_monthly = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, n_sim=50_000, random_state=42)

# 10-year deferred whole-life annuity (pension, m=10 deferral)
r_deferred = simulate_pv(at, x=40, benefit="annuity", m=10, n_sim=50_000, random_state=42)

# Combine: monthly pension starting in 10 years
r_pension = simulate_pv(at, x=40, n=20, benefit="annuity", k=12, m=10, n_sim=50_000)

# Export to pandas for custom analysis
df = r.to_dataframe()   # DataFrame with column "pv"
df["pv"].hist(bins=30)
df["pv"].describe()

# Convenience method on the table object
r_ann = at.simulate_pv(x=40, n=20, benefit="annuity", k=12, n_sim=10_000)
```

Supported benefit types: `term`, `whole` / `whole_life`, `annuity`,
`pure_endowment`, `endowment`, `increasing`, `decreasing`.

`k > 1` (fractional payments) and `m > 0` (deferral) are supported for `annuity`.
`m > 0` is also supported for all insurance benefit types.

---

## Mortality-law graduation

```python
import numpy as np
from pylifecontingencies import (
    available_mortality_laws,
    load_table,
    fit_mortality_law,
    GompertzMakeham,
    HeligmanPollard,
)

lt = load_table("soa_ilt")

# String dispatch
gm_fit = fit_mortality_law(lt, "gompertz_makeham", ages=np.arange(40, 90))
print(gm_fit.params_dict)
print(gm_fit.rmse, gm_fit.aic)

# Explicit law object
hp_fit = fit_mortality_law(lt, HeligmanPollard(), ages=np.arange(1, 90))
df_fit = hp_fit.to_dataframe()
available_mortality_laws()
```

`MortalityLawFit` stores fitted parameters, observed/fitted `q_x`, residuals,
and goodness-of-fit metrics (`loglik`, `AIC`, `BIC`, `RMSE`, `MAE`). The current
implementation is Python-only. R packages can be used as inspiration for future
models, but they are not required at runtime.

```python
from pylifecontingencies import available_mortality_laws

available_mortality_laws()
```

---

## Interest-rate utilities

```python
from pylifecontingencies import InterestRate

ir = InterestRate(i=0.05)
ir.v           # 0.952...  discount factor
ir.delta       # 0.04879...  force of interest
ir.d           # 0.04762...  annual discount rate
ir.i_m(12)     # monthly nominal rate
ir.d_m(12)     # monthly nominal discount rate

InterestRate.from_delta(0.05)    # from force of interest
InterestRate.from_discount(0.04) # from annual discount rate
```

---

## Demographic functions

```python
from pylifecontingencies import pxt, qxt, dxt, mxt, Lxt, Tx

lt = load_table("soa_ilt")
pxt(lt, x=40, t=10)   # _10 p_40
qxt(lt, x=40, t=10)   # _10 q_40
exn(lt, x=40)         # e_40  (curtate)
```

---

## Bundled tables

Over 100 mortality tables are shipped with the package and discoverable at runtime:

```python
from pylifecontingencies import list_tables, load_table

list_tables()          # returns all available names
lt = load_table("at_2000_female")
lt = load_table("ibge_2020_homens")
lt = load_table("up_94_male")
```

### CSV tables (automatically discovered)

Selected groups:

| Group | Examples |
|-------|---------|
| SOA | `soa_ilt` |
| AT (Annuity 2000) | `at_2000_female`, `at_2000_male`, `at_49_female`, `at_49_male`, `at_83_female_basic`, … |
| UP / RP (Group annuity) | `up_84_f`, `up_84_m`, `up_94_female`, `up_94_male`, `rp_2000_female`, `rp_2000male`, … |
| GAM | `gam_71_female`, `gam_71_male`, `gam_83_female_suav_10`, `gam_94_female`, … |
| CSO | `cso_41`, `cso_58`, `cso58_female`, `cso58_male`, `cso80` |
| BR-EMS (Sobrevivência) | `br_emssb_2010_m/f`, `br_emssb_2015_m/f`, `br_emssb_2021_m/f` |
| BR-EMS (Mortalidade) | `br_emsmt_2010_m/f`, `br_emsmt_2015_m/f`, `br_emsmt_2021_m/f` |
| IBGE | `ibge_2006_ambos_os_sexos`, `ibge_2020_homens`, `ibge_2020_mulheres`, … |
| Historical | `american_experience`, `bentzien`, `muller`, `rentiers_francais`, `zimmermann`, … |

All CSV tables have `age` and `qx` columns; `x_min` is inferred automatically from
the first age in the file.

### Multi-column R tables

Some bundled parquet datasets contain several sub-tables in one file. Use
`list_columns(name)` to inspect the available columns and `load_table(..., column=...)`
to select one:

```python
from pylifecontingencies import load_table, list_columns

list_columns("demoUsa")
# ['USSS2007M', 'USSS2007F', 'USSS2000M', 'USSS2000F', 'USSS1990M', 'USSS1990F']

lt_usa = load_table("demoUsa", column="USSS2007M")
lt_ger = load_table("demoGermany", column="qxMale")
```

Single-column parquet tables such as `soa08`, `AM92Lt`, and `AF92Lt` can be loaded
directly:

```python
lt = load_table("soa08")
```

Additional tables (AM92, AF92, demoUsa, etc.) can be imported from R using the provided conversion script:

```bash
# requires R + lifecontingencies + rpy2 + pyarrow
python scripts/convert_rda_to_parquet.py
```

---

## Comparison with R lifecontingencies

| R | Python |
|---|--------|
| `axn(at, x=40, n=20)` | `axn(at, x=40, n=20)` |
| `Axn(at, x=40, n=20)` | `Axn(at, x=40, n=20)` |
| `Exn(at, x=40, n=20)` | `Exn(at, x=40, n=20)` |
| `exn(lt, x=40)` | `exn(lt, x=40)` |
| `pxt(lt, x=40, t=5)` | `pxt(lt, x=40, t=5)` |
| `new("lifetable", x=..., lx=..., name=...)` | `LifeTable(lx, x_min=0, name=...)` |
| `new("actuarialtable", ..., interest=0.06)` | `ActuarialTable(lt, interest=0.06)` |

---

## Scope and roadmap

**Current:** Single-life EPVs, stochastic PV simulation (k-thly payments, deferral, visualisation, VaR/TVaR, pandas export), mortality-law fitters, interest-rate utilities, demographic functions, bundled tables, Lee-Carter and CBD M5 mortality forecasting.

**Planned next:** Multi-decrement tables, Renshaw-Haberman and APC forecasting models, and broader stochastic simulation equivalents to `rLifeContingencies`.

## License

GPL-2.0 — matching the upstream `lifecontingencies` R package.
