ptlasso
Python implementation of the Pretrained Lasso — a two-step procedure for fitting sparse linear models when samples belong to distinct groups, leveraging shared structure across groups via pretraining.
Use ptlasso when your dataset has natural groups (cohorts, sites, populations) and you want group-specific models that still borrow strength from a shared signal — outperforming both a single pooled model and fully independent per-group models.
Craig, E., Pilanci, M., Le Menestrel, T., Narasimhan, B., Rivas, M. A., Gullaksen, S. E., ... & Tibshirani, R. (2025). Pretraining and the lasso. Journal of the Royal Statistical Society Series B: Statistical Methodology, qkaf050.
How it works
Standard group-specific Lasso models are fit independently per group, ignoring shared signal. The Pretrained Lasso fits in two steps:
Step 1 — Overall model. Fit a Lasso on all samples to capture shared structure:
$$\hat{\beta}^{\text{overall}} = \arg\min_\beta \frac{1}{2n}|y - X\beta|^2 + \lambda|\beta|_1$$
Step 2 — Group models. For each group $k$, fit a Lasso with an offset equal to $\alpha$ times the overall model's linear predictor:
$$\hat{\beta}^{(k)} = \arg\min_\beta \frac{1}{2n_k}|y^{(k)} - \underbrace{\alpha \cdot X^{(k)}\hat{\beta}^{\text{overall}}}_{\text{offset}} - X^{(k)}\beta|^2 + \lambda_k|\beta|_1$$
The parameter $\alpha \in [0, 1]$ controls the pretraining strength:
- $\alpha = 0$: group models explain residuals from the overall model (maximum pretraining)
- $\alpha = 1$: independent per-group models (no pretraining)
- $\alpha \in (0, 1)$: group models are anchored to the overall fit
Final prediction for group $k$: $\hat{y}^{(k)} = \alpha \cdot X^{(k)}\hat{\beta}^{\text{overall}} + X^{(k)}\hat{\beta}^{(k)}$
Supports gaussian, binomial, and multinomial families.
Installation
pip install ptlasso
Requires Python ≥ 3.9 and adelie for the underlying Lasso solver, which supports fitting with offsets (unlike scikit-learn).
Quick start
import numpy as np
from ptlasso import PretrainedLasso, PretrainedLassoCV
rng = np.random.default_rng(42)
n, p, k = 300, 100, 3
X = rng.standard_normal((n, p))
groups = rng.integers(0, k, size=n)
beta = np.zeros(p)
beta[:5] = [2, -1.5, 1, -0.8, 0.5]
y = X @ beta + 0.5 * rng.standard_normal(n)
# Fixed alpha
model = PretrainedLasso(alpha=0.5)
model.fit(X, y, groups)
print(model)
PretrainedLasso(alpha=0.5, family='gaussian', overall_lambda='lambda.1se', fit_intercept=True, lmda_path_size=100)
family : gaussian
n_features : 100
n_groups : 3
overall |Ŝ| : |Ŝ| = 6 / 100 [0, 1, 2, 3, 4, ...]
pretrain |Ŝ| : 0: |Ŝ|=36, 1: |Ŝ|=42, 2: |Ŝ|=19
overall λ : lambda.1se (idx 34)
print("R²:", model.score(X, y, groups))
# R²: 0.9827
# Cross-validate over alpha
cv = PretrainedLassoCV(alphas=[0.0, 0.25, 0.5, 0.75, 1.0])
cv.fit(X, y, groups)
print("Best alpha:", cv.alpha_)
# Best alpha: 0.0
Families
# Binary classification
model = PretrainedLasso(alpha=0.5, family="binomial")
model.fit(X, y_binary, groups)
probs = model.predict(X, groups) # shape (n,), P(y=1)
# Multi-class classification (integer labels 0..K-1)
model = PretrainedLasso(alpha=0.5, family="multinomial")
model.fit(X, y_multiclass, groups)
probs = model.predict(X, groups) # shape (n, K)
Feature names and group labels
Both fit() methods accept human-readable names. pandas DataFrames are supported natively — column names are picked up automatically.
import pandas as pd
X_df = pd.DataFrame(X, columns=[f"gene_{i}" for i in range(p)])
group_labels = {0: "control", 1: "treated_A", 2: "treated_B"}
model = PretrainedLasso(alpha=0.5)
model.fit(X_df, y, groups, group_labels=group_labels)
# overall |Ŝ| : |Ŝ| = 5 / 100 [gene_0, gene_1, gene_2, gene_3, gene_4]
# pretrain |Ŝ| : control: |Ŝ|=5, treated_A: |Ŝ|=4, treated_B: |Ŝ|=6
Inspecting the support
The support is the set of non-zero variables selected by the model.
from ptlasso import (
get_overall_support,
get_pretrain_support,
get_pretrain_support_split,
get_individual_support,
)
get_overall_support(model) # features from the overall model
get_pretrain_support(model) # union across pretrained group models
get_pretrain_support(model, common_only=True) # features selected by >50% of groups
get_pretrain_support(model, groups=[0, 1]) # restrict to specific groups
get_individual_support(model) # features from no-pretraining baselines
common, indiv = get_pretrain_support_split(model)
# common : features from the overall model (stage 1)
# indiv : additional features picked up by group models (stage 2)
Evaluating all sub-models at once
result = model.evaluate(X_test, y_test, groups_test)
# {"pretrain": {"predictions": ..., "score": ...},
# "individual": {"predictions": ..., "score": ...},
# "overall": {"predictions": ..., "score": ...}}
Retrieving coefficients
coefs = model.get_coef() # all sub-models
coefs["overall"] # {"coef": ndarray, "intercept": ndarray}
coefs["pretrain"]["control"] # {"coef": ndarray, "intercept": ndarray}
coefs["individual"]["treated_A"]
model.get_coef(model="pretrain") # just pretrain sub-dict
CV details
cv = PretrainedLassoCV(
alphas=[0.0, 0.25, 0.5, 0.75, 1.0],
n_folds=10,
alphahat_choice="overall", # or "mean" (unweighted mean of per-group CV errors)
family="gaussian",
overall_lambda="lambda.1se", # or "lambda.min"
foldid=my_foldid, # optional: custom integer fold assignments
)
cv.fit(X, y, groups)
cv.alpha_ # globally best alpha
cv.varying_alphahat_ # {group: best_alpha} per group
cv.cv_results_ # {alpha: mean CV loss}
cv.cv_results_se_ # {alpha: SE of CV loss}
cv.cv_results_per_group_ # {alpha: {group: mean CV loss}}
cv.cv_results_mean_ # {alpha: unweighted mean of per-group losses}
cv.cv_results_wtd_mean_ # {alpha: size-weighted mean of per-group losses}
cv.cv_results_individual_ # CV loss for individual (no-pretraining) baseline
cv.cv_results_overall_ # CV loss for overall model baseline
cv.best_estimator_ # PretrainedLasso fitted with alpha_
cv.all_estimators_ # {alpha: PretrainedLasso} for varying-alpha prediction
# Predict using each group's own best alpha
cv.predict(X, groups, alphatype="varying")
cv.evaluate(X, y, groups, alphatype="varying")
Plotting
from ptlasso import plot_cv, plot_paths
plot_cv(cv) # CV loss curve over alpha with ±1 SE band
plot_paths(model) # regularisation paths for all sub-models
Saving and loading models
PretrainedLasso and PretrainedLassoCV can be serialised with joblib:
import joblib
joblib.dump(model, "model.pkl")
model = joblib.load("model.pkl")
Avoid
pickledirectly — the underlying adelie solver stores C++ objects that are not natively picklable. ptlasso handles this transparently through joblib. Usingpickledirectly will raise aTypeError.
API reference
Full documentation at thomaslemenestrel.com/ptlasso.
Citation
@article{craig2025pretraining,
title = {Pretraining and the lasso},
author = {Craig, Erin and Pilanci, Mert and Le Menestrel, Thomas and Narasimhan, Balasubramanian and Rivas, Manuel A. and Gullaksen, Stein-Erik and Tibshirani, Robert},
journal = {Journal of the Royal Statistical Society Series B: Statistical Methodology},
pages = {qkaf050},
year = {2025}
}
License
MIT
Metadata
Release files for ptlasso 0.8.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| ptlasso-0.8.0.tar.gz | 40.8 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| ptlasso-0.8.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 79.1 kB
Release files / ptlasso-0.8.0.tar.gz
| Download URL | ptlasso-0.8.0.tar.gz |
|---|---|
| Size | 40.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
920478ee4fcb543ba86b6192eecf5b6855d97697d4e58ce73edf97fc3a08cad3
|
|
BLAKE2b-256 checksum How to use checksums |
dbe3bfe55ed2ee5a4ad722283dd8f554b31b936d72377d4518e4c45bc1848c2a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 24, 2026.
Transparency logRelease files / ptlasso-0.8.0-py3-none-any.whl
| Download URL | ptlasso-0.8.0-py3-none-any.whl |
|---|---|
| Size | 38.3 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6b907429f723717ed1fe892071c520f1b243dadfbc262c6f8ea099d38e74f515
|
|
BLAKE2b-256 checksum How to use checksums |
22cf209121a2695bf8a1e974b0f30d58233f439a9ae0f941a3dfc494e86dc88b
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on May 24, 2026.
Transparency log