Skip to main content

Heartwood

Gradient-boosted trees that read the rings.

Heartwood is the dense inner wood of a tree, and it carries that tree's whole history as growth rings — a time series recorded inside a tree. That is what this library does: gradient-boosted trees whose splits can read the trajectory a row carries, not just the columns beside it.

from heartwood import HeartwoodClassifier

model = HeartwoodClassifier(n_estimators=200, dense_base=True, random_state=0)
model.fit(X_static, X_series, y)          # (n, p) attributes + (n, channels, T) series
model.predict_proba(X_static_test, X_series_test)

What it is good at, and what it is not

Both halves are measured, on this code, in RESULTS_V24.md.

On 12-lead and single-lead ECG it beats MiniROCKET. On CPSC-2018 restricted to lead II — one channel, n=1000, 5 seeds — balanced accuracy 0.588 against MiniROCKET-10k's 0.555, a margin of +3.3 points, positive on every seed (+4.0, +2.5, +3.5, +3.2, +3.4).

On general time-series benchmarks it does not. Across sixteen UEA datasets it is ahead on 5 of 16. Mean balanced accuracy is 0.798 against MiniROCKET-10k's 0.787 — but that mean is carried by one 22-point win on EthanolConcentration, and the count is the more honest summary. If your problem looks like a UEA classification task, use MiniROCKET: it is simpler, roughly two orders of magnitude faster, and ahead of this library more often than not.

Heartwood MiniROCKET-10k
CPSC-2018, single lead 0.588 0.555
16 UEA datasets (mean) 0.798 0.787
16 UEA datasets (won) 5 11

That is the whole claim. There is no larger one.

Why it exists

Real datasets routinely pair per-row attributes with a raw trajectory per row: a patient's ECG and their BMI, a machine's vibration trace and its model number. The standard workaround summarises the series into aggregates and hands the result to XGBoost, which discards exactly what often matters — when something happened, what shape it had, how the journey unfolded.

Heartwood keeps XGBoost's second-order boosting machinery and enlarges what a split may ask about, so a node can ask "was the slope between t=12 and t=40 negative?" or "does this shape occur, and how early?" as readily as "is the customer over 50?" — all scored on the same gain, all learned from the gradients rather than fixed up front.

It also stays readable. model.dump_splits() returns rules a human can check:

slope of channel 0 between t=12 and t=40 <= 0.31

A convolution bank cannot tell you that.

Install

pip install -e .            # numpy only
pip install -e ".[test]"    # + pytest

Python 3.10+. Pure NumPy, single-threaded, no GPU.

Documentation

How it works the split kinds, the convolution bank underneath the trees, and why the base is a ridge
When to use it including when to reach for MiniROCKET instead
How this was validated the pre-registration rule, and the ten studies that failed their bars

The unusual part: how this was validated

Every performance claim here comes from a study written down and committed before it ran — arms fixed, a numeric bar per hypothesis, what each outcome would mean, and a section naming the outcome the author would least like. A script then applies the bars mechanically. No verdict in this repository was reached by reading a table.

Ten studies. Every bar failed. Five features were deleted as a result:

deleted why
virtual channels −0.2 points over eight datasets
window-statistic bank +0.4 points; 0.015% of RMSE on synthetics
comparison splits 0 of 8 on two independent suites
Lévy areas 0 of 8 on two independent suites
the no-regret fallback picked the better component 48% of the time — a coin flip

Three published claims were withdrawn, including one where the counterexample had been sitting two paragraphs below it in this README for weeks. The full record — corrections included, especially the corrections — is in RESEARCH_LOG.md and the VALIDATION_V*.md / RESULTS_V*.md files.

What the library is, after all that: MiniROCKET's kernel bank, with a ridge over it, under gradient-boosted trees that can also split on static attributes. Every embellishment this project added to that was measured and most were removed.

Screen your dataset before you commit to it

python validation/screen_dataset.py --datasets apnea sleepedf --seeds 3
apnea      REJECT  static_lift=+0.149 (ok)   exogeneity=+0.252 (FAIL)  regime_gap=-0.010 (FAIL)
sleepedf   REJECT  static_lift=-0.001 (FAIL) exogeneity=+0.000 (ok)    regime_gap=-0.008 (FAIL)

Three numbers in minutes: are the static covariates informative on their own, are they exogenous (can the series predict them — age and sex are recoverable from an ECG, which disqualifies them), and is there temporal structure a fixed summary would lose. It fits no Heartwood model, so it cannot be tuned to the answer it exists to give.

It rejects every dataset this project ever studied. That is the point.

Known limitations

  • Behind MiniROCKET on most UEA benchmarks — 5 of 16. Stated above; repeated here so it cannot be missed.
  • Slow. 20–100× slower to fit than XGBoost on pre-aggregated features. A CPSC-2018 cell at n=1000 takes ~22 minutes.
  • Small data. Everything here is designed and measured between n=100 and n=2000.
  • The founding claim is still unproven. That a model seeing raw series and static covariates beats one seeing either alone remains untested after five attempts — not disproven, untested, for want of a dataset where both halves are strong and the statics are genuinely exogenous. validation/screen_dataset.py exists to make attempt six cheaper.
  • Most of the 42 constructor parameters should be left alone. About a quarter are switched off because a pre-registered study failed them; the package docstring lists which, with the study and the margin it failed by.
  • Single-threaded, pure NumPy, no GPU. Series are padded to a common length.

Tests

python -m pytest tests -q     # 374 tests, ~75 s

The suite leans on properties rather than snapshots: exact leave-one-group-out is checked against literally refitting without each group to ~5e-15, and that check has caught two defects that had already produced confident wrong answers.

Licence

MIT.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

heartwood-1.0.0.tar.gz (105.8 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

heartwood-1.0.0-py3-none-any.whl (61.7 kB view details)

Uploaded Python 3

File details

Details for the file heartwood-1.0.0.tar.gz.

File metadata

  • Download URL: heartwood-1.0.0.tar.gz
  • Upload date:
  • Size: 105.8 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.15

File hashes

Hashes for heartwood-1.0.0.tar.gz
Algorithm Hash digest
SHA256 173a568eaf6bec26aeb109541f110aa45351ddb1e85dbcf3325d0fa18bfcdaba
MD5 7eedb787d21e8dc19696e9137895459a
BLAKE2b-256 cebc1b31fbe7438719009024d06227c01a028316b5acd88cdc54b346841edb53

See more details on using hashes here.

File details

Details for the file heartwood-1.0.0-py3-none-any.whl.

File metadata

  • Download URL: heartwood-1.0.0-py3-none-any.whl
  • Upload date:
  • Size: 61.7 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: twine/7.0.0 CPython/3.13.15

File hashes

Hashes for heartwood-1.0.0-py3-none-any.whl
Algorithm Hash digest
SHA256 048c529149c0ddd2c163a104b37c75e6b1fb5cc4b28a33c7ee791909b1322694
MD5 d0ba3a282d866cc26a9809e5ebf9af4a
BLAKE2b-256 03912ad298749f04d7607a649184c9ab7818e4a8f328d2644a72e022480dc3e1

See more details on using hashes here.

Release history Release notifications | RSS feed

This release

1.0.0 This release

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page