Skip to content

perf: compute accuracy and reconstructed on first access#113

Draft
FBumann wants to merge 1 commit into
mainfrom
feat/lazy-result
Draft

perf: compute accuracy and reconstructed on first access#113
FBumann wants to merge 1 commit into
mainfrom
feat/lazy-result

Conversation

@FBumann

@FBumann FBumann commented Jul 23, 2026

Copy link
Copy Markdown
Owner

First of the three perf drafts (this one is the wrapper-side lever unlocked by tsam v4's lazy metrics).

What

AggregationResult.accuracy and .reconstructed are now populated on first access via cached_property over a stored per-field factory, instead of being materialized during result conversion. All four construction paths (single, slice-concat, slice view, clustering.apply) defer consistently — for the sliced case even the per-slice materialization and concat are deferred. repr() no longer forces the computation (shows accuracy=<not computed> until first read).

Why / measured impact

tsam v4 computes metrics lazily, but the wrapper immediately materialized them — paying v4's deferred cost on every aggregate() call even when nobody reads the metrics. Measured at 730 days × 256 columns, Distribution(scope="global") + extremes replace, metrics unread:

main this PR
tsam develop-v4 2.59 s 1.83 s (−29%)
tsam 3.4.2 2.52 s 2.52 s (defers only the xarray conversion — v3 computes metrics inside tsam.aggregate() regardless)

Reading result.accuracy/.reconstructed (or residuals/compare()/to_dataframe(), which touch them) computes once and caches — behavior and values are unchanged, verified by the full suite plus new laziness tests (test/test_lazy_result.py).

Notes

  • The two fields moved from eager dataclass fields to kw-only private factories + cached_property; AggregationResult construction is internal-only, so no public API changes.
  • CI benchmark note: the head-vs-base comment runs on tsam v3.4.x, where the win is only the deferred conversion (visible in test_wrapper_result_conversion, not e2e).

🤖 Generated with Claude Code

tsam v4 defers metric computation until result.accuracy is read, but
the wrapper materialized both fields eagerly during result conversion,
paying the cost even for callers that never look at them. Store a
factory per field and let cached_property populate it on first access;
repr reports the metrics only once computed instead of forcing them.
On tsam v3 the metrics are computed inside tsam.aggregate() either way,
so this defers only the xarray conversion there.

730d x 256 cols, Distribution(scope=global) + extremes, metrics unread:
tsam develop-v4 2.59s -> 1.83s (-29%); tsam 3.4.2 unchanged.

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

coderabbitai Bot commented Jul 23, 2026

Copy link
Copy Markdown

Important

Review skipped

Draft detected.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 18aae05f-33eb-4e9d-8939-f7a4e58b7b7f

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/lazy-result

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@github-actions

Copy link
Copy Markdown

Benchmarks

Δ% vs base, one table sorted by biggest change first
(min walltime and memray peak). Peak memory is
deterministic — any delta there is real; walltime on GitHub
runners is noisy, so only large time deltas are meaningful.
The interactive plot is attached as the benchmark-plot artifact.

Full table

benchmarks/test_bench_aggregate.py

name metric base head
test_user_disaggregate time 0.01089s +2.0%
peak 26.7 MiB 0.0%
test_wrapper_to_dataframe time 0.0003656s -1.1%
peak 8.55 MiB 0.0%
test_e2e_wide[dist_global] time 1.782s -2.1%
peak 104 MiB -41.4%
test_e2e_default time 0.06297s -2.9%
peak 7.11 MiB -37.9%
test_e2e_wide[medoid] time 0.4844s -7.2%
peak 113 MiB -38.4%
test_e2e_full time 0.1436s -8.1%
peak 6.6 MiB -40.9%
test_config_representation[dist_global_minmax] time 0.1223s -8.3%
peak 1.78 MiB -42.8%
test_config_representation[dist_global] time 0.1202s -8.5%
peak 1.78 MiB -42.8%
test_e2e_slices time 1.168s -8.9%
peak 14.3 MiB +91.0%
test_user_apply time 0.3882s -11.1%
peak 104 MiB -41.4%
test_e2e_multidim time 0.1989s -14.4%
peak 26.7 MiB -34.6%
test_config_extremes[append] time 0.04859s -22.3%
peak 1.9 MiB -40.1%
test_config_extremes[replace] time 0.04891s -22.7%
peak 1.9 MiB -40.2%
test_config_representation[dist_cluster] time 0.04627s -23.1%
peak 1.78 MiB -42.8%
test_config_representation[medoid] time 0.04707s -23.1%
peak 1.9 MiB -40.3%
test_config_representation[mean] time 0.04498s -24.3%
peak 1.78 MiB -42.8%
test_wrapper_concat_results time 0.01898s -52.7%
peak 8.72 MiB -49.1%
test_wrapper_result_conversion time 0.03495s -93.6%
peak 60.3 MiB -97.1%

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant