PyCDFt
CDF-t statistical downscaling / bias correction, vectorized over grid cells (pure numpy).
PyCDFt is a generic, dependency-light implementation of the CDF-t
(Cumulative Distribution Function transform) method of
Michelangeli, Vrac & Loukos (2009), with the delta-shift and
tail conventions of the R CDFt package and of
ibicus. It operates on plain
numpy arrays — 1-D time series or (T, N) fields (any trailing dimensions
are treated as independent cells) — in one vectorized call, and is derived
from a production grid-scale implementation that has been run at
1056 × 1056 cells × 29-year calibration × 86-year target.
import numpy as np
from pycdft import CDFt
rng = np.random.default_rng(7)
obs = rng.normal(0.0, 1.0, 1000) # observations (calibration period)
cm_hist = rng.normal(3.0, 1.0, 1000) # model, calibration period
cm_future = rng.normal(4.0, 1.0, 1000) # model, target period
model = CDFt(n_quantiles=1001, tail="linear", delta_shift="additive")
ds = model.fit_transform(obs, cm_hist, cm_future) # (1000,) downscaled series
Installation
pip install pycdft # from PyPI (numpy only)
pip install "pycdft[gpu]" # + optional torch GPU backend
# or from this repository:
pip install . # core only
pip install -e ".[test]" # editable, with the test tools
Requires Python ≥ 3.9 and numpy>=1.20. The core has no other
dependencies (scipy, xarray, pandas and matplotlib are never required;
matplotlib is only used by the optional examples/, ibicus only by the
optional cross-validation tests).
Optional GPU backend. pip install .[gpu] adds an optional torch-based
backend (pycdft.gpu) that mirrors this API and runs the same math on CUDA
/ AMD ROCm / Apple MPS / CPU devices (numpy in, numpy out by default; the
results match the numpy backend to ~1e-15). See docs/gpu.md
for the supported devices, the installation per hardware (CUDA / ROCm / CPU
torch builds), and the verified equivalence.
Documentation
- docs/design.md — the math, conventions, design choices and the measured comparison with the reference implementations;
- docs/gpu.md — the optional torch GPU backend (
pycdft.gpu); - docs/guide.html — a plain-language companion guide (no statistics background needed).
What is CDF-t?
CDF-t (Michelangeli et al. 2009) corrects both the distribution and its change between the calibration and target periods, by defining the target observation CDF as
F_o^tar = F_o^cal ∘ (F_m^cal)^{-1} ∘ F_m^tar
(here m = model, o = observations, cal = calibration period,
tar = target period, F = CDF and q = its inverse, the quantile
function), so that each model value x of the target period is mapped to
y(x) = q_m^tar( F_m^cal( q_o^cal( F_m^tar(x) ) ) )
where F_m, F_o are the model and observation CDFs, q the quantile
(inverse CDF) functions, cal/tar the calibration and target periods.
In practice the composition is evaluated on a uniform probability grid
p = linspace(0, 1, Q) (default Q = 1001, endpoints included): the
calibration mapping R = F_m^cal(q_o^cal(p)) is computed once, and the
transfer curve (x_grid, T) = (q_m^tar(p), q_m^tar(R)) is interpolated per
cell at the target values. See docs/design.md for the
full math, conventions and a detailed comparison with the reference
implementations.
Conventions (matching the reference implementations)
| aspect | convention |
|---|---|
| ECDF | right-continuous step F(x) = #{finite values ≤ x} / n (NaNs excluded) — identical to the R CDFt package and to ibicus ecdf_method="step_function" |
| quantiles | type-7 linear (numpy default method="linear" = R quantile(type=7)), re-implemented bit-for-bit on pre-sorted arrays |
| delta shift | per-cell additive x + (mean(obs) − mean(cm_hist)) (default) or multiplicative x * (mean(obs) / mean(cm_hist)), applied to the model series (calibration and target) before the transfer; "none" disables it (ibicus "no_shift") |
| output frame | the shifted frame (the mean bias is removed, the model change calibration → target is preserved) |
| grid | linspace(0, 1, Q) with Q = 1001; the transfer is interpolated in x-space on the target's own value grid |
| tails | "clip" = constant at the transfer endpoints; "linear" = 30-rank windowed least-squares slope at the first/last unsaturated rank, capped at 2 × the median interior slope (default) |
| NaN | excluded from ECDF counts and quantile interpolation; NaN inputs pass through; cells with an empty (all-NaN) calibration map entirely to NaN |
Quick start
import numpy as np
from pycdft import CDFt
rng = np.random.default_rng(0)
obs, cm_hist, cm_future = (rng.normal(m, 1.0, 2000) for m in (0.0, 3.0, 4.0))
# 1-D series ---------------------------------------------------------------
ds = CDFt().fit_transform(obs, cm_hist, cm_future)
# (T, N) field: every cell (column) is corrected independently -------------
per_cell = np.linspace(-2.0, 2.0, 64) # per-cell spatial offset
obs2d, ch2d, cf2d = (rng.normal(0.0, 1.0, (1000, 64)) + per_cell * j
for j in (0, 3, 4)) # dataset bias grows with j
ds2d = CDFt().fit_transform(obs2d, ch2d, cf2d) # (1000, 64)
# options ------------------------------------------------------------------
ds_clip = CDFt(tail="clip").fit_transform(obs, cm_hist, cm_future)
ds_none = CDFt(delta_shift="none").fit_transform(obs, cm_hist, cm_future)
ds_hist = CDFt(use_histograms=True, n_bins=2000).fit_transform(obs, cm_hist,
cm_future)
# fit / transform separately (reuse the calibration) -----------------------
model = CDFt().fit(obs, cm_hist)
ds_a = model.transform(cm_future) # identical to fit_transform
cm_future2 = rng.normal(4.5, 1.0, 1000) # another scenario / member
ds_b = model.transform(cm_future2) # same calibration, new target
# one-shot functional API --------------------------------------------------
from pycdft import cdft_transform
ds = cdft_transform(obs, cm_hist, cm_future, n_quantiles=1001)
Low-level API
The building blocks are exposed in pycdft.math (kernels) and pycdft.core
(pipeline). Every kernel accepts (T, N) arrays and is vectorized per column
(cell); workers > 1 threads the per-cell kernels (results are identical,
numpy releases the GIL):
from pycdft import calibrate, apply_cdft
from pycdft.math import quantile_grid
p = quantile_grid(1001)
calib = calibrate(cm_hist, obs, p=p, delta_shift="additive")
# -> Calibration(R=(Q, N), delta=(N,), mode="additive", n_quantiles=1001)
pred = apply_cdft(cm_future, calib, p=p, tail="linear") # (T, N)
calibrate takes the model first, the observations second (the reverse of
CDFt.fit). The histogram variants (calibrate_hist, apply_cdft_hist,
use_histograms=True) and the individual kernels are documented in
pycdft.math / pycdft.core.
Comparison with the reference implementations
PyCDFt follows the conventions of the R CDFt package and ibicus, so it
agrees closely with both in the middle of the distribution; the differences
are at the extremes (tail models) and in the exact grid:
| aspect | PyCDFt | R CDFt |
ibicus | SBCK |
|---|---|---|---|---|
| composition | same | same | same | same (histogram margins) |
| ECDF / quantiles | step + type-7 | step + type-7 | configurable (step + linear in the tests) | rank-based + interp1d |
| delta shift | per-cell, 3 modes | additive only | per-series, 3 modes | per-column normalization |
| grid | p-grid Q=1001, x-space interp | value grid npas=1000, dev=2 | none (per value) | adaptive support, dsupp=1000 |
| tails | capped LSQ linear / clip | obs-tail copy + 0/1 padding | clamp | support extension |
| seasonality | none (a monthly mode exists in the production codebase this package is derived from) | none | 31-day + 17y/9y windows (default ON) | none |
| precipitation SSR | no | no | yes (Vrac et al. 2016) | no |
| NaN | robust | robust | not supported | n/a |
| grid scale | vectorized, chunked, cacheable | single series | per-location loop, full arrays | full arrays |
The measured agreement (interior RMSE ~0.005–0.012 vs ibicus; the residual
is the probability-grid discretization of the transfer curve) and the full
analysis of where the implementations differ are in
docs/design.md §4, which is reproduced by
pytest -m cross -s.
Examples
Run from the repository root (with pip install -e ., or set
PYTHONPATH=.):
python examples/example_r_gaussian.py # the R package's Gaussian example
python examples/example_skewed.py # non-Gaussian / skewed case
python examples/example_grid.py # small (T, N) grid with QQ plots
python examples/example_ibicus_compare.py # vs ibicus (needs `pip install ibicus`)
python examples/gpu_example.py # torch GPU backend (needs torch)
Each example prints the summary statistics and saves figures to
examples/figures/ when matplotlib is available.
Tests
pip install -e .[test]
pytest # core suite (numpy only, no extras)
pytest -m cross # cross-validation vs ibicus + the R port
pytest -m "not cross" # core suite only (CI)
pytest -m gpu # torch GPU backend (skipped without torch)
The core suite is pure numpy and does not require ibicus: the ibicus
cross tests skip automatically when ibicus is missing, while the R-port
cross tests need only numpy (R is not required — the R algorithm is ported
in tests/r_cdft_reference.py). The GPU tests are device-agnostic: they
run on CUDA / ROCm / MPS if available and always pass on CPU.
Limitations
- Univariate: each cell (series) is corrected independently; no multivariate, spatial or temporal-consistency constraints are imposed.
- No seasonality: the calibration does not distinguish months or seasons (a monthly mode exists in the production codebase this package is derived from, but it is not part of PyCDFt).
- No precipitation-specific handling: the SSR (singularity stochastic removal) zero-inflation of Vrac et al. (2016) is not implemented.
- Histogram mode is approximate:
use_histograms=Trueand the_histfunctions trade a small accuracy for bounded memory (exact in the limit of many bins; seedocs/design.md§3.1). - No autocorrelation modelling: each series is treated as a single sample; temporal structure (daily/monthly persistence) is not modelled.
References
- Michelangeli, P.-A., Vrac, M. & Loukos, H. (2009). Probabilistic downscaling approaches: Application to wind cumulative distribution functions. Geophys. Res. Lett., 36, L11708. doi:10.1029/2009GL038401
- Vrac, M., Noël, T. & Vautard, R. (2016). Bias correction of precipitation through singularity stochastic removal: Method and application to the CORDEX-Africa domain. J. Geophys. Res. Atmos.
- Vrac, M. & Michelangeli, P.-A. (2021). CDFt: Downscaling and bias correction via CDF transform. R package version 1.2, CRAN. https://cran.r-project.org/package=CDFt
- Cucchi, M. et al. (2024). ibicus: A Python package for statistical bias correction and downscaling. Geosci. Model Dev. (Apache-2.0) https://github.com/ecmwf-projects/ibicus
- Robin, Y. (2018+). SBCK: Statistical Bias Correction Kit. https://github.com/yrobink/SBCK (GPL-3, needs Eigen)
The R CDFt source is GPL (≥ 2); it is reproduced in the tests
(tests/r_cdft_reference.py) solely as a validation reference for the
Apache-2.0 PyCDFt package.
License
Apache-2.0. See LICENSE.
Metadata
Release files for PyCDFt 0.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pycdft-0.3.1.tar.gz | 80.0 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pycdft-0.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 123.6 kB
Release files / pycdft-0.3.1.tar.gz
| Download URL | pycdft-0.3.1.tar.gz |
|---|---|
| Size | 80.0 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
37bef815d5783e048b7f3d8b43db0fa0eff0d15488bf35e0c8d60df4ad6dfedc
|
|
BLAKE2b-256 checksum How to use checksums |
6017a8b0ee91ea22840671bc474295c63cb61273cafda4db7393439acf8975b9
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 13, 2026.
Transparency logRelease files / pycdft-0.3.1-py3-none-any.whl
| Download URL | pycdft-0.3.1-py3-none-any.whl |
|---|---|
| Size | 43.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
6db95ce32c7cb455a3e517e96452e47f334b2980e812867339fb5d279ca9f340
|
|
BLAKE2b-256 checksum How to use checksums |
724f2daf26c9d6ef4c8a78fc0a9c62b251d5f77aa05848bd003cc2010ca28841
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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 Oct 13, 2026.
Transparency log