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
- 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
- Gasparrini A, Armstrong B, Kenward MG. Distributed lag non-linear models. Statistics in Medicine. 2010; 29(21):2224–2234. doi:10.1002/sim.3940
- 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
Release history Release notifications | RSS feed
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
e92a67e096abfac136ece87b4f7ba5c7807ce21bdc8b60ec845fc81be707cad8
|
|
| MD5 |
32b01fba4b6514b69efc2f2850b704a9
|
|
| BLAKE2b-256 |
13d526f2cce882cf45eaff16a6cf948dc81c38f5a1ce90102763c0bb47fce449
|
Provenance
The following attestation bundles were made for dlnm-0.1.1.tar.gz:
Publisher:
publish.yml on Victorivus/PyDLNM
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dlnm-0.1.1.tar.gz -
Subject digest:
e92a67e096abfac136ece87b4f7ba5c7807ce21bdc8b60ec845fc81be707cad8 - Sigstore transparency entry: 2138289740
- Sigstore integration time:
-
Permalink:
Victorivus/PyDLNM@b75cc26954dd63a1856f2c7eba20d21a807ae032 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Victorivus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b75cc26954dd63a1856f2c7eba20d21a807ae032 -
Trigger Event:
release
-
Statement type:
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
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
66e655deac14ad62800c112df75930eed9897c08bd4cb43da1bc77062d6eb001
|
|
| MD5 |
e1b6c2ca0e095310591c73f6a71ee3e6
|
|
| BLAKE2b-256 |
b42c4f77549243e7ab564153dd215e086a44aeafbdfd19b9ecd8e8b3e25ba698
|
Provenance
The following attestation bundles were made for dlnm-0.1.1-py3-none-any.whl:
Publisher:
publish.yml on Victorivus/PyDLNM
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
dlnm-0.1.1-py3-none-any.whl -
Subject digest:
66e655deac14ad62800c112df75930eed9897c08bd4cb43da1bc77062d6eb001 - Sigstore transparency entry: 2138289755
- Sigstore integration time:
-
Permalink:
Victorivus/PyDLNM@b75cc26954dd63a1856f2c7eba20d21a807ae032 -
Branch / Tag:
refs/tags/v0.1.1 - Owner: https://github.com/Victorivus
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@b75cc26954dd63a1856f2c7eba20d21a807ae032 -
Trigger Event:
release
-
Statement type: