Repository navigation
ToggleSpecialistMethod: blended reference and block legs, symmetric selection, caller-stated toggle cycle - #145
Merged
Merged
Conversation
Contributor
There was a problem hiding this comment.
Copilot review overview
🟢 Approval recommended
Only a non-blocking documentation nit remains; no approval-blocking issues were identified.
Review effort: Lite
Findings: None
What changed in this PR
Adds pairing-aware selection, per-stage accounting, imbalance warnings, and pairing-gap diagnostics to ToggleSpecialistMethod.
Changes:
- Adds validated
pairing_max_gapfiltering. - Exports segment-level selection accounting.
- Adds pairing-gap plots and Windows-safe path handling.
- Review note: update the module docstring to document the fourth diagnostic plot.
| File | Summary |
|---|---|
tests/benchmarking/campaigns/test_uplift_plots.py |
Makes plot path assertions platform-independent. |
tests/benchmarking/baselines/test_toggle_specialist.py |
Tests pairing, accounting, warnings, and plots. |
benchmarking/harness/method.py |
Adds additive selection_accounting output support. |
benchmarking/baselines/toggle_specialist.py |
Implements pairing, accounting, diagnostics, and output changes. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
aclerc
commented
Sep 24, 2026
- pairing_max_gap: keep a used row only if the other segment has a used row within the gap, inclusive. Must be a positive whole multiple of the timebase, so a sub-timebase value can no longer silently drop every row (v0's floored-seconds semantics). Applied after exclude_row, so the uplift and the bootstrap see the same selection. Parity-tested against v0's any_within_timedelta. - MethodOutput.selection_accounting: rows kept per segment after each stage (segment, filters, exclude_row, pairing), also written to a selection CSV; a warning when a stage keeps the two segments' rows unevenly (segment_imbalance_warning). - Pairing-gap diagnostic plot (2_filter): each used row's gap to the nearest used row of the other segment, before and after pairing, with hours kept. Drawn without a pairing filter too, to judge whether one is needed. - Path-separator-agnostic assertion in test_uplift_plots (Windows fix, as on northing-improvement). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
index.asi8 is in the index's own unit; real SCADA often arrives as datetime64[us], so a 10-minute gap read as 0.6 s and the pairing filter kept every row. Convert to ns first. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aclerc
force-pushed
the
v1-pairing-filter
branch
from
September 25, 2026 12:14
7e928bf to
68d7098
Compare
…listMethod reference_mode selects what the test power is compared against: "sum" (the summed references, as before), "block_mean" (the mean test power of the row's own fixed wall-clock block, so no references are needed at all) or "block_scaled" (the summed references rescaled per block so each block's reference total equals its test total). Every used row of a block gets the same reference factor, so the on/off ratio cancels the block's wind level exactly; a block whose used rows lack either state is dropped and reported as a new "block" selection stage. The block length defaults to pairing_max_gap. The exclude_row column is now honoured on every turbine's rows, so a flagged reference row removes the timestamp the way a reference outage does. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…mates reference_mode="combined" runs the sum and block_mean estimates, each on its own row selection, and blends them by inverse variance, headline and per bin. The bootstrap now exposes each cell's resampled uplifts; two bootstraps with the same campaign span, timebase, block length, resample count and seed draw the same blocks, so combine_estimates can take the components' correlation from their draw-paired resamples and report an honest sigma for the blend. A window with no references at all falls back to block_mean with a warning. The uncertainty diagnostics gain the reported sigma and a component column; the selection CSV lists every component's selection. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…l back on A cell with at most three records a side blends to the fallback alone; when the per-record scatter cannot be measured either, the result is NaN rather than the ~1e-17 std of resamples that all see the same handful of records. That residue read as near-certainty, and in the combined reference mode it took the whole weight. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… draw by draw Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…d is never worse than its better leg The weights were plain inverse variance, with the resample correlation entering only the reported sigma; a noisier leg correlated with the better one could then widen the blend beyond the better leg alone. The weights are now wa ∝ max(sb² - r·sa·sb, 0) and wb ∝ max(sa² - r·sa·sb, 0), which drop a strongly correlated noisy leg to zero. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…ialist uses The test turbine went through NormalOperationFilter while the references got a separate complete-case plus availability check. With the stuck filter off both tests are the same, so nothing changes today; one code path means any future filter change reaches the references. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…erence block The pairing filter removes stretches where one toggle state is absent, never the counterpart of a single dropped row, so a caller-side selection that thins one state's rows in particular conditions (a controlled-data residual test, say) leaves the two states with different wind mixes. A row flagged in the exclude_block column drops every row of its one-cycle block, for every turbine and in every reference mode, so both states lose the same conditions. Block ids are now computed whenever a block length is available, sum mode included. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e subset with the smallest sigma Every non-empty subset of the available references is estimated in full (own row selection, bootstrap); the subset with the smallest finite headline sigma becomes the sum estimate, alone or as the blend's reference leg, ties to the larger subset. A reference that tracks badly or is screened out for long stretches loses to the subsets without it. Every subset's result is written to a reference_subsets CSV; the results CSV records the choice and the references used. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…hts on label-permuted bootstraps Ranking reference subsets by the realised uplift's bootstrap sigma, and weighting the combined blend by the legs' realised sigmas, both favour the alternative whose realised uplift happened to come out low, because the on/off contrast noise is skewed: a winner's curse that biases the estimate down and under-reports sigma, and does not shrink with more data. With decision_basis="permuted" each component also bootstraps the same rows with the on/off labels shuffled within each reference block (block on/off counts kept), and the min_sigma choice and the blend weights read that sigma instead. The reported uplift and sigma are always the actual ones. Subsets being ranked skip their actual bootstrap and the winner is re-run, so the cost is unchanged. combine_estimates gains a `weighting` argument (another bootstrap's sigmas and resamples to take the weights from); the blend's variance still uses the actual ones. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…, decided on permuted labels Remove the reference_mode, reference_choice and decision_basis options (and the block_scaled mode). The method is always the minimum-variance blend of the reference leg (the summed power of the reference subset with the smallest label-permuted sigma) and the block leg (the test turbine against its own one-cycle block mean), with the subset choice and the blend weights judged on label-permuted bootstraps and the reported uplift and sigma always the actual ones. With no reference turbine the estimate is the block leg alone. reference_block is now a required constructor argument: one toggle cycle, stated by the caller, independent of pairing_max_gap and never inferred. The blend's per-bin n_records counts the used upgraded rows by the merged bin label, so labeled_rows reproduces it; the data-stats CSV counts rows of the turbines the estimate used only. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
- The placebo fixture is 10-minute SCADA: its 100-minute toggle cycle cannot tile an hourly grid, which the required reference_block now refuses. - A component with no resample spread takes the default correlation rather than tripping np.corrcoef's divide warning (identical-turbine placebo). - ToggleSpecialist hands the harness one uncertainty_diagnostics row per (condition, condition_bin), each leg's columns suffixed _<component>; the per-leg rows clashed with the harness's sigma column and its one-row-per-key rule. The uncertainty CSV keeps the per-leg rows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ived power lies in a band power_band=(lo, hi] restricts each leg's used rows to those whose bin power (rho_label * ref_total, the state-neutral baseline power the bins use) lies in the band, and estimates the uplift over them. The cut is a "power_band" stage in the selection accounting and is written to the run config. A caller measuring the uplift only where a control table is followed uses it instead of cutting on the test turbine's own power, which the state moves. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
aclerc
commented
Oct 7, 2026
… segment_imbalance_warning toggle_period says what the caller states (one toggle cycle) and no longer reads like the bootstrap block or the block leg. The imbalance warning was hard to interpret; the same per-stage kept fractions stay in selection_accounting and the selection CSV. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Several diagnostics contradict the blended estimate, the promised imbalance warning is absent, and block conditioning introduces correctness risks.
8 open findings
Combined estimate uses mismatched reference totals and usage · New Block origin ignores the toggle schedule start · New Unbounded subset enumeration can exhaust runtime and memory · New Block conditioning signal depends on post-treatment values · New Missing segment imbalance warning option and firing logic · New Validate power_band bounds during construction · New exclude_row behavior breaks the public test-only contract · New Copied reference ratios do not match the blended uplift · New
🧠 Review effort: Balanced
…diagnostics - bin_power: "baseline" (default) bins and power_band read power no upgraded row enters (rho_base * ref_total on the reference leg, the cycle's baseline-row mean on the block leg); "both_states" keeps the two-state mean, for campaigns with no true baseline state. - toggle_datum (required): the toggle-cycle grid is tiled from it, never from the first record, so a missing record cannot shift cycle boundaries. Toggle studies state one and draw treatment starts only on cycle boundaries. - Data-stats rows and results-CSV ratios per leg, plus the blend weight, so each leg and the headline are re-derivable; plots show one leg's own rows. - Refuse more than 8 references for the subset choice; validate power_band. - ColumnSchema.exclude_row: which turbines' flags count is the method's. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…blend weight The blended per-bin frame took sum_actual / sum_counterfactual from one leg while p50_uplift is the blend. Each leg's frame now carries sum_actual_<leg> / sum_counterfactual_<leg>, and the blended frame carries both legs' and the bin's weight_sum, so every bin's blend is re-derivable. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ach leg's uplift A block-bootstrap resample whose on-side or off-side reference energy is below a tenth of the whole campaign's (a calm spell or outage drawn over and over) now counts as non-finite instead of feeding the spread: on a near-calm fortnight the reported sigma reached 10^5 %. Every row of uncertainty_diagnostics carries uplift, per leg and for the blend, overall and per bin, so a consumer can see which leg an error came from without re-running. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.


Summary
ToggleSpecialistMethodbecomes one method with two blended legs and a symmetric row selection,and the caller now states the toggle cycle explicitly: its length (
toggle_period) and its grid(
toggle_datum). A selection on a signal the treatment affects (for example, keeping only rowswhere a control curve was in effect) must not leave the two toggle states with different wind
mixes. Pairing, cycle-level exclusion and per-stage accounting address that. The per-bin and
power_bandoptions read a power the treatment cannot move.The estimate
sum) compares the test turbine with the summed powerof a reference subset. A block leg (
block_mean) compares each row with its own toggle cycle'smean test power, and needs no reference at all, so a one-turbine campaign still gets an estimate.
Headline and per bin are blended by minimum-variance weights that account for the legs'
correlation, so the blend is never worse than its better leg.
and keeps the one with the smallest sigma (ties to the larger subset). More than 8 references
raises: pass the nearest or best-correlated.
bootstrap with the on/off labels shuffled within each toggle cycle, so neither can favour the
alternative whose realised uplift came out low. The reported uplift and sigma are the actual ones.
Row selection
pairing_max_gap(optional): a used row is kept only if the other segment has a used rowwithin the gap, inclusive. It must be a positive whole multiple of the timebase. This replaces
v0's
pairing_filter_timedelta_seconds, whose floor-to-timebase semantics silently dropped everyrow below 600 s on 10-min data.
columns.exclude_block(new column role): a flagged row removes its whole toggle cycle fromboth legs, so both states lose the same conditions. It is the symmetric alternative to
exclude_row.exclude_rownow reads the references' flags as well as the test turbine's. A timestampflagged on any turbine a leg uses is dropped from that leg, just as if that reference were down.
ColumnSchema.exclude_rowno longer promises "test turbine only"; it leaves which turbines'flags count to each method.
power_band(optional,(lo, hi]kW, finite withlo < hi): each leg keeps only rows whosebin power lies in the band.
MethodOutput.selection_accounting: rows kept per segment after each stage (segment,filters,exclude_row,pairing,block, andpower_bandwhen set), also written per leg to<run>_selection_<ts>.csv.The toggle cycle, stated by the caller
toggle_period(required): one full on + off cycle. It sets the block leg's cycles, the labelpermutation and
exclude_block.toggle_datum(required): any boundary of the cycle grid, never taken from the data, so amissing first record cannot shift every cycle. A
ToggleScheduleinput is checked against it.StudyConfigneeds atoggle_datum, and treatment starts are drawn only oncycle boundaries, as a clock-aligned controller toggles. This changes which starts a toggle
study draws.
Power bins
bin_power: the per-row power the bins andpower_bandread."baseline"(default): no upgraded row enters it. That isrho_base * ref_totalon thereference leg, and the mean of the cycle's baseline rows on the block leg.
"both_states": the two states' mean, for a campaign in which neither state is a truebaseline. Renaming which state is "on" then moves no row between bins.
Outputs
all/baseline/upgradedrows per leg. The results CSVreports each leg's ratios and uplift and the blend weight (
weight_sum). Each leg's uplift andthe headline are re-derivable from the CSVs.
sum_actual_<leg>,sum_counterfactual_<leg>) and the bin'sweight_sum, so each bin's blend is re-derivable.uncertainty_diagnosticsframe has one row per(condition, condition_bin), eachleg's columns suffixed
_<leg>.plots/2_filter/<test>_pairing_gap.png, undersave_plots): each used row'sgap to the nearest used row of the other segment, before and after pairing.
Breaking changes
toggle_periodandtoggle_datumare required.sum_actual/sum_counterfactualand the results CSV'sratio_baseline/ratio_upgradedare replaced by per-leg columns.1 / (1 + uplift/2)againstv1's two-state mean (bin_power="both_states"keepsv1's).exclude_rownow honours reference flags.StudyConfigneedstoggle_datum, and its draws change.Also
resample in
_paired_correlation).test_uplift_plots.py.Test plan
pair_withinboundaries; parity with v0's pairing filter at 10/30/60 minexclude_blockremoves the whole cycle in both legs; no flags change nothingtoggle_datum: required; the cycle grid survives a missing first record; an off-grid schedule and a tz mismatch are refusedbin_power:baselinebins do not move with the upgraded rows,both_statesbins do; withboth_states, swapping the state labels moves no row between binspower_band: every used row lies in the band; a constant uplift reads true inside it; bad bands refusedtests/benchmarking(not slow): 1363 passed; ruff and mypy clean🤖 Generated with Claude Code