tsbootstrap 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. {tsbootstrap-0.3.0/src/tsbootstrap.egg-info → tsbootstrap-0.4.0}/PKG-INFO +32 -7
  2. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/README.md +30 -6
  3. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/pyproject.toml +72 -6
  4. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/__init__.py +26 -1
  5. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/adapters/estimators.py +43 -7
  6. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/_compiled.py +24 -0
  7. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/mcp.py +4 -5
  8. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/metadata.py +8 -1
  9. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/methods.py +56 -1
  10. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/model/recursive.py +173 -16
  11. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/uq/__init__.py +20 -0
  12. tsbootstrap-0.4.0/src/tsbootstrap/uq/classical.py +821 -0
  13. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0/src/tsbootstrap.egg-info}/PKG-INFO +32 -7
  14. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap.egg-info/SOURCES.txt +1 -0
  15. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap.egg-info/requires.txt +1 -0
  16. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/LICENSE +0 -0
  17. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/setup.cfg +0 -0
  18. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/adapters/__init__.py +0 -0
  19. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/api.py +0 -0
  20. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/__init__.py +0 -0
  21. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/indices.py +0 -0
  22. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/pwsd.py +0 -0
  23. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/stationary.py +0 -0
  24. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/block/tapered.py +0 -0
  25. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/diagnostics.py +0 -0
  26. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/dispatch.py +0 -0
  27. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/engines/__init__.py +0 -0
  28. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/engines/arma_scipy.py +0 -0
  29. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/engines/var.py +0 -0
  30. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/errors.py +0 -0
  31. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/model/__init__.py +0 -0
  32. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/model/arima.py +0 -0
  33. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/model/fit.py +0 -0
  34. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/model/stability.py +0 -0
  35. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/py.typed +0 -0
  36. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/results.py +0 -0
  37. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/rng.py +0 -0
  38. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/uq/adaptive.py +0 -0
  39. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/uq/calibration.py +0 -0
  40. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/uq/conformal.py +0 -0
  41. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/uq/forecast.py +0 -0
  42. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap/validation.py +0 -0
  43. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap.egg-info/dependency_links.txt +0 -0
  44. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap.egg-info/entry_points.txt +0 -0
  45. {tsbootstrap-0.3.0 → tsbootstrap-0.4.0}/src/tsbootstrap.egg-info/top_level.txt +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: tsbootstrap
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A Python package to generate bootstrapped time series
5
5
  Author-email: Sankalp Gilda <sankalp.gilda@gmail.com>
6
6
  Maintainer-email: Sankalp Gilda <sankalp.gilda@gmail.com>, Franz Kiraly <franz.kiraly@sktime.net>, Benedikt Heidrich <benedikt.heidrich@sktime.net>
@@ -89,6 +89,7 @@ Requires-Dist: pytest; extra == "dev"
89
89
  Requires-Dist: pytest-xdist[psutil]; extra == "dev"
90
90
  Requires-Dist: pytest-cov; extra == "dev"
91
91
  Requires-Dist: pytest-mock; extra == "dev"
92
+ Requires-Dist: pytest-timeout; extra == "dev"
92
93
  Requires-Dist: nbmake; extra == "dev"
93
94
  Requires-Dist: sktime; extra == "dev"
94
95
  Requires-Dist: polars; extra == "dev"
@@ -232,6 +233,15 @@ from tsbootstrap import AR, forecast_intervals
232
233
  lower, upper, median = forecast_intervals(x, model=AR(order=2), horizon=12, alpha=0.1)
233
234
  ```
234
235
 
236
+ For a confidence interval on a statistic of one series, `conf_int` runs the bootstrap
237
+ and reads the interval in one call:
238
+
239
+ ```python
240
+ from tsbootstrap import IID, conf_int
241
+
242
+ lower, upper, point = conf_int(x, "mean", method=IID(), kind="bca", alpha=0.1)
243
+ ```
244
+
235
245
  The conformal pieces (`EnbPIEnsemble` and the calibrators) need the `uq` extra
236
246
  (scikit-learn). The interactive
237
247
  [tutorial gallery](https://tsbootstrap.readthedocs.io/en/latest/tutorials/index.html)
@@ -278,7 +288,7 @@ the `models` extra is missing.
278
288
 
279
289
  ## ⚡ Performance
280
290
 
281
- ![tsbootstrap 0.3.0: speedup over arch and peak-memory reduction](benchmarks/launch_speed_memory.png)
291
+ ![tsbootstrap: speedup over arch and peak-memory reduction](benchmarks/launch_speed_memory.png)
282
292
 
283
293
  *Left: speedup of the compiled reduce path over the arch library on the four overlapping methods. Right: peak memory before and after on the two headline reduce workloads (baseline = materialize every path, then reduce). Regenerate with `python benchmarks/plot_launch.py`.*
284
294
 
@@ -319,7 +329,7 @@ Package layout:
319
329
  | Infrastructure | `rng.py`, `validation.py`, `dispatch.py`, `metadata.py` | deterministic RNG contract, input coercion (incl. the narwhals DataFrame boundary), spec to executor dispatch, method metadata |
320
330
  | Block methods | `block/` | vectorized index kernels, true Politis-Romano stationary, energy-normalized tapering, PWSD block length, OOB primitives |
321
331
  | Model methods | `model/`, `engines/` | model fitting, stability guards, and recursive AR/ARMA/VAR simulation |
322
- | Uncertainty quantification | `uq/` | EnbPI prediction intervals, the static / sliding-window / ACI / NexCP calibrators, and AR forecast intervals |
332
+ | Uncertainty quantification | `uq/` | classical confidence intervals (percentile, basic, studentized, BCa) via `conf_int`, EnbPI prediction intervals, the static / sliding-window / ACI / NexCP calibrators, and AR forecast intervals |
323
333
  | Ecosystem | `adapters/` | skbase / sktime estimator classes over the functional core |
324
334
 
325
335
 
@@ -327,14 +337,13 @@ Package layout:
327
337
 
328
338
  The full, living roadmap is [issue #181](https://github.com/astrogilda/tsbootstrap/issues/181). Highlights:
329
339
 
330
- Near term (v0.2.x):
331
- - Out-of-sample forecast intervals for ARIMA and VAR (v0.2.0 ships AR-only).
332
- - The tutorial gallery and getting-started notebook ([#46](https://github.com/astrogilda/tsbootstrap/issues/46)).
340
+ Near term:
341
+ - Out-of-sample forecast intervals for ARIMA and VAR (currently AR-only).
333
342
  - Python 3.14, once statsmodels publishes a 3.14 wheel ([#202](https://github.com/astrogilda/tsbootstrap/issues/202)).
334
343
 
335
344
  Candidate methods (good first issues):
336
345
  - Generalized block ([#104](https://github.com/astrogilda/tsbootstrap/issues/104)), local block ([#105](https://github.com/astrogilda/tsbootstrap/issues/105)), and frequency-domain ([#107](https://github.com/astrogilda/tsbootstrap/issues/107)) bootstraps.
337
- - Wild and dependent-wild bootstraps, and a GARCH / volatility residual bootstrap.
346
+ - A GARCH / volatility residual bootstrap, and the smooth-kernel dependent-wild bootstrap.
338
347
 
339
348
  Distributed execution (`Dask` / `Spark` / `Ray`), an async layer, and a string-keyed
340
349
  factory were considered and deliberately left out. The library is a CPU-bound,
@@ -476,6 +485,22 @@ explosive paths.
476
485
  `SieveAR` selects an autoregressive order on the original series, then runs the AR recursion;
477
486
  suited to data with autoregressive structure.
478
487
 
488
+ ### Innovation resamplers
489
+ The `innovation` argument on `ResidualBootstrap` and `SieveAR` controls how the centered
490
+ residuals are resampled. It defaults to `IID` (uniform resampling); two wild resamplers relax
491
+ the exchangeability that assumes.
492
+
493
+ - **Wild** (`Wild(distribution=...)`): multiplies each residual in place by a mean-zero,
494
+ unit-variance draw (`e*_t = v_t * e_hat_t`), keeping its time position and magnitude, so it
495
+ stays valid under conditional heteroskedasticity (Wu 1986; Liu 1988; Rademacher default per
496
+ Davidson-Flachaire 2008).
497
+ - **Block-wild** (`BlockWild(block_length=...)`): holds one multiplier constant across each
498
+ block of residuals, so serial dependence left by a misspecified mean survives the resampling
499
+ (piecewise-constant dependent wild bootstrap, Shao 2010).
500
+
501
+ Both require the host model's `burn_in=0` and `initial="fixed"` defaults so the multipliers
502
+ align one-to-one with the residuals.
503
+
479
504
  ### Deferred to a later release
480
505
  Markov resampling, the distribution bootstrap, GARCH/volatility models, and
481
506
  frequency-domain / seasonal block methods are planned for a future version. The
@@ -116,6 +116,15 @@ from tsbootstrap import AR, forecast_intervals
116
116
  lower, upper, median = forecast_intervals(x, model=AR(order=2), horizon=12, alpha=0.1)
117
117
  ```
118
118
 
119
+ For a confidence interval on a statistic of one series, `conf_int` runs the bootstrap
120
+ and reads the interval in one call:
121
+
122
+ ```python
123
+ from tsbootstrap import IID, conf_int
124
+
125
+ lower, upper, point = conf_int(x, "mean", method=IID(), kind="bca", alpha=0.1)
126
+ ```
127
+
119
128
  The conformal pieces (`EnbPIEnsemble` and the calibrators) need the `uq` extra
120
129
  (scikit-learn). The interactive
121
130
  [tutorial gallery](https://tsbootstrap.readthedocs.io/en/latest/tutorials/index.html)
@@ -162,7 +171,7 @@ the `models` extra is missing.
162
171
 
163
172
  ## ⚡ Performance
164
173
 
165
- ![tsbootstrap 0.3.0: speedup over arch and peak-memory reduction](benchmarks/launch_speed_memory.png)
174
+ ![tsbootstrap: speedup over arch and peak-memory reduction](benchmarks/launch_speed_memory.png)
166
175
 
167
176
  *Left: speedup of the compiled reduce path over the arch library on the four overlapping methods. Right: peak memory before and after on the two headline reduce workloads (baseline = materialize every path, then reduce). Regenerate with `python benchmarks/plot_launch.py`.*
168
177
 
@@ -203,7 +212,7 @@ Package layout:
203
212
  | Infrastructure | `rng.py`, `validation.py`, `dispatch.py`, `metadata.py` | deterministic RNG contract, input coercion (incl. the narwhals DataFrame boundary), spec to executor dispatch, method metadata |
204
213
  | Block methods | `block/` | vectorized index kernels, true Politis-Romano stationary, energy-normalized tapering, PWSD block length, OOB primitives |
205
214
  | Model methods | `model/`, `engines/` | model fitting, stability guards, and recursive AR/ARMA/VAR simulation |
206
- | Uncertainty quantification | `uq/` | EnbPI prediction intervals, the static / sliding-window / ACI / NexCP calibrators, and AR forecast intervals |
215
+ | Uncertainty quantification | `uq/` | classical confidence intervals (percentile, basic, studentized, BCa) via `conf_int`, EnbPI prediction intervals, the static / sliding-window / ACI / NexCP calibrators, and AR forecast intervals |
207
216
  | Ecosystem | `adapters/` | skbase / sktime estimator classes over the functional core |
208
217
 
209
218
 
@@ -211,14 +220,13 @@ Package layout:
211
220
 
212
221
  The full, living roadmap is [issue #181](https://github.com/astrogilda/tsbootstrap/issues/181). Highlights:
213
222
 
214
- Near term (v0.2.x):
215
- - Out-of-sample forecast intervals for ARIMA and VAR (v0.2.0 ships AR-only).
216
- - The tutorial gallery and getting-started notebook ([#46](https://github.com/astrogilda/tsbootstrap/issues/46)).
223
+ Near term:
224
+ - Out-of-sample forecast intervals for ARIMA and VAR (currently AR-only).
217
225
  - Python 3.14, once statsmodels publishes a 3.14 wheel ([#202](https://github.com/astrogilda/tsbootstrap/issues/202)).
218
226
 
219
227
  Candidate methods (good first issues):
220
228
  - Generalized block ([#104](https://github.com/astrogilda/tsbootstrap/issues/104)), local block ([#105](https://github.com/astrogilda/tsbootstrap/issues/105)), and frequency-domain ([#107](https://github.com/astrogilda/tsbootstrap/issues/107)) bootstraps.
221
- - Wild and dependent-wild bootstraps, and a GARCH / volatility residual bootstrap.
229
+ - A GARCH / volatility residual bootstrap, and the smooth-kernel dependent-wild bootstrap.
222
230
 
223
231
  Distributed execution (`Dask` / `Spark` / `Ray`), an async layer, and a string-keyed
224
232
  factory were considered and deliberately left out. The library is a CPU-bound,
@@ -360,6 +368,22 @@ explosive paths.
360
368
  `SieveAR` selects an autoregressive order on the original series, then runs the AR recursion;
361
369
  suited to data with autoregressive structure.
362
370
 
371
+ ### Innovation resamplers
372
+ The `innovation` argument on `ResidualBootstrap` and `SieveAR` controls how the centered
373
+ residuals are resampled. It defaults to `IID` (uniform resampling); two wild resamplers relax
374
+ the exchangeability that assumes.
375
+
376
+ - **Wild** (`Wild(distribution=...)`): multiplies each residual in place by a mean-zero,
377
+ unit-variance draw (`e*_t = v_t * e_hat_t`), keeping its time position and magnitude, so it
378
+ stays valid under conditional heteroskedasticity (Wu 1986; Liu 1988; Rademacher default per
379
+ Davidson-Flachaire 2008).
380
+ - **Block-wild** (`BlockWild(block_length=...)`): holds one multiplier constant across each
381
+ block of residuals, so serial dependence left by a misspecified mean survives the resampling
382
+ (piecewise-constant dependent wild bootstrap, Shao 2010).
383
+
384
+ Both require the host model's `burn_in=0` and `initial="fixed"` defaults so the multipliers
385
+ align one-to-one with the residuals.
386
+
363
387
  ### Deferred to a later release
364
388
  Markov resampling, the distribution bootstrap, GARCH/volatility models, and
365
389
  frequency-domain / seasonal block methods are planned for a future version. The
@@ -7,7 +7,7 @@ where = ["src"]
7
7
 
8
8
  [project]
9
9
  name = "tsbootstrap"
10
- version = "0.3.0"
10
+ version = "0.4.0"
11
11
  description = "A Python package to generate bootstrapped time series"
12
12
  maintainers = [
13
13
  { name = "Sankalp Gilda", email = "sankalp.gilda@gmail.com" },
@@ -114,6 +114,7 @@ dev = [
114
114
  "pytest-xdist[psutil]",
115
115
  "pytest-cov",
116
116
  "pytest-mock",
117
+ "pytest-timeout", # hang guard for the mutation ratchet (kills a non-terminating mutant)
117
118
  "nbmake", # execute the tutorial notebooks as a CI test gate (issue #46)
118
119
  "sktime", # validates the skbase/sktime adapter discovery + check_estimator
119
120
  "polars", # exercises the narwhals DataFrame boundary with a second backend
@@ -382,15 +383,80 @@ reportUnnecessaryIsInstance = false
382
383
  reportConstantRedefinition = false
383
384
 
384
385
  [tool.mutmut]
386
+ # STATUS (2026-06-24): in-process mutmut is NOT a blocking CI gate. Root cause, proven this session
387
+ # and reproduced on a clean 16-core box (not CPU contention): mutmut 3.x runs pytest IN-PROCESS via
388
+ # repeated pytest.main(), and numba's @njit(parallel=True) kernels (engines/var.py + block/_compiled.py)
389
+ # DEADLOCK in LLVM finalize_object/parse_assembly when that compile recurs inside one interpreter, so
390
+ # the stats pass times out and the run aborts before any mutant is tested. NUMBA_DISABLE_JIT=1 avoids
391
+ # the deadlock but runs the kernels interpreted, which then blows the per-test timeout. The locked path
392
+ # (cross-family debate 2026-06-24) is a Layer-2 subprocess-per-mutant runner: keep mutmut ONLY for AST
393
+ # mutant generation, execute each mutant in an isolated subprocess so LLVM never churns in one process,
394
+ # run nightly/non-blocking, gate on the new-survivor diff vs tests/mutation_equivalents.md. The config
395
+ # below + tools/mutmut_sitecustomize remain usable for a one-off LOCAL inspection on an idle box, but
396
+ # the durable value already landed is the killing tests + the equivalents registry, not this in-process
397
+ # runner. See ~/.claude/skills/mutation-ratchet and the debate transcript for the subprocess design.
398
+ #
385
399
  # Mutation-score ratchet over the engine + model core (the established baseline scope).
386
- # Run with: NUMBA_DISABLE_JIT=1 HYPOTHESIS_PROFILE=mutmut mutmut run
387
- # - NUMBA_DISABLE_JIT=1: the @njit VAR kernel is otherwise cache-masked, so source mutations to
388
- # it never execute; interpreted execution lets them take effect (compiled correctness is
389
- # covered separately by test_var_numba_and_numpy_backends_agree in the normal JIT-on suite).
400
+ # Run with: PYTHONPATH=tools/mutmut_sitecustomize HYPOTHESIS_PROFILE=mutmut uv run mutmut run
401
+ # - PYTHONPATH=tools/mutmut_sitecustomize (REQUIRED): mutate_only_covered_lines below makes mutmut
402
+ # run an in-process coverage pass and then UNLOAD every module imported during it
403
+ # (mutmut.code_coverage._unload_modules_not_in) so the source reloads fresh per mutant. That
404
+ # unload is fatal to C-extension libraries that cannot re-import in the same process: numpy
405
+ # ("cannot load module more than once per process" from _multiarray_umath) and numba
406
+ # ("duplicate registration for PolynomialType"). The sitecustomize on this path patches the
407
+ # unload to keep numpy/numba/scipy/statsmodels/... resident while still reloading tsbootstrap.
408
+ # It is gated on HYPOTHESIS_PROFILE=mutmut, so it is inert outside the ratchet. This is an
409
+ # upstream mutmut-3.x interaction (2.x did not copy + reload this way), not specific to this repo.
390
410
  # - HYPOTHESIS_PROFILE=mutmut: derandomized profile so the property baseline is reproducible.
411
+ # It also activates the mutmut-only test fixture in tests/conftest.py:
412
+ # * an ephemeral per-session NUMBA_CACHE_DIR so the @njit VAR kernel recompiles its mutated
413
+ # source fresh on every mutant (JIT stays ON, at native speed). This replaces the old
414
+ # NUMBA_DISABLE_JIT=1 workaround, whose interpreted execution cost ~28 minutes per run;
415
+ # the cache, not the JIT, was what masked source mutations to the compiled kernel.
416
+ # There is deliberately NO solver-maxiter clamp (see the long note in tests/conftest.py): it
417
+ # changed the numerics on the unmutated code and broke the clean-baseline ARIMA golden, which
418
+ # mutmut requires to pass. Spinning mutants are bounded by --timeout below instead.
419
+ # RUN ON AN IDLE / DEDICATED MACHINE. mutmut 3.x runs its stats and per-mutant suites in-process
420
+ # (pytest.main) with --timeout below; under heavy CPU contention (e.g. other multi-hour jobs) the
421
+ # in-process stats pass can exceed the per-test timeout and abort the whole run before any mutant is
422
+ # tested. The full run is reliable on an idle box or a dedicated remote runner. Accepted equivalent
423
+ # survivors are catalogued in tests/mutation_equivalents.md (the ratchet baseline).
391
424
  # source_paths is the whole importable tree (subdirs alone yield an incomplete copied package);
392
425
  # only_mutate then restricts mutation to the engines+model scope.
393
426
  source_paths = ["src/"]
394
427
  only_mutate = ["src/tsbootstrap/engines/*", "src/tsbootstrap/model/*"]
428
+
429
+ # MUST stay false for the subprocess gate. true makes mutmut run an IN-PROCESS coverage pass (the
430
+ # whole suite under coverage.py) before generating, which hits the same numba parallel-JIT LLVM
431
+ # deadlock as the in-process executor -> empty coverage -> ZERO mutants generated. false skips that
432
+ # pass entirely: generation is pure libcst (no suite run, no deadlock) and mutates every in-scope
433
+ # line. The subprocess gate runs all generated mutants anyway, so covered-lines filtering is not
434
+ # needed; uncovered-line survivors are correct signal (a real "no test exercises this line").
435
+ mutate_only_covered_lines = false
436
+
437
+ # Systemic non-behavioral equivalents, suppressed at the source-pattern level (~164 of these were
438
+ # hand-classified as equivalent in the v0.2.0 mutation work). In mutmut 3.6 do_not_mutate_patterns
439
+ # is a list of PYTHON REGEXES, each re.search()-matched against every raw source LINE; a matching
440
+ # line has its expression nodes excluded from mutation (it is line-granular, NOT AST-node text and
441
+ # NOT a glob). Patterns are therefore anchored to their own kwarg/continuation lines so they do not
442
+ # over-suppress mutation of a real algorithmic expression that happens to share the line.
443
+ # - code=Codes.X / context={...}: error-construction kwargs (enum tag + diagnostic dict). Mutating
444
+ # these never changes behavior, only the exception payload, which no test asserts on by value.
445
+ # - dtype=np.<type>: the dtype-default class (e.g. dtype=np.float64 -> None). Inputs are already
446
+ # float64/int by construction, so dropping the explicit dtype is behavior-preserving.
447
+ # - def __<dunder>__ and logger/logging/log.<level>(...) call args: covered for fleet generality
448
+ # (no occurrences in the current engines+model scope, so inert here, but harmless to keep).
449
+ do_not_mutate_patterns = [
450
+ "^\\s*code=Codes\\.",
451
+ "^\\s*context=",
452
+ "dtype=np\\.",
453
+ "^\\s*def __[a-zA-Z]+__\\s*\\(",
454
+ "(?:logger|logging|log)\\.(?:debug|info|warning|error|critical|exception)\\(",
455
+ ]
456
+
395
457
  pytest_add_cli_args_test_selection = ["-m", "not slow", "tests/"]
396
- pytest_add_cli_args = ["-o", "addopts="]
458
+ # -o addopts= drops the project's default addopts (coverage, xdist, durations). The per-test
459
+ # timeout is mutmut's hang guard: a mutant that sends an iterative solver or a recurrence into a
460
+ # non-terminating loop is killed at 60s (thread method, so it interrupts even C-extension calls)
461
+ # instead of stalling the whole run.
462
+ pytest_add_cli_args = ["-o", "addopts=", "--timeout=60", "--timeout-method=thread"]
@@ -35,12 +35,14 @@ from tsbootstrap.errors import (
35
35
  TSBootstrapError,
36
36
  TSBootstrapWarning,
37
37
  )
38
+ from tsbootstrap.metadata import MethodMetadata, metadata_for
38
39
  from tsbootstrap.methods import (
39
40
  AR,
40
41
  ARIMA,
41
42
  IID,
42
43
  VAR,
43
44
  BaseMethodSpec,
45
+ BlockWild,
44
46
  CircularBlock,
45
47
  MethodSpec,
46
48
  MovingBlock,
@@ -49,6 +51,7 @@ from tsbootstrap.methods import (
49
51
  SieveAR,
50
52
  StationaryBlock,
51
53
  TaperedBlock,
54
+ Wild,
52
55
  )
53
56
  from tsbootstrap.results import (
54
57
  BootstrapResult,
@@ -64,12 +67,21 @@ from tsbootstrap.results import (
64
67
  from tsbootstrap.uq import (
65
68
  EnbPIEnsemble,
66
69
  aci_halfwidths,
70
+ basic_interval,
71
+ bca_interval,
72
+ block_jackknife_se,
73
+ conf_int,
74
+ conf_int_panel,
67
75
  enbpi_intervals,
68
76
  fit_predict_oob,
69
77
  forecast_intervals,
78
+ jackknife_acceleration,
79
+ jackknife_statistics,
70
80
  nexcp_quantile,
81
+ percentile_interval,
71
82
  sliding_window_halfwidths,
72
83
  static_halfwidths,
84
+ studentized_interval,
73
85
  )
74
86
 
75
87
  try:
@@ -84,9 +96,11 @@ __all__ = [
84
96
  "bootstrap_iter",
85
97
  "bootstrap_reduce",
86
98
  "bootstrap_reduce_panel",
87
- # diagnostics
99
+ # diagnostics and method introspection
88
100
  "diagnose",
89
101
  "Diagnosis",
102
+ "metadata_for",
103
+ "MethodMetadata",
90
104
  # method specifications
91
105
  "IID",
92
106
  "MovingBlock",
@@ -94,6 +108,8 @@ __all__ = [
94
108
  "StationaryBlock",
95
109
  "NonOverlappingBlock",
96
110
  "TaperedBlock",
111
+ "Wild",
112
+ "BlockWild",
97
113
  "AR",
98
114
  "ARIMA",
99
115
  "VAR",
@@ -115,6 +131,15 @@ __all__ = [
115
131
  "nexcp_quantile",
116
132
  "static_halfwidths",
117
133
  "sliding_window_halfwidths",
134
+ "percentile_interval",
135
+ "basic_interval",
136
+ "jackknife_statistics",
137
+ "block_jackknife_se",
138
+ "studentized_interval",
139
+ "jackknife_acceleration",
140
+ "bca_interval",
141
+ "conf_int",
142
+ "conf_int_panel",
118
143
  # errors and warnings
119
144
  "Codes",
120
145
  "TSBootstrapError",
@@ -22,7 +22,9 @@ from tsbootstrap.methods import (
22
22
  IID,
23
23
  VAR,
24
24
  BlockLength,
25
+ BlockWild,
25
26
  CircularBlock,
27
+ Innovation,
26
28
  MethodSpec,
27
29
  MovingBlock,
28
30
  NonOverlappingBlock,
@@ -30,6 +32,7 @@ from tsbootstrap.methods import (
30
32
  SieveAR,
31
33
  StationaryBlock,
32
34
  TaperedBlock,
35
+ Wild,
33
36
  )
34
37
 
35
38
  _Sample = NDArray[np.floating]
@@ -251,17 +254,30 @@ class ARResidualBootstrap(BaseTimeSeriesBootstrap):
251
254
  }
252
255
 
253
256
  def __init__(
254
- self, order: int = 1, n_bootstraps: int = 999, random_state: int | None = None
257
+ self,
258
+ order: int = 1,
259
+ innovation: Innovation | None = None,
260
+ n_bootstraps: int = 999,
261
+ random_state: int | None = None,
255
262
  ) -> None:
256
263
  self.order = order
264
+ self.innovation = innovation
257
265
  super().__init__(n_bootstraps=n_bootstraps, random_state=random_state)
258
266
 
259
267
  def _make_spec(self) -> MethodSpec:
260
- return ResidualBootstrap(model=AR(order=self.order))
268
+ return ResidualBootstrap(
269
+ model=AR(order=self.order),
270
+ innovation=self.innovation if self.innovation is not None else IID(),
271
+ )
261
272
 
262
273
  @classmethod
263
274
  def get_test_params(cls) -> list[dict[str, Any]]:
264
- return [{"order": 1, "n_bootstraps": 10}, {"order": 2, "n_bootstraps": 5}]
275
+ return [
276
+ {"order": 1, "n_bootstraps": 10},
277
+ {"order": 2, "n_bootstraps": 5},
278
+ {"order": 1, "innovation": Wild(), "n_bootstraps": 5},
279
+ {"order": 1, "innovation": BlockWild(block_length=5), "n_bootstraps": 5},
280
+ ]
265
281
 
266
282
 
267
283
  class ARIMAResidualBootstrap(BaseTimeSeriesBootstrap):
@@ -276,14 +292,19 @@ class ARIMAResidualBootstrap(BaseTimeSeriesBootstrap):
276
292
  def __init__(
277
293
  self,
278
294
  order: tuple[int, int, int] = (1, 1, 1),
295
+ innovation: Innovation | None = None,
279
296
  n_bootstraps: int = 999,
280
297
  random_state: int | None = None,
281
298
  ) -> None:
282
299
  self.order = order
300
+ self.innovation = innovation
283
301
  super().__init__(n_bootstraps=n_bootstraps, random_state=random_state)
284
302
 
285
303
  def _make_spec(self) -> MethodSpec:
286
- return ResidualBootstrap(model=ARIMA(order=self.order))
304
+ return ResidualBootstrap(
305
+ model=ARIMA(order=self.order),
306
+ innovation=self.innovation if self.innovation is not None else IID(),
307
+ )
287
308
 
288
309
  @classmethod
289
310
  def get_test_params(cls) -> list[dict[str, Any]]:
@@ -300,13 +321,21 @@ class VARResidualBootstrap(BaseTimeSeriesBootstrap):
300
321
  }
301
322
 
302
323
  def __init__(
303
- self, order: int = 1, n_bootstraps: int = 999, random_state: int | None = None
324
+ self,
325
+ order: int = 1,
326
+ innovation: Innovation | None = None,
327
+ n_bootstraps: int = 999,
328
+ random_state: int | None = None,
304
329
  ) -> None:
305
330
  self.order = order
331
+ self.innovation = innovation
306
332
  super().__init__(n_bootstraps=n_bootstraps, random_state=random_state)
307
333
 
308
334
  def _make_spec(self) -> MethodSpec:
309
- return ResidualBootstrap(model=VAR(order=self.order))
335
+ return ResidualBootstrap(
336
+ model=VAR(order=self.order),
337
+ innovation=self.innovation if self.innovation is not None else IID(),
338
+ )
310
339
 
311
340
  @classmethod
312
341
  def get_test_params(cls) -> list[dict[str, Any]]:
@@ -327,16 +356,23 @@ class SieveBootstrap(BaseTimeSeriesBootstrap):
327
356
  min_lag: int = 1,
328
357
  max_lag: int | None = None,
329
358
  criterion: Literal["aic", "bic", "hqic"] = "bic",
359
+ innovation: Innovation | None = None,
330
360
  n_bootstraps: int = 999,
331
361
  random_state: int | None = None,
332
362
  ) -> None:
333
363
  self.min_lag = min_lag
334
364
  self.max_lag = max_lag
335
365
  self.criterion: Literal["aic", "bic", "hqic"] = criterion
366
+ self.innovation = innovation
336
367
  super().__init__(n_bootstraps=n_bootstraps, random_state=random_state)
337
368
 
338
369
  def _make_spec(self) -> MethodSpec:
339
- return SieveAR(min_lag=self.min_lag, max_lag=self.max_lag, criterion=self.criterion)
370
+ return SieveAR(
371
+ min_lag=self.min_lag,
372
+ max_lag=self.max_lag,
373
+ criterion=self.criterion,
374
+ innovation=self.innovation if self.innovation is not None else IID(),
375
+ )
340
376
 
341
377
  @classmethod
342
378
  def get_test_params(cls) -> list[dict[str, Any]]:
@@ -2114,6 +2114,12 @@ def compiled_supports(method: object) -> bool:
2114
2114
  if isinstance(method, ResidualBootstrap):
2115
2115
  # The recursive residual AR and VAR fast paths are built; ARIMA models stay
2116
2116
  # on the numpy backend (the unified entry raises the typed error for them).
2117
+ # The residual kernels resample IID innovations inside the compiled loop, so
2118
+ # any other innovation (the wild multiplier family, block innovations) must be
2119
+ # rejected here: running the IID kernel for them would silently return a
2120
+ # different bootstrap distribution than the one the spec asks for.
2121
+ if not isinstance(method.innovation, IID):
2122
+ return False
2117
2123
  return isinstance(method.model, (AR, VAR))
2118
2124
  return isinstance(
2119
2125
  method, (IID, MovingBlock, CircularBlock, StationaryBlock, NonOverlappingBlock)
@@ -2122,6 +2128,24 @@ def compiled_supports(method: object) -> bool:
2122
2128
 
2123
2129
  def unsupported_method_error(method: object) -> MethodConfigError:
2124
2130
  """The typed error raised when the compiled backend has no kernel for a method."""
2131
+ from tsbootstrap.methods import AR, IID, VAR, ResidualBootstrap
2132
+
2133
+ # A residual bootstrap whose MODEL is supported but whose INNOVATION is not:
2134
+ # name the innovation, not the method, so the message does not falsely imply
2135
+ # that ResidualBootstrap itself lacks a compiled kernel.
2136
+ if (
2137
+ isinstance(method, ResidualBootstrap)
2138
+ and isinstance(method.model, (AR, VAR))
2139
+ and not isinstance(method.innovation, IID)
2140
+ ):
2141
+ innovation_name = type(method.innovation).__name__
2142
+ return MethodConfigError(
2143
+ f"backend='compiled' does not support {innovation_name} innovations; "
2144
+ "the compiled residual kernels resample IID innovations only",
2145
+ code=Codes.INVALID_PARAMETER,
2146
+ context={"method": type(method).__name__, "innovation": innovation_name},
2147
+ hint="Use the default backend='numpy' for wild-type or block innovations.",
2148
+ )
2125
2149
  return MethodConfigError(
2126
2150
  f"backend='compiled' does not support {type(method).__name__}; "
2127
2151
  f"supported methods: {list(_SUPPORTED_METHOD_NAMES)}.",
@@ -40,6 +40,7 @@ from tsbootstrap.methods import (
40
40
  StationaryBlock,
41
41
  TaperedBlock,
42
42
  )
43
+ from tsbootstrap.uq.classical import percentile_interval
43
44
 
44
45
  if TYPE_CHECKING: # pragma: no cover - import only for type checkers
45
46
  from mcp.server.fastmcp import FastMCP
@@ -373,15 +374,13 @@ def bootstrap_confidence_interval(
373
374
  return _error(f"bootstrap run failed: {result.failure_reason}")
374
375
 
375
376
  replicate_stats = np.asarray(result.statistics, dtype=np.float64).reshape(-1)
376
- alpha = 1.0 - confidence_level
377
- lower_q = 100.0 * (alpha / 2.0)
378
- upper_q = 100.0 * (1.0 - alpha / 2.0)
377
+ ci_lower, ci_upper = percentile_interval(replicate_stats, alpha=1.0 - confidence_level)
379
378
 
380
379
  return {
381
380
  "point_estimate": float(reducer(arr)),
382
381
  "standard_error": float(np.std(replicate_stats, ddof=0)),
383
- "ci_lower": float(np.percentile(replicate_stats, lower_q)),
384
- "ci_upper": float(np.percentile(replicate_stats, upper_q)),
382
+ "ci_lower": float(ci_lower),
383
+ "ci_upper": float(ci_upper),
385
384
  "confidence_level": float(confidence_level),
386
385
  "n_bootstraps": int(n_bootstraps),
387
386
  "method": method_name,
@@ -118,7 +118,14 @@ METHODS: dict[type, MethodMetadata] = {
118
118
  supports_indices=False,
119
119
  supports_oob=False,
120
120
  preserves_temporal_dependence=True,
121
- references=("Efron-Tibshirani 1993", "Kreiss-Lahiri 2012"),
121
+ references=(
122
+ "Efron-Tibshirani 1993",
123
+ "Kreiss-Lahiri 2012",
124
+ "Wu 1986 (wild innovations)",
125
+ "Mammen 1993 (wild innovations)",
126
+ "Davidson-Flachaire 2008 (Rademacher default)",
127
+ "Shao 2010 (block-wild innovations)",
128
+ ),
122
129
  complexity="O(B*n*p) AR; O(B*n*p*d^2) VAR",
123
130
  failure_modes=("model misspecification", "near-unit-root instability"),
124
131
  ),
@@ -14,6 +14,8 @@ Composition:
14
14
 
15
15
  - Observation-resampling specs (:class:`IID`, the ``*Block`` family) double as
16
16
  the ``innovation`` resampler for residual/sieve bootstraps.
17
+ - :class:`Wild` and :class:`BlockWild` are innovation-only specs (multiplier
18
+ resamplers); they are valid as ``innovation`` but not as a top-level method.
17
19
  - :class:`ResidualBootstrap` pairs a model (:class:`AR`/:class:`ARIMA`/
18
20
  :class:`VAR`) with an innovation resampler.
19
21
  """
@@ -121,6 +123,56 @@ class TaperedBlock(BaseMethodSpec):
121
123
  _v = field_validator("block_length", mode="before")(_check_block_length)
122
124
 
123
125
 
126
+ # --------------------------------------------------------------------------- #
127
+ # Innovation-only specs (valid ONLY as an `innovation` resampler, never as a
128
+ # top-level method: they transform residuals in place rather than resample
129
+ # observations, so no observation indices exist).
130
+ # --------------------------------------------------------------------------- #
131
+ WildDistribution = Literal["rademacher", "gaussian", "mammen"]
132
+
133
+
134
+ class Wild(BaseMethodSpec):
135
+ """Wild bootstrap innovations (Wu 1986; Liu 1988): ``e*_t = v_t * e_hat_t``.
136
+
137
+ Each centered residual keeps its time position and magnitude; an i.i.d.
138
+ external multiplier ``v_t`` (mean 0, variance 1) randomizes its sign/scale.
139
+ Valid under conditional heteroskedasticity of unknown form, where index
140
+ resampling would average the variance profile away. The default Rademacher
141
+ multiplier follows Davidson and Flachaire (2008); ``"mammen"`` is the
142
+ two-point distribution of Mammen (1993) matching the third moment;
143
+ ``"gaussian"`` is the standard normal multiplier.
144
+
145
+ Requires ``burn_in=0`` and ``initial="fixed"`` on the host model spec: the
146
+ multiplier stream is aligned one-to-one with the residuals, conditional on
147
+ the observed initial values.
148
+ """
149
+
150
+ kind: Literal["wild"] = "wild"
151
+ distribution: WildDistribution = "rademacher"
152
+
153
+
154
+ class BlockWild(BaseMethodSpec):
155
+ """Block-wild bootstrap innovations: multipliers constant on time blocks.
156
+
157
+ One multiplier is drawn per contiguous non-overlapping block of the
158
+ residual sequence and repeated across that block, so within-block residual
159
+ dependence survives the resampling (a piecewise-constant special case of
160
+ the dependent wild bootstrap of Shao 2010; the block-constant construction
161
+ mirrors the wild cluster bootstrap of Cameron, Gelbach, and Miller 2008
162
+ with time blocks as clusters). ``block_length=1`` degenerates to
163
+ :class:`Wild`. ``"auto"`` resolves the block length from the centered
164
+ residuals via the Politis-White rule at fit time.
165
+
166
+ Same host-model constraints as :class:`Wild` (``burn_in=0``,
167
+ ``initial="fixed"``).
168
+ """
169
+
170
+ kind: Literal["block_wild"] = "block_wild"
171
+ distribution: WildDistribution = "rademacher"
172
+ block_length: BlockLength = "auto"
173
+ _v = field_validator("block_length", mode="before")(_check_block_length)
174
+
175
+
124
176
  # --------------------------------------------------------------------------- #
125
177
  # Model specs (the conditional mean for residual bootstraps).
126
178
  # --------------------------------------------------------------------------- #
@@ -160,7 +212,7 @@ class VAR(_RecursiveInitSpec):
160
212
 
161
213
 
162
214
  Innovation = Annotated[
163
- Union[IID, MovingBlock, CircularBlock, StationaryBlock, NonOverlappingBlock],
215
+ Union[IID, MovingBlock, CircularBlock, StationaryBlock, NonOverlappingBlock, Wild, BlockWild],
164
216
  Field(discriminator="kind"),
165
217
  ]
166
218
  ModelSpec = Annotated[Union[AR, ARIMA, VAR], Field(discriminator="kind")]
@@ -223,6 +275,9 @@ __all__ = [
223
275
  "StationaryBlock",
224
276
  "NonOverlappingBlock",
225
277
  "TaperedBlock",
278
+ "Wild",
279
+ "BlockWild",
280
+ "WildDistribution",
226
281
  "AR",
227
282
  "ARIMA",
228
283
  "VAR",