Skip to content

[ENH] Speed up interval-based forest regressors#3628

Draft
alexbanwell1 wants to merge 15 commits into
mainfrom
speedup-interval-forest-regressors
Draft

[ENH] Speed up interval-based forest regressors#3628
alexbanwell1 wants to merge 15 commits into
mainfrom
speedup-interval-forest-regressors

Conversation

@alexbanwell1

@alexbanwell1 alexbanwell1 commented Jul 9, 2026

Copy link
Copy Markdown
Contributor

What does this implement/fix? Explain your changes.

Speeds up the interval-based forest regressors (DrCIFRegressor, CanonicalIntervalForestRegressor, TimeSeriesForestRegressor, RandomIntervalSpectralEnsembleRegressor, IntervalForestRegressor), which currently become infeasibly slow on larger datasets (e.g. windowed forecasting problems with 1000-10000 training cases).

1. Change the default base estimator for regression from DecisionTreeRegressor(criterion="absolute_error") to DecisionTreeRegressor(criterion="squared_error") in BaseIntervalForest.

sklearn's absolute_error criterion scales roughly quadratically with the number of cases (a long-standing sklearn limitation, see scikit-learn/scikit-learn#9626), while squared_error is O(n log n). Fitting a single tree on DrCIF-transformed features (~210 features):

n_cases absolute_error squared_error
500 0.39s 0.07s
1000 1.23s 0.17s
2500 6.18s 0.47s
5000 22.5s 1.03s
10000 84.1s 2.26s

With the default 200 estimators, tree fitting alone at 10000 cases drops from ~4.7 hours to ~8 minutes per fit. Test regression quality is essentially unchanged in a quick check (DrCIF, 50 trees): CardanoSentiment test MAE 0.224 (absolute_error) vs 0.226 (squared_error); on a synthetic windowed forecasting problem squared_error was slightly more accurate (MAE 1.62 vs 1.82).

This is a behavioural change for anyone relying on the old default: results produced with defaults will differ slightly from previous versions (e.g. the TSER bake-off reference results). The old behaviour is available by passing base_estimator=DecisionTreeRegressor(criterion="absolute_error"), and this is noted in the docstrings. Classification defaults are untouched.

2. Transform whole collections in a single numba call in Catch22.

Catch22 is called once per interval per tree inside DrCIF/CIF (classification and regression) with n_jobs=1. Previously each case was transformed by a Python-level _transform_case call (via a joblib delayed wrapper), paying Python branching and numba dispatch overhead tens of millions of times in a large forest fit. For single-threaded equal-length 3D input, the per-case loop now runs inside a single numba function per transform call (_transform_collection_3d), which mirrors _transform_case exactly. The FFT used by the two SP_Summaries features is precomputed for all cases with a vectorised np.fft.fft call (verified bit-identical to the per-case calls), as np.fft is unsupported in numba. The per-case path is kept for unequal length input, n_jobs > 1 and pycatch22.

One caveat: numba compiles the feature functions a second time when they are called from the batch function, and with fastmath=True this can differ in the last float digits from the Python-called versions (max observed relative difference 3.5e-13 over a large grid of inputs, feature subsets, catch24 and outlier_norm settings, NaN patterns identical). Care was taken to avoid this where it matters: the FFT input mean is computed exactly as the per-case path does, since the FFT DC term is a catastrophic cancellation that would otherwise meaningfully change the SP_Summaries features for short series. In testing, DrCIFRegressor predictions and the stored expected results outputs for Catch22Classifier, Catch22Regressor, CanonicalIntervalForest (both), DrCIFClassifier and RISTClassifier were byte-identical with and without the batch path. The multithreading and datatype consistency estimator checks (which use np.allclose) all pass.

End-to-end DrCIFRegressor.fit (default params, univariate, series length 100, all runs in a single session):

n_cases n_estimators main this PR speedup
1000 20 102.9s 42.6s 2.4x
2500 20 308.0s 108.7s 2.8x
10000 4 436.6s 71.3s 6.1x

The advantage grows with n_cases as the quadratic tree fitting cost takes over on main. At 10000 cases with the default 200 estimators this works out to roughly 6 hours on main vs 1 hour with this PR per fit.

Expected test results for the four affected regressors with stored results were regenerated using the _write_estimator_results.py procedure. Doctest outputs are unchanged (the examples predict on their training data, where fully-grown trees reproduce the targets exactly regardless of criterion). All check_estimator checks pass for the five affected regressors plus Catch22, Catch22Classifier and DrCIFClassifier, and the interval-forest/regression/catch22 test suites pass.

Does your contribution introduce a new dependency? If yes, which one?

No.

Any other comments?

The criterion change alters default behaviour, so flagging for maintainer opinion: absolute_error has been the regression default since the base class was introduced, but it makes the regressors unusable beyond a few thousand cases (a week-long fit on a windowed forecasting problem motivated this PR). sklearn's own RandomForestRegressor defaults to squared_error.

The two changes are independent — happy to split the PR if preferred.

PR checklist

For all contributions
  • I've added myself to the list of contributors. Alternatively, you can use the @all-contributors bot to do this for you after the PR has been merged.
  • The PR title starts with either [ENH], [MNT], [DOC], [BUG], [REF], [DEP] or [GOV] indicating whether the PR topic is related to enhancement, maintenance, documentation, bugs, refactoring, deprecation or governance.

🤖 Generated with Claude Code

TonyBagnall and others added 11 commits July 8, 2026 11:03
_SB_MotifThree_quantile_hh (entropy_pairs): replace the r1/r2 per-symbol
position-list machinery and terminal-position trim with a direct 3x3
adjacent-transition count. The trim is just excluding the last position as
a transition source, which range(n-1) handles, so this is a straight
simplification that drops ~7 temporary length-n arrays. ~1.5x faster in
isolation.

_sb_coarsegrain: collapse the num_groups labelling passes into a single
pass with an early break, since the group ranges are disjoint.

Output is bit-for-bit identical: max abs diff 0.0 vs a reference set and a
5760-case fuzz of _SB_MotifThree_quantile_hh against the original r1/r2
implementation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_fluct_prop: bespoke least squares over the fixed grid d=[1..tau] - the
grid moments are closed-form and computed once per tau, and the DFA
fluctuation is a residual-sum-of-squares from sums instead of detrending
each window. ~1.6x on rs_range/dfa.

_compute_autocorrelations: factor out the FFT twiddle table (depends only
on the series length) and precompute it once per transform via
_ac_twiddle_cache, reused for the shared autocorrelation features
(2, 3, 8, 10, 12) instead of rebuilding it per call. ~1.15x on the acf
cluster.

Output is bit-for-bit identical to the previous implementation (verified
against the original over fixed-seed panels incl. multivariate and edge
cases, plus 4200 direct _fluct_prop fuzz cases); catch22 tests pass.
Together ~1.2x on the full 22-feature transform.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
_FC_LocalSimple_mean1_tauresrat computes the autocorrelation of the
differenced series; pass it the transform's precomputed twiddle table
(added for the other autocorrelation features) so it reuses them instead
of rebuilding, falling back to computing its own when the differenced
series lands on a different FFT size (length 2**k + 1). Bit-for-bit
identical; ~1.1x on whiten_timescale.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Change the default base estimator criterion for interval-based forest
regressors (DrCIF, CIF, TSF, RISE, IntervalForest) from absolute_error
to squared_error. The sklearn absolute_error criterion scales
quadratically with the number of cases, making fitting infeasible for
larger datasets (84s vs 2.3s per tree at 10000 cases). Also avoid
joblib overhead in Catch22 transform when n_jobs=1.

Expected test results regenerated for the new default. Doctest outputs
are unchanged.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@aeon-actions-bot aeon-actions-bot Bot added enhancement New feature, improvement request or other non-bug code enhancement regression Regression package testing Testing related issue or pull request transformations Transformations package labels Jul 9, 2026
@aeon-actions-bot

Copy link
Copy Markdown
Contributor

Thank you for contributing to aeon

I have added the following labels to this PR based on the title: [ enhancement ].
I have added the following labels to this PR based on the changes made: [ regression, testing, transformations ]. Feel free to change these if they do not properly represent the PR.

The Checks tab will show the status of our automated tests. You can click on individual test runs in the tab or "Details" in the panel below to see more information if there is a failure.

If our pre-commit code quality check fails, please run pre-commit locally and push the fixes to your PR branch.

Don't hesitate to ask questions on the aeon Discord channel if you have any.

PR CI actions

These checkboxes will add labels to enable or disable CI functionality for this PR. This may not take effect immediately, and a new commit may be required to run the new configuration.

  • Run pre-commit checks for all files
  • Run mypy typecheck tests
  • Run all pytest tests and configurations
  • Run all notebook example tests
  • Run numba-disabled codecov tests
  • Disable numba cache loading
  • Regenerate expected results for testing
  • Push an empty commit to re-run CI checks

@alexbanwell1 alexbanwell1 marked this pull request as ready for review July 9, 2026 22:43
Add a numba batch function mirroring _transform_case that transforms all
cases of an equal length collection in one call, removing Python-level
per-case overhead. Used for single-threaded 3D numpy input, the hot path
for interval-based forests. The FFT for the SP_Summaries features is
precomputed for all cases with a vectorised np.fft call, with the series
means calculated to exactly match the per-case path as the FFT DC term
is sensitive to catastrophic cancellation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@TonyBagnall

Copy link
Copy Markdown
Contributor

hi alex, this will conflict with this #3620 and #3619 , maybe wait until after that is in?

…-forest-regressors

# Conflicts:
#	aeon/transformations/collection/feature_based/_catch22.py
@alexbanwell1 alexbanwell1 marked this pull request as draft July 10, 2026 13:08
@TonyBagnall

Copy link
Copy Markdown
Contributor

those speed ups for decision trees regression are insane. I suggest rebase off #3620, make the change to squared and I will run the timing and accuracy comparison as main -> 3620 (x2.5 faster) -> squared error regression/ gini for classification

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature, improvement request or other non-bug code enhancement regression Regression package testing Testing related issue or pull request transformations Transformations package

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants