👖 Conformal Tights
Conformal Tights is a Python package for Coherent Conformal Prediction✦ that exports:
- 🍬 a scikit-learn meta-estimator that adds coherent conformal prediction of quantiles and intervals to any scikit-learn regressor
- 🔮 a Darts forecaster that adds coherent conformal probabilistic time series forecasting to any scikit-learn regressor
Features
- 🚦 Coherent: quantiles increase monotonically instead of crossing each other
- 🌡️ Conformal: prediction intervals with reliable coverage and accurate quantile predictions
- 🪜 Dynamic: two-level conformal calibration of both absolute and relative residuals
- 👖 Tight: selects the lowest dispersion that provides the desired coverage
- 🎁 Data efficient: requires only a small number of calibration examples to fit
- 🐼 Pandas support: optionally predict on DataFrames and receive DataFrame output
Using
Quick links
Installing
pip install conformal-tights
Predicting quantiles
Conformal Tights exports a meta-estimator called ConformalCoherentQuantileRegressor that you can use to equip any scikit-learn regressor with a predict_quantiles method that predicts conformally calibrated quantiles. Example usage:
from conformal_tights import ConformalCoherentQuantileRegressor
from sklearn.datasets import fetch_openml
from sklearn.model_selection import train_test_split
from xgboost import XGBRegressor
# Fetch dataset and split in train and test
X, y = fetch_openml("ames_housing", version=1, return_X_y=True, as_frame=True, parser="auto")
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.15, random_state=42)
# Create a regressor, equip it with conformal prediction, and fit on the train set
my_regressor = XGBRegressor(objective="reg:absoluteerror")
conformal_predictor = ConformalCoherentQuantileRegressor(estimator=my_regressor)
conformal_predictor.fit(X_train, y_train)
# Predict with the underlying regressor
ŷ_test = conformal_predictor.predict(X_test)
# Predict quantiles with the conformal predictor
ŷ_test_quantiles = conformal_predictor.predict_quantiles(
X_test, quantiles=(0.025, 0.05, 0.1, 0.5, 0.9, 0.95, 0.975)
)
When the input data is a pandas DataFrame, the output is also a pandas DataFrame. For example, printing the head of ŷ_test_quantiles yields:
| house_id | 0.025 | 0.05 | 0.1 | 0.5 | 0.9 | 0.95 | 0.975 |
|---|---|---|---|---|---|---|---|
| 1357 | 114720.3 | 120910.8 | 131770.3 | 156708.2 | 175907.8 | 187996.1 | 205443.4 |
| 2367 | 67382.7 | 80191.7 | 86871.8 | 105807.1 | 118470.3 | 127582.6 | 142321.1 |
| 2822 | 119068.0 | 131864.8 | 138541.6 | 159447.7 | 179178.5 | 197245.5 | 214206.2 |
| 2126 | 93860.7 | 100048.8 | 111389.7 | 134292.7 | 150557.1 | 164595.8 | 182524.1 |
| 1544 | 68959.8 | 81648.8 | 88364.1 | 108298.3 | 122315.3 | 132425.1 | 147109.3 |
Let's visualize the predicted quantiles on the test set:
Expand to see the code that generated the graph above
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%config InlineBackend.figure_format = "retina"
plt.rc("font", family="DejaVu Sans", size=10)
plt.figure(figsize=(8, 4.5))
idx = ŷ_test_quantiles[0.5].sample(50, random_state=42).sort_values().index
x = list(range(1, len(idx) + 1))
x_ticks = [1, *list(range(5, len(idx) + 1, 5))]
for j in range(3):
coverage = round(100 * (ŷ_test_quantiles.columns[-(j + 1)] - ŷ_test_quantiles.columns[j]))
plt.bar(
x,
ŷ_test_quantiles.loc[idx].iloc[:, -(j + 1)] - ŷ_test_quantiles.loc[idx].iloc[:, j],
bottom=ŷ_test_quantiles.loc[idx].iloc[:, j],
color=["#b3d9ff", "#86bfff", "#4da6ff"][j],
label=f"{coverage}% Prediction interval",
)
plt.plot(
x,
y_test.loc[idx],
"s",
label="Actual (test)",
markeredgecolor="#e74c3c",
markeredgewidth=1.414,
markerfacecolor="none",
markersize=4,
)
plt.plot(x, ŷ_test.loc[idx], "s", color="blue", label="Predicted (test)", markersize=2)
plt.xlabel("House")
plt.xticks(x_ticks, x_ticks)
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"${x/1000:,.0f}k"))
plt.gca().tick_params(axis="both", labelsize=10)
plt.gca().spines["top"].set_visible(False)
plt.gca().spines["right"].set_visible(False)
plt.grid(False)
plt.grid(axis="y")
plt.legend(loc="upper left", title="House price", title_fontproperties={"weight": "bold"})
plt.tight_layout()
Predicting intervals
In addition to quantile prediction, you can use predict_interval to predict conformally calibrated prediction intervals. Compared to quantiles, these focus on reliable coverage over quantile accuracy. Example usage:
# Predict an interval for each example with the conformal predictor
ŷ_test_interval = conformal_predictor.predict_interval(X_test, coverage=0.95)
# Measure the coverage of the prediction intervals on the test set
coverage = ((ŷ_test_interval.iloc[:, 0] <= y_test) & (y_test <= ŷ_test_interval.iloc[:, 1])).mean()
print(coverage) # 96.6%
When the input data is a pandas DataFrame, the output is also a pandas DataFrame. For example, printing the head of ŷ_test_interval yields:
| house_id | 0.025 | 0.975 |
|---|---|---|
| 1357 | 107202.8 | 206290.4 |
| 2367 | 66665.1 | 146004.8 |
| 2822 | 115591.8 | 220314.8 |
| 2126 | 85288.1 | 183037.8 |
| 1544 | 67889.9 | 150646.2 |
Forecasting time series
Conformal Tights also exports a Darts forecaster called DartsForecaster that uses a ConformalCoherentQuantileRegressor to make conformally calibrated probabilistic time series forecasts. To demonstrate its usage, let's begin by loading a time series dataset:
from darts.datasets import ElectricityConsumptionZurichDataset
# Load a forecasting dataset
ts = ElectricityConsumptionZurichDataset().load()
ts = ts.resample("h")
# Split the dataset into covariates X and target y
X = ts.drop_columns(["Value_NE5", "Value_NE7"])
y = ts["Value_NE5"] # NE5 = Household energy consumption
# Add categorical covariates to X
X = X.add_holidays(country_code="CH")
X = X.add_datetime_attribute("month")
X = X.add_datetime_attribute("dayofweek")
X = X.add_datetime_attribute("hour")
X_categoricals = ["holidays", "month", "dayofweek", "hour"]
Printing the tail of the covariates time series X.pd_dataframe() yields:
| Timestamp | Hr [%Hr] | RainDur [min] | StrGlo [W/m2] | T [°C] | WD [°] | WVs [m/s] | WVv [m/s] | p [hPa] | holidays | month | dayofweek | hour |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| 2022‑08‑30 20h | 70.2 | 0.0 | 0.0 | 19.9 | 290.2 | 1.7 | 1.5 | 968.5 | 0.0 | 7.0 | 1.0 | 20.0 |
| 2022‑08‑30 21h | 70.1 | 0.0 | 0.0 | 19.5 | 239.2 | 1.0 | 0.7 | 968.1 | 0.0 | 7.0 | 1.0 | 21.0 |
| 2022‑08‑30 22h | 71.3 | 0.0 | 0.0 | 19.5 | 28.9 | 1.5 | 1.3 | 967.9 | 0.0 | 7.0 | 1.0 | 22.0 |
| 2022‑08‑30 23h | 80.4 | 0.0 | 0.0 | 18.9 | 24.3 | 1.6 | 1.1 | 967.9 | 0.0 | 7.0 | 1.0 | 23.0 |
| 2022‑08‑31 00h | 81.6 | 1.0 | 0.0 | 18.7 | 293.5 | 0.9 | 0.3 | 967.8 | 0.0 | 7.0 | 2.0 | 0.0 |
We can now equip a scikit-learn regressor with conformal prediction using ConformalCoherentQuantileRegressor as before, and then equip that conformal predictor with probabilistic time series forecasting using DartsForecaster:
from conformal_tights import DartsForecaster, ConformalCoherentQuantileRegressor
from pandas import Timestamp
from xgboost import XGBRegressor
# Split the dataset into train and test
test_cutoff = Timestamp("2022-06-01")
y_train, y_test = y.split_after(test_cutoff)
X_train, X_test = X.split_after(test_cutoff)
# Now let's:
# 1. Create an sklearn regressor of our choosing, in this case `XGBRegressor`
# 2. Add conformal quantile prediction to the regressor with `ConformalCoherentQuantileRegressor`
# 3. Add probabilistic forecasting to the conformal predictor with `DartsForecaster`
my_regressor = XGBRegressor()
conformal_predictor = ConformalCoherentQuantileRegressor(estimator=my_regressor)
forecaster = DartsForecaster(
model=conformal_predictor,
lags=5 * 24, # Add the last 5 days of the target to the prediction features
lags_future_covariates=[0], # Add the current timestamp's covariates to the prediction features
categorical_future_covariates=X_categoricals, # Convert these covariates to pd.Categorical
)
# Fit the forecaster
forecaster.fit(y_train, future_covariates=X_train)
# Make a probabilistic forecast 5 days into the future by generating 500 forecast paths, where each
# step is sampled from the point-in-time quantiles predicted by the conformal model
quantiles = (0.025, 0.05, 0.1, 0.25, 0.5, 0.75, 0.9, 0.95, 0.975)
forecast = forecaster.predict(
n=5 * 24, future_covariates=X_test, num_samples=500, quantiles=quantiles
)
Printing the head of the forecast quantiles time series forecast.quantile(quantiles).to_dataframe() yields:
| Timestamp | Value_NE5_q0.025 | Value_NE5_q0.050 | Value_NE5_q0.100 | Value_NE5_q0.250 | Value_NE5_q0.500 | Value_NE5_q0.750 | Value_NE5_q0.900 | Value_NE5_q0.950 | Value_NE5_q0.975 |
|---|---|---|---|---|---|---|---|---|---|
| 2022‑06‑01 01h | 19274.5 | 19394.9 | 19516.3 | 19756.8 | 19983.2 | 20185.9 | 20392.7 | 20497.1 | 20591.7 |
| 2022‑06‑01 02h | 19147.0 | 19252.3 | 19396.7 | 19624.3 | 19895.0 | 20130.5 | 20334.6 | 20445.2 | 20582.1 |
| 2022‑06‑01 03h | 19332.1 | 19448.3 | 19632.1 | 19931.0 | 20231.7 | 20486.7 | 20689.0 | 20876.7 | 20999.2 |
| 2022‑06‑01 04h | 21649.3 | 21827.0 | 22195.7 | 22619.9 | 23158.6 | 23470.7 | 23802.8 | 24026.9 | 24246.9 |
| 2022‑06‑01 05h | 24669.7 | 25045.8 | 25279.7 | 25624.5 | 26153.4 | 26797.1 | 27147.0 | 27318.5 | 27487.5 |
Let's visualize the forecast and its prediction interval on the test set:
Expand to see the code that generated the graph above
import matplotlib.pyplot as plt
import matplotlib.ticker as ticker
%config InlineBackend.figure_format = "retina"
plt.rc("font", family="DejaVu Sans", size=10)
plt.figure(figsize=(8, 4.5))
y_train[-2 * 24 :].plot(label="Actual (train)")
y_test[: len(forecast)].plot(label="Actual (test)")
forecast.plot(label="Forecast with\n90% Prediction interval", low_quantile=0.05, high_quantile=0.95)
plt.gca().set_xlabel("")
plt.gca().yaxis.set_major_formatter(ticker.FuncFormatter(lambda x, _: f"{x/1000:,.0f} MWh"))
plt.gca().tick_params(axis="both", labelsize=10)
plt.legend(loc="upper right", title="Energy consumption", title_fontproperties={"weight": "bold"})
plt.tight_layout()
Contributing
Prerequisites
-
Generate an SSH key and add the SSH key to your GitHub account.
-
Configure SSH to automatically load your SSH keys:
cat << EOF >> ~/.ssh/config Host * AddKeysToAgent yes IgnoreUnknown UseKeychain UseKeychain yes ForwardAgent yes EOF
-
Install VS Code and VS Code's Dev Containers extension. Alternatively, install PyCharm.
-
Optional: install a Nerd Font such as FiraCode Nerd Font and configure VS Code or PyCharm to use it.
Development environments
The following development environments are supported:
-
⭐️ GitHub Codespaces: click on Open in GitHub Codespaces to start developing in your browser.
-
⭐️ VS Code Dev Container (with container volume): click on Open in Dev Containers to clone this repository in a container volume and create a Dev Container with VS Code.
-
⭐️ uv: clone this repository and run the following from root of the repository:
# Create and install a virtual environment uv sync --python 3.10 --all-extras # Activate the virtual environment source .venv/bin/activate # Install the pre-commit hooks pre-commit install --install-hooks
-
VS Code Dev Container: clone this repository, open it with VS Code, and run Ctrl/⌘ + ⇧ + P → Dev Containers: Reopen in Container.
-
PyCharm Dev Container: clone this repository, open it with PyCharm, create a Dev Container with Mount Sources, and configure an existing Python interpreter at
/opt/venv/bin/python.
Developing
- This project follows the Conventional Commits standard to automate Semantic Versioning and Keep A Changelog with Commitizen.
- Run
poefrom within the development environment to print a list of Poe the Poet tasks available to run on this project. - Run
uv add {package}from within the development environment to install a run time dependency and add it topyproject.tomlanduv.lock. Add--devto install a development dependency. - Run
uv sync --upgradefrom within the development environment to upgrade all dependencies to the latest versions allowed bypyproject.toml. Add--only-devto upgrade the development dependencies only. - Run
cz bumpto bump the package's version, update theCHANGELOG.md, and create a git tag. Then push the changes and the git tag withgit push origin main --tags.
Metadata
Release files for conformal-tights 0.5.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 | |
|---|---|---|---|
| conformal_tights-0.5.0.tar.gz | 24.6 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| conformal_tights-0.5.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 44.8 kB
Release files / conformal_tights-0.5.0.tar.gz
| Download URL | conformal_tights-0.5.0.tar.gz |
|---|---|
| Size | 24.6 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
346043c85416028bb8af90da239a3ade830fd2343606aff55dc18492558740dd
|
|
BLAKE2b-256 checksum How to use checksums |
402f017bc3d767177e844b5c9a606f52fcc76480df9d84a419430ad6d2689124
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / conformal_tights-0.5.0-py3-none-any.whl
| Download URL | conformal_tights-0.5.0-py3-none-any.whl |
|---|---|
| Size | 20.2 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
1d0b437b08a52ba0a5012763bb4465052497c0d507a281f8c0fc698484f6e338
|
|
BLAKE2b-256 checksum How to use checksums |
fe82e0c81d7fb8cb89331d24f5ef0aac85d65a3eba5282beb894f10a9a45d15e
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
uv/0.10.12 {"installer":{"name":"uv","version":"0.10.12","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|