Skip to main content

Distributed Lag Non-Linear Models for Python

Project description

PyDLNM — Distributed Lag Non-Linear Models for Python

CI PyPI version Python License: GPL v2+

A Python implementation of Distributed Lag Non-Linear Models (DLNMs), based on the widely-used R package [dlnm](https://github.com/gasparrini/dlnm) by Antonio Gasparrini, Ben Armstrong, and Fabian Scheipl.

DLNMs are a modelling framework for describing simultaneously the non-linear and delayed effects of exposures on health outcomes. They are extensively used in environmental epidemiology, pharmacology, and other fields studying lagged exposure-response relationships.

Attribution

This package is a Python port of the original R package:

Gasparrini A. Distributed lag linear and non-linear models in R: the package dlnm. Journal of Statistical Software. 2011; 43(8):1–20. doi:10.18637/jss.v043.i08

Original R source code: https://github.com/gasparrini/dlnm

All core algorithms and methodology are from the original R implementation. If you use this package in academic work, please cite the original publication. Acknowledging this Python adaptation in your citations is also appreciated.

Installation

pip install dlnm

For development:

git clone https://github.com/Victorivus/PyDLNM.git
cd PyDLNM
pip install -e ".[dev]"

Quick Start

import numpy as np
import statsmodels.api as sm
from dlnm import crossbasis, crosspred, logknots, load_chicagoNMMAPS

# Load example data (Chicago NMMAPS)
df = load_chicagoNMMAPS()

# Define the cross-basis for temperature with 21-day lag
lknots = logknots(21, nk=3)
cb_temp = crossbasis(
    df["temp"].values,
    lag=21,
    argvar={"fun": "ns", "df": 4},
    arglag={"fun": "ns", "knots": lknots},
)

# Fit a model (e.g., quasi-Poisson GLM)
# Build design matrix with cross-basis + covariates
time = df["time"].values
X = np.column_stack([np.ones(len(df)), cb_temp, time])
y = df["death"].values

# Use coefficients and vcov directly
# (or extract from a fitted statsmodels model)

Features

Basis Functions

Function Description
ns Natural cubic splines
bs B-splines
ps P-splines (penalised B-splines)
cr Natural cubic regression splines (with penalty)
strata Indicator (dummy) variables
thr Threshold / hockey-stick
integer Categorical coding for integer values
lin Simple linear term
poly Scaled polynomial basis

Core Functions

  • **onebasis(x, fun, ...)** — Create a single-dimension basis matrix
  • **crossbasis(x, lag, argvar, arglag)** — Create a cross-basis matrix (tensor product of variable and lag bases)
  • **crosspred(basis, model, ...)** — Compute predictions (lag-specific, overall, cumulative)
  • **crossreduce(basis, model, type, ...)** — Reduce to uni-dimensional summaries

Utilities

  • **logknots(x, nk)** — Log-spaced knot placement (ideal for lag dimension)
  • **equalknots(x, nk)** — Equally-spaced knot placement
  • **exphist(exp, times, lag)** — Build exposure history matrices
  • **cb_pen(cb)** — Penalty matrices for penalised regression

Plotting

  • **plot_crosspred(pred, ptype)** — 3D surfaces, contour plots, slices, overall effects
  • **plot_crossreduce(red)** — Plot reduced uni-dimensional associations

Datasets

  • **load_chicagoNMMAPS()** — Daily mortality/weather/pollution data, Chicago 1987–2000
  • **load_drug()** — Simulated RCT with time-varying drug doses
  • **load_nested()** — Simulated nested case-control study

Usage Examples

Exposure-Response at a Specific Lag

from dlnm import crossbasis, crosspred, plot_crosspred

# After fitting a model...
pred = crosspred(cb, coef=coef, vcov=vcov, at=np.arange(-20, 35), cen=21)
fig = plot_crosspred(pred, ptype="slices", lag=0)  # Exposure-response at lag 0

Lag-Response at a Specific Exposure

fig = plot_crosspred(pred, ptype="slices", var=30)  # Lag-response at 30°C

3D Surface Plot

fig = plot_crosspred(pred, ptype="3d")

Overall Cumulative Effect

fig = plot_crosspred(pred, ptype="overall")

Reduced to One Dimension

from dlnm import crossreduce, plot_crossreduce

# Overall cumulative exposure-response
red_overall = crossreduce(cb, coef=coef, vcov=vcov, type="overall", cen=21)
fig = plot_crossreduce(red_overall)

# Lag-response at a specific exposure value
red_var = crossreduce(cb, coef=coef, vcov=vcov, type="var", value=30, cen=21)
fig = plot_crossreduce(red_var)

R Compatibility Notes

PyDLNM is designed to produce results numerically identical to R's dlnm package (within floating-point tolerance). Several non-obvious implementation choices were required to match R's behaviour exactly. These are documented here for maintainers and contributors.

1. Natural spline basis — linear extrapolation outside boundary knots

Location: [src/dlnm/basis.py](src/dlnm/basis.py), _ns_basis()

R's splines::ns() extrapolates linearly beyond the boundary knots (zero second derivative, by the natural spline constraint). scipy's BSpline(extrapolate=True) continues the cubic polynomial instead, producing completely different values outside the boundary. This matters whenever prediction points fall outside the training data range (a common case for crosspred).

Fix: After computing the B-spline basis values, points outside [boundary_knots[0], boundary_knots[1]] are replaced with a linear extrapolation anchored at the boundary value and first derivative:

if left_mask.any():
    val_l  = spl(boundary_knots[0])
    slope_l = spl(boundary_knots[0], 1)
    basis[left_mask, i] = val_l + slope_l * (x[left_mask] - boundary_knots[0])

2. Natural spline basis — intercept=False constraint row

Location: [src/dlnm/basis.py](src/dlnm/basis.py), _ns_basis()

R excludes the "intercept" B-spline (B-spline 0, the leftmost) by adding the unit vector e0 = [1, 0, 0, …] as a third constraint row before the QR null-space projection, not an all-ones row.

Fix:

if not intercept:
    e0 = np.zeros((1, n_basis))
    e0[0, 0] = 1.0
    const = np.vstack([const, e0])

3. crosspred — prediction grid ordering (varvec / lagvec)

Location: [src/dlnm/crosspred.py](src/dlnm/crosspred.py), _mkXpred()

R builds the flat prediction vector as rep(predvar, length(predlag)) (the full predvar sequence repeated once per lag) with lagvec = rep(predlag, each=length(predvar)). This means predvar varies fastest (inner loop), predlag slowest (outer loop). The opposite ordering breaks both the allfit accumulation loop and the matfit reshape.

Fix:

varvec = np.tile(predvar, len(predlag))       # predvar repeats for each lag
lagvec = np.repeat(predlag, len(predvar))     # each lag repeated len(predvar) times

4. crosspred — matfit / matse reshape order

Location: [src/dlnm/crosspred.py](src/dlnm/crosspred.py), crosspred()

With the corrected predvar-fastest ordering (point 3), the flat Xpred @ coef vector is laid out as [predvar@lag0, predvar@lag1, …]. Reshaping it directly as (n_pred, n_lag) gives the wrong column assignments. The correct reshape matches R's column-major (Fortran-order) matrix filling:

matfit = (Xpred @ coef).reshape(len(predlag), len(predvar)).T
matse  = np.sqrt(...).reshape(len(predlag), len(predvar)).T

5. Generating R golden-reference fixtures

The test suite compares Python output against fixtures produced by R. To regenerate them, run the script from the PyDLNM repository root:

Rscript tests/fixtures/generate_fixtures.R

On Windows, Rscript may not be in PATH. Use the full path:

Rscript tests\fixtures\generate_fixtures.R

Differences from the R Package

Feature R dlnm Python dlnm
Model fitting Integrated with glm, gam, coxph, etc. Works with statsmodels or manual coef/vcov
Penalised smoothing Via mgcv::gam Penalty matrices provided via cb_pen()
Plotting Base R graphics matplotlib
S3 classes crossbasis, crosspred, etc. Python dataclasses and numpy.ndarray subclasses
Function naming crossbasis, crosspred Same names, snake_case helpers (cb_pen, plot_crosspred)

Dependencies

  • NumPy >= 1.22
  • SciPy >= 1.9
  • pandas >= 1.5
  • matplotlib >= 3.5
  • patsy >= 0.5
  • statsmodels >= 0.13

Development

# Run tests
pytest

# Run tests with coverage
pytest --cov=dlnm --cov-report=html

# Lint
ruff check src/ tests/

# Type check
mypy src/dlnm/

References

  1. Gasparrini A. Distributed lag linear and non-linear models in R: the package dlnm. Journal of Statistical Software. 2011; 43(8):1–20. doi:10.18637/jss.v043.i08
  2. Gasparrini A, Armstrong B, Kenward MG. Distributed lag non-linear models. Statistics in Medicine. 2010; 29(21):2224–2234. doi:10.1002/sim.3940
  3. Gasparrini A. Modeling exposure-lag-response associations with distributed lag non-linear models. Statistics in Medicine. 2014; 33(5):881–899. doi:10.1002/sim.5963

License

GPL-2.0-or-later — same as the original R package.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

dlnm-0.1.1.tar.gz (233.4 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

dlnm-0.1.1-py3-none-any.whl (221.9 kB view details)

Uploaded Python 3

File details

Details for the file dlnm-0.1.1.tar.gz.

File metadata

  • Download URL: dlnm-0.1.1.tar.gz
  • Upload date:
  • Size: 233.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dlnm-0.1.1.tar.gz
Algorithm Hash digest
SHA256 e92a67e096abfac136ece87b4f7ba5c7807ce21bdc8b60ec845fc81be707cad8
MD5 32b01fba4b6514b69efc2f2850b704a9
BLAKE2b-256 13d526f2cce882cf45eaff16a6cf948dc81c38f5a1ce90102763c0bb47fce449

See more details on using hashes here.

Provenance

The following attestation bundles were made for dlnm-0.1.1.tar.gz:

Publisher: publish.yml on Victorivus/PyDLNM

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file dlnm-0.1.1-py3-none-any.whl.

File metadata

  • Download URL: dlnm-0.1.1-py3-none-any.whl
  • Upload date:
  • Size: 221.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for dlnm-0.1.1-py3-none-any.whl
Algorithm Hash digest
SHA256 66e655deac14ad62800c112df75930eed9897c08bd4cb43da1bc77062d6eb001
MD5 e1b6c2ca0e095310591c73f6a71ee3e6
BLAKE2b-256 b42c4f77549243e7ab564153dd215e086a44aeafbdfd19b9ecd8e8b3e25ba698

See more details on using hashes here.

Provenance

The following attestation bundles were made for dlnm-0.1.1-py3-none-any.whl:

Publisher: publish.yml on Victorivus/PyDLNM

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page