What’s new in 0.3.0
tsbootstrap 0.3.0 adds a compiled acceleration backend, named reducers, a
float32 simulation dtype, and bootstrap_reduce_panel for whole-panel
calibration.
Compiled backend (backend="compiled")
Installation
The compiled backend requires the [accel] extra, which installs numba:
# with uv (recommended):
uv add "tsbootstrap[accel]"
# with pip:
pip install "tsbootstrap[accel]"
The default backend="numpy" path is unchanged and has no extra dependencies.
When to use it
The compiled backend covers the observation-resampling methods: IID,
MovingBlock, CircularBlock, StationaryBlock, and
NonOverlappingBlock. It builds every replicate index row in one
replicate-parallel kernel and gathers the output in a single pass. The speedup is
largest when n_bootstraps is large.
For bootstrap_reduce the compiled backend also covers the residual bootstrap
with an AR or VAR model. The materialising bootstrap() path does not
support recursive methods (it would have to build the full path), and no path
supports ARIMA, SieveAR, or TaperedBlock. Any unsupported pairing with
backend="compiled" raises a MethodConfigError before any model fit.
Reproducibility note
The compiled backend uses a distinct counter-based RNG stream (Philox), not the PCG64-based stream of the default numpy backend. The two paths are equal in distribution but not bit-identical. You cannot mix results from the two backends and expect matching numerical values.
To get reproducible results, pin backend and random_state together:
backend="numpy"(default): reproducible across runs with the samerandom_state.backend="compiled": reproducible across runs with the samerandom_stateand the same installednumbaversion, but the numbers differ from the numpy path.
Never mix replicates from the two backends in the same analysis.
Example: bootstrap() with the compiled backend
import numpy as np
from tsbootstrap import bootstrap, MovingBlock
rng = np.random.default_rng(0)
x = rng.standard_normal(500)
# Default numpy path: reproducible, no extra deps
result_np = bootstrap(
x,
method=MovingBlock(block_length="auto"),
n_bootstraps=999,
random_state=0,
)
# Compiled path: faster on large B, distinct RNG stream
result_compiled = bootstrap(
x,
method=MovingBlock(block_length="auto"),
n_bootstraps=999,
random_state=0,
backend="compiled",
)
# The two arrays have the same shape and comparable summary stats,
# but are not numerically identical.
print(result_np.values().shape) # (999, 500)
print(result_compiled.values().shape) # (999, 500)
Example: bootstrap_reduce() with the compiled backend
The compiled backend for bootstrap_reduce() fuses index build, gather, and
reduction in one kernel and never materialises the full (B, n) path. It
requires a named reducer (see Named reducers below).
import numpy as np
from tsbootstrap import bootstrap_reduce, MovingBlock
x = np.random.default_rng(0).standard_normal(500)
result = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=9999,
random_state=0,
backend="compiled",
)
print(result.statistics.shape) # (9999,)
print(result.statistics.mean()) # approximately 0
Named reducers for bootstrap_reduce() and bootstrap_reduce_panel()
In earlier releases statistic had to be a callable. In 0.3.0 it can also be
a string name or a ("quantile", q) tuple. The named reducers are implemented
as optimised built-in functions on both backends and are the only option when
backend="compiled".
Available named reducers
"mean"Column mean of each replicate. For a univariate series this is a scalar per replicate; for a multivariate series it is a vector of length
d. Usesnumpy.ndarray.mean(axis=0)."var"Population variance (ddof=0) per column. Matches
numpy.ndarray.var(axis=0, ddof=0)."std"Population standard deviation (ddof=0) per column. Matches
numpy.ndarray.std(axis=0, ddof=0).("quantile", q)Per-replicate quantile at level
qin[0, 1]. Pass as a two-element tuple("quantile", 0.9)for the 90th percentile within each replicate. Usesnumpy.quantile(values, q, axis=0); for a univariate series this is a scalar per replicate.
Callable statistic (numpy backend only)
An arbitrary callable is still accepted on backend="numpy" (the default).
The compiled backend cannot introspect a Python callable and raises
MethodConfigError if one is supplied together with backend="compiled".
The callable signature is:
statistic(values: ndarray, indices: ndarray | None) -> scalar | ndarray
values is the replicate array, shape (n,) for a univariate series or
(n, d) for multivariate. indices is the integer array of original
observation positions ((n,) int32) for observation-resampling methods, or
None for recursive methods (useful for building out-of-bag masks).
Example: named reducers
import numpy as np
from tsbootstrap import bootstrap_reduce, MovingBlock
x = np.random.default_rng(0).standard_normal(300)
# Built-in mean reducer (numpy path)
r_mean = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=999,
random_state=0,
)
print(r_mean.statistics.shape) # (999,)
# Population standard deviation
r_std = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic="std",
n_bootstraps=999,
random_state=0,
)
print(r_std.statistics.shape) # (999,)
# 90th-percentile within each replicate (median over residuals, etc.)
r_q90 = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic=("quantile", 0.9),
n_bootstraps=999,
random_state=0,
)
print(r_q90.statistics.shape) # (999,)
# Custom callable: numpy backend only
def trimmed_mean(values, indices):
lo, hi = np.quantile(values, [0.1, 0.9])
mask = (values >= lo) & (values <= hi)
return values[mask].mean()
r_trim = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic=trimmed_mean,
n_bootstraps=999,
random_state=0,
)
print(r_trim.statistics.shape) # (999,)
float32 simulation dtype (dtype="float32")
Both bootstrap() and bootstrap_reduce() accept a dtype keyword that
controls the precision of the replicate tensor returned to the caller.
dtype="float64"(default)All internal computation and the returned array use 64-bit float. No behaviour change from 0.2.x.
dtype="float32"The returned simulation/path tensor is cast to 32-bit float, halving its memory footprint compared to the default. Model fits, autocovariance estimation, and every internal reduction always run in float64. The float32 output is a faithful down-cast of the float64 computation, not a different numerical path.
Use dtype="float32" when peak memory is the constraint, for example when
materialising a large (B, n) array from bootstrap() with a large
n_bootstraps, and the slight loss of trailing-digit precision is acceptable.
import numpy as np
from tsbootstrap import bootstrap, bootstrap_reduce, MovingBlock
x = np.random.default_rng(0).standard_normal(1000)
# float32 replicate tensor: half the memory of float64 for large B
result = bootstrap(
x,
method=MovingBlock(block_length="auto"),
n_bootstraps=9999,
random_state=0,
dtype="float32",
)
print(result.values().dtype) # float32
print(result.values().shape) # (9999, 1000)
# float32 also works with bootstrap_reduce; the statistic buffer stays float64
r = bootstrap_reduce(
x,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=9999,
random_state=0,
dtype="float32",
)
print(r.statistics.dtype) # float64 (reduction always float64)
print(r.statistics.shape) # (9999,)
bootstrap_reduce_panel(): whole-panel calibration
bootstrap_reduce_panel() is a new entry point for
bootstrapping an entire collection of (possibly unequal-length) series at once
and reducing each replicate of each series to a statistic, in a single call.
Signature
from tsbootstrap import bootstrap_reduce_panel
result = bootstrap_reduce_panel(
panel, # list of arrays, or a flat array + indptr
*,
indptr=None, # CSR offsets when panel is flat; None when panel is a list
method, # observation method spec (IID or block family)
statistic, # named reducer, ("quantile", q) tuple, or callable
n_bootstraps=999,
random_state=None,
dtype="float64",
backend="numpy",
)
Input forms
bootstrap_reduce_panel accepts two equivalent input representations.
List of per-series arrays (recommended for most callers):
Pass panel as a Python list where each element is the observations for one
series. Series may have different lengths (ragged). Leave indptr=None.
import numpy as np
from tsbootstrap import bootstrap_reduce_panel, MovingBlock
rng = np.random.default_rng(0)
# Three series of different lengths
series_a = rng.standard_normal(200)
series_b = rng.standard_normal(350)
series_c = rng.standard_normal(150)
result = bootstrap_reduce_panel(
[series_a, series_b, series_c],
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=999,
random_state=0,
)
print(result.statistics.shape) # (999, 3)
Flat array with explicit CSR offsets (zero-copy from pre-packed data):
Pass the concatenated observations as a flat (total_N,) or (total_N, d)
array and an indptr array of shape (num_series + 1,) whose consecutive
differences give each series length. This is the CSR (compressed sparse row)
layout used by many array frameworks.
import numpy as np
from tsbootstrap import bootstrap_reduce_panel, MovingBlock
# Same three series, packed into a single flat array
flat = np.concatenate([series_a, series_b, series_c])
indptr = np.array([0, 200, 550, 700]) # lengths: 200, 350, 150
result = bootstrap_reduce_panel(
flat,
indptr=indptr,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=999,
random_state=0,
)
print(result.statistics.shape) # (999, 3)
Output shape
.statistics has shape (n_bootstraps, num_series, |theta|) where
|theta| is the shape of the per-replicate per-series statistic. For a
univariate panel and a scalar statistic (such as "mean"), the trailing
axis is collapsed and the result is (n_bootstraps, num_series). For a
multivariate panel with d columns and a scalar statistic, the shape is
(n_bootstraps, num_series, d).
import numpy as np
from tsbootstrap import bootstrap_reduce_panel, MovingBlock
rng = np.random.default_rng(1)
# Univariate panel: statistic shape collapses
panel_1d = [rng.standard_normal(n) for n in [100, 200, 150]]
r = bootstrap_reduce_panel(
panel_1d,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=500,
random_state=1,
)
print(r.statistics.shape) # (500, 3)
# Multivariate panel: trailing axis kept
panel_2d = [rng.standard_normal((n, 2)) for n in [100, 200, 150]]
r2 = bootstrap_reduce_panel(
panel_2d,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=500,
random_state=1,
)
print(r2.statistics.shape) # (500, 3, 2)
Reproducibility note
Reproducibility in bootstrap_reduce_panel is tied to both the seed and the
slot order of the panel. Each series is assigned an RNG stream keyed by its
position in the panel list. Changing the panel order or membership reassigns the
per-slot streams, so those series’ replicates change. For reproducible results,
keep the panel order fixed and use the same random_state.
# Reproducible: same seed, same order -> same statistics
r1 = bootstrap_reduce_panel(
[series_a, series_b, series_c],
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=200,
random_state=42,
)
r2 = bootstrap_reduce_panel(
[series_a, series_b, series_c],
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=200,
random_state=42,
)
import numpy as np
assert np.array_equal(r1.statistics, r2.statistics) # True
Supported methods
Only observation-resampling methods are supported: IID, MovingBlock,
CircularBlock, StationaryBlock, and NonOverlappingBlock. Recursive
(model-based) methods such as ResidualBootstrap and SieveAR have no
coherent ragged-panel formulation in this release and raise a MethodConfigError.
Backend
backend="numpy" (default) loops over series calling the per-series
bootstrap_reduce and uses one PCG64 stream per replicate per series. It is
the reproducible default.
backend="compiled" runs a fused, fully parallel panel kernel (Philox RNG)
that is much faster on large panels and large n_bootstraps. It requires the
[accel] extra and a named reducer; an arbitrary callable raises
MethodConfigError. The results are equal in distribution to the numpy path
but not bit-identical.
import numpy as np
from tsbootstrap import bootstrap_reduce_panel, MovingBlock
rng = np.random.default_rng(0)
panel = [rng.standard_normal(n) for n in range(100, 600, 50)] # 10 series
# Compiled path (requires [accel] extra)
result = bootstrap_reduce_panel(
panel,
method=MovingBlock(block_length="auto"),
statistic="mean",
n_bootstraps=9999,
random_state=0,
backend="compiled",
)
print(result.statistics.shape) # (9999, 10)
Quick-reference table
Feature |
|
|
|
|---|---|---|---|
|
yes (default) |
yes (default) |
yes (default) |
|
yes ([accel]) |
yes ([accel], named reducer required) |
yes ([accel], named reducer required) |
|
yes |
yes |
yes |
Named statistic ( |
n/a |
yes |
yes |
|
n/a |
yes |
yes |
Callable statistic |
n/a |
numpy only |
numpy only |
Recursive methods |
yes |
yes |
no (raises error) |
Ragged-length series |
no |
no |
yes |