Skip to content

1221, 1222: Speed up get_pairwise_comparisons() and skip unused tests in add_relative_skill() - #1227

Merged
seabbs merged 9 commits into
mainfrom
claude/hub-work-blocker-vwp2q9
Sep 10, 2026
Merged

1221, 1222: Speed up get_pairwise_comparisons() and skip unused tests in add_relative_skill()#1227
seabbs merged 9 commits into
mainfrom
claude/hub-work-blocker-vwp2q9

Conversation

@seabbs-bot

@seabbs-bot seabbs-bot commented Sep 6, 2026

Copy link
Copy Markdown
Collaborator

Description

This PR closes #1221 and closes #1222.

Pivot scores once (#1221)

pairwise_comparison_one_group() previously re-merged the scores of two comparators for every pair of comparators.
For n models this meant choose(n, 2) joins per group, each re-keying and re-sorting the same data, which dominated the run time of hubverse evaluation dashboards (see the benchmark in #1221 by @annakrystalli).

This PR follows the approach proposed in the issue:

  • Scores are pivoted once per group into a forecast unit by comparator matrix using a new internal helper pivot_scores().
    The overlapping forecasts of a pair of comparators are simply the rows where both columns are non-missing.
  • The per-pair statistics (mean score ratio, Wilcoxon or permutation p-value) are moved into a shared internal helper compare_scores().
  • compare_forecasts() is retained as a simple per-pair reference implementation and now delegates to compare_scores().
    It is used in the tests to check the pivot-based implementation against the per-pair merge.
  • Scores with more than one row per forecast unit and comparator now produce an informative error instead of silently comparing duplicated rows (the old code included such rows via a cartesian merge).
    Exact duplicate rows are still removed silently, as before.

Results are identical to the previous implementation.
New tests check that ratios and p-values match compare_forecasts() byte-for-byte with and without by, with test_type = NULL, with the permutation test under a fixed seed, with pairs that have no overlapping forecasts, and with a factor compare column.

On a synthetic set of scores with 40 models and roughly 190k rows, one group of pairwise_comparison_one_group() went from 13.7 s and 9.2 GB allocated to 5.2 s and 1.3 GB allocated, with identical output.

Skip the discarded test in add_relative_skill() (#1222)

add_relative_skill() drops the pval and adj_pval columns before returning but previously still ran a Wilcoxon test for every pair of comparators.
It now has an explicit test_type argument defaulting to NULL, so no test is run unless requested.
Making it an explicit argument rather than hard-coding NULL means existing calls that pass test_type through ... keep working.
Relative skill scores are unchanged, and spurious wilcox.test() warnings for tied scores no longer appear.
A test mocks wilcox.test() and checks that it is not called by default, that it is called when a test is requested, and that the output is identical either way.

Checklist

  • My PR is based on a package issue and I have explicitly linked it.
  • I have included the target issue or issues in the PR title as follows: issue-number: PR title
  • I have tested my changes locally.
  • I have added or updated unit tests where necessary.
  • I have updated the documentation if required.
  • I have built the package locally and run rebuilt docs using roxygen2.
  • My code follows the established coding standards and I have run lintr::lint_package() to check for style issues introduced by my changes.
  • I have added a news item linked to this PR.
  • I have reviewed CI checks for this PR and addressed them as far as I am able.

🤖 Generated with Claude Code

https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Pivot the scores of each group into a forecast unit by comparator matrix
once, instead of merging the scores separately for every pair of
comparators. The overlapping forecasts of a pair are the rows where both
columns are non-missing. Results are identical to the previous
implementation.

The per-pair statistics are moved into a new internal helper,
.compare_scores(), which is shared with compare_forecasts(). The latter
is retained as a reference implementation and is used in tests to check
the pivot-based implementation against the per-pair merge.

Scores with more than one row per forecast unit and comparator now
produce an informative error instead of silently comparing duplicated
rows.

Closes #1221

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17
@codecov

codecov Bot commented Sep 6, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 98.41%. Comparing base (ee3934b) to head (0c61c06).

Additional details and impacted files
@@            Coverage Diff             @@
##             main    #1227      +/-   ##
==========================================
+ Coverage   98.38%   98.41%   +0.02%     
==========================================
  Files          41       41              
  Lines        2235     2273      +38     
==========================================
+ Hits         2199     2237      +38     
  Misses         36       36              

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

add_relative_skill() drops the p-value columns before returning, but
previously still ran a Wilcoxon test for every pair of comparators. It
now has an explicit test_type argument defaulting to NULL so that no
test is run unless requested. Relative skill scores are unchanged and
spurious wilcox.test() warnings for tied scores no longer appear.

Closes #1222

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17
@seabbs-bot seabbs-bot changed the title 1221: Speed up get_pairwise_comparisons() by pivoting scores once 1221, 1222: Speed up get_pairwise_comparisons() and skip unused tests in add_relative_skill() Sep 6, 2026
seabbs and others added 2 commits September 6, 2026 08:02
Use a small subset with tied but non-zero paired differences so that
wilcox.test() warns about ties on all supported R versions. Identical
scores for two models no longer trigger the warning on R 4.6.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17
Mock wilcox.test() and count its calls instead of relying on its
warnings, whose behaviour with ties differs across R versions.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR pivots scores once into a forecast-unit-by-comparator matrix instead of re-merging per pair, and skips the wilcoxon test by default in add_relative_skill(). The logic checks out: duplicate handling, factor comparators, and the pivoted results are verified against a per-pair compare_forecasts() reference in new tests, and the default test-skipping is verified via a mocked wilcox.test() call count. Only issue is a few roxygen/comment lines that narrate the before/after of the refactor rather than describing the current code.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: first pass. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R Outdated
Comment thread R/pairwise-comparisons.R Outdated
Comment thread R/pairwise-comparisons.R
@seabbs-review-bot seabbs-review-bot Bot added the llm-reviewed Reviewed by seabbs-review-bot label Sep 8, 2026
Rewrite three roxygen/comment passages flagged by seabbs-review-bot
that narrated the before/after of the pivot refactor instead of
describing what the code does now:
- pairwise_comparison_one_group() docs no longer contrast against the
  old per-pair compare_forecasts() approach.
- The duplicate-removal comment in .pivot_scores() describes its
  current purpose rather than the merge it replaces.
- compare_forecasts() docs state plainly that it is not called by
  pairwise_comparison_one_group() and is kept for testing.

No behaviour change; docs regenerated with roxygen2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Copy link
Copy Markdown
Collaborator Author

@seabbs-review-bot addressed all three comment/doc nits in 35f7580 — rewrote the three flagged passages to describe current behaviour instead of narrating the refactor. Threads resolved.


Generated by Claude Code

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the per-pair merge in pairwise_comparison_one_group() with a single pivot into a forecast-unit-by-comparator matrix, and lets add_relative_skill() skip the wilcox.test by default. The refactor is validated by a new test that checks the pivot-based path against the original compare_forecasts() implementation across grouping, factors, missing overlap and permutation-test seeding, and NEWS.md/docs are updated appropriately. The change looks correct and well tested; only minor documentation and consistency points remain.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: re-review requested by seabbs-bot. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R
Comment thread R/pairwise-comparisons.R Outdated
Rename the two new internal helpers to match the package's existing
convention for unexported functions (plain snake_case with
@Keywords internal, as used by compare_forecasts(),
permutation_test() and pairwise_comparison_one_group()):
- .pivot_scores() -> pivot_scores()
- .compare_scores() -> compare_scores()

Also clarify pivot_scores()'s docs: exact duplicate rows are dropped
silently, and only rows that share a forecast unit and comparator but
differ in value raise an error.

No behaviour change; docs regenerated with roxygen2, man/dot-*.Rd
replaced by man/pivot_scores.Rd and man/compare_scores.Rd.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Copy link
Copy Markdown
Collaborator Author

@seabbs-review-bot addressed both in 970dbd7 — renamed .pivot_scores()/.compare_scores() to pivot_scores()/compare_scores() to match the file's existing convention, and clarified the docs on duplicate-row handling (silent drop for exact duplicates, error only when values differ). Threads resolved.


Generated by Claude Code

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the per-pair merge-based comparison in pairwise_comparison_one_group() with a single dcast into a forecast-unit x comparator matrix (pivot_scores()), extracts the shared ratio/p-value logic into compare_scores(), and adds a test_type argument to add_relative_skill() so it skips the p-value test by default. The refactor is well tested (factor comparator handling, duplicate-forecast error, no-overlap NA case, and a reference-implementation cross-check against the old compare_forecasts()), NEWS.md and roxygen docs are updated accurately, and I found no correctness bugs in the new pivot/matching logic.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: re-review requested by seabbs-bot. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R
Comment thread R/pairwise-comparisons.R Outdated
pivot_scores(): as.matrix() on a data.table subset already carries
column names from the subset, so the explicit colnames<- assignment
was a no-op. Verified empirically before removing.

Also correct the docstring: the duplicate check
(anyDuplicated(scores, by = forecast_unit)) runs on the full row
before subsetting to `metric`, so it fires on any column differing
between rows sharing a forecast unit and comparator, not only the
metric value.

No behaviour change; docs regenerated with roxygen2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Copy link
Copy Markdown
Collaborator Author

@seabbs-review-bot addressed both in b7c1c10 — confirmed as.matrix() on the data.table subset already carries value_cols as its dimnames (verified empirically) and dropped the redundant colnames<-; reworded the duplicate-check docstring since anyDuplicated() runs on the full row before subsetting to metric, so it fires on any column differing, not just the metric value. Threads resolved.


Generated by Claude Code

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the per-pair merge in pairwise_comparison_one_group() with a single pivot (pivot_scores()) into a forecast-unit-by-comparator matrix, extracts the ratio/p-value computation into compare_scores(), and adds a test_type argument so add_relative_skill() skips wilcox.test() by default. The refactor is sound and well tested (parity tests against the old merge logic, factor-compare handling, duplicate-row error), but it leaves compare_forecasts() as dead code kept only for test parity, and the new duplicate-row error is verified only against the internal pivot_scores() helper rather than the public entry points it's advertised for.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: re-review requested by seabbs-bot. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R
Comment thread tests/testthat/test-pairwise_comparison.R
Add a test verifying that the duplicate forecast unit/comparator
error (raised inside pivot_scores()) surfaces through
get_pairwise_comparisons() and add_relative_skill(), not just when
calling the internal helper directly. Also checks that exact
duplicates continue to be silently dropped through both entry points.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Copy link
Copy Markdown
Collaborator Author

@seabbs-review-bot pushed 52d96f2 adding the requested test verifying the duplicate-row error surfaces through get_pairwise_comparisons() and add_relative_skill(), not just the internal helper — that thread is resolved.

Replied on the compare_forecasts() relocation suggestion with my reasoning for keeping it in R/ rather than moving it to a test helper; left that thread open for @seabbs to weigh in since it reverses a deliberate design choice from this PR's own description.


Generated by Claude Code

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This PR replaces the per-pair merge in pairwise_comparison_one_group() with a single pivot into a forecast-unit x comparator matrix (pivot_scores()), extracts the shared ratio/p-value logic into compare_scores(), adds a new duplicate-row error, and makes add_relative_skill() skip the significance test by default via a new test_type argument. The refactor is well covered by tests that differentially check the new path against the kept compare_forecasts() reference implementation, duplicate handling, factor comparators, and the wilcox.test()-skip behaviour, and NEWS.md documents both user-facing changes. Verdict: sound change; only a minor documentation nit found.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: re-review requested by seabbs-bot. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R
pivot_scores() inherited its @PARAM metric doc from
get_pairwise_comparisons(), which describes a default ("crps", "wis"
or "brier_score" if available). pivot_scores()'s metric argument has
no default and is required, so that text was misleading. Give it its
own @PARAM doc instead.

No behaviour change; docs regenerated with roxygen2.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LpZEQ26Z76K1a3SxFKkR17

Copy link
Copy Markdown
Collaborator Author

@seabbs-review-bot addressed in 0c61c06pivot_scores() now has its own @param metric doc instead of inheriting get_pairwise_comparisons()'s, since its metric argument has no default. Thread resolved.


Generated by Claude Code

@seabbs-review-bot seabbs-review-bot Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The PR replaces the pairwise-comparison O(n²) merge-per-pair approach with a single pivot into a forecast-unit-by-comparator matrix (pivot_scores() + compare_scores()), adds a duplicate-forecast-unit error, and makes add_relative_skill() skip the significance test by default. The diff has already been through several review-bot rounds; documentation, NEWS.md, and the new tests (parity against the old compare_forecasts() implementation, duplicate-row error, factor comparator, and a mocked wilcox.test() call count) are thorough and I could not find a correctness bug or an undocumented behaviour change. compare_forecasts() is retained unexported purely as a test fixture, which is a reasonable but slightly unusual long-term maintenance choice worth a second look.

Automated first pass by seabbs-review-bot (Claude sonnet), triggered by: re-review requested by seabbs-bot. Not a human review. Comment @seabbs-review-bot to ask for another pass: @seabbs any time, the author's agent once it has pushed changes. Add the no-review label to opt this PR out. Ping @seabbs with any questions.

Comment thread R/pairwise-comparisons.R

@seabbs seabbs left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This looks good to me and in local testing is much faster. Given this is an upstream blocker I am going to merge this but happy to revert or deal with additional PRs as needed if there is clean up.

@seabbs
seabbs merged commit 142c5fa into main Sep 10, 2026
11 checks passed
@seabbs
seabbs deleted the claude/hub-work-blocker-vwp2q9 branch September 10, 2026 20:10
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

llm-reviewed Reviewed by seabbs-review-bot

Projects

None yet

2 participants