Skip to content

Build the mid-course prediction interval at the decision point; add MidCourseCorrector.predict - #541

Open
kgdunn wants to merge 4 commits into
mainfrom
claude/batch-mid-course-correction-hdnkzp
Open

kgdunn wants to merge 4 commits into
mainfrom
claude/batch-mid-course-correction-hdnkzp

Conversation

@kgdunn

@kgdunn kgdunn commented Sep 4, 2026

Copy link
Copy Markdown
Owner

Follow-up to #516, found while reworking the book chapter (kgdunn/pid-book#272) around when and how to apply mid-course correction.

The problem. MidCourseCorrector's dead band compared the projected shortfall with a prediction interval built from the model's full-row training RMSE. That interval cannot see that a score estimate resting on a few observed columns is far less certain than one resting on the whole row, nor that the restricted trimmed-score-regression estimator can be ill-conditioned early in the batch. Measured on the chapter's bioreactor: class A's estimator has a condition number near 8e5 at day 2 (a few hundred from day 4 on) and projects batches that finish at 8 g/L to -140 g/L, and the old interval declared those projections precise. The harmful early corrections in the decision-day sweep came from exactly this. On 40 held-out batches the old 95% interval covered about 70% of outcomes even at the decision point that matters.

The fix.

  • limits_at(k) now also measures the model's prediction error at the decision point (rmse_k): the training batches re-projected under the candidate pattern, their predicted quality against their measured quality, on N - A - 1 degrees of freedom (the same construction as PLS.prediction_interval; at the full row it equals the training RMSE). It also reports the condition numbers of the monitoring and candidate operators.
  • New public MidCourseCorrector.predict(batch_so_far, initial_conditions=, schedule=, k=): the monitoring question at a decision point. Returns the prediction with its interval at that point, the SPE of the batch so far against its limit (in_control), the candidate row's T2 against the per-decision-point covariance, and the condition number. A corrector built with only the model, the nominal schedule and mv_tags can predict but not correct (the y_target check moved from the constructor to correct).
  • correct() builds both gates on predict() and reports y_hat_no_change, half_width and condition_number on every outcome. The no-change prediction and the movement penalty use the currently planned remainder (the implemented schedule after an earlier decision point) rather than always the nominal schedule.
  • evaluate_control_policies and the two agent tools default to dead_band=1.0 (the class default: correct only when the whole interval falls short of the target) instead of the 2.5 that compensated for the old interval; the evaluation records each batch's no-change prediction and half-width.
  • Bug fix: midcourse_correction no longer fails with n_knots set when a tag has a single remaining free sample (the last decision point before the batch ends).
  • The user guide's headline table, sweep table and decision-point window are re-measured under the new dead band (five corrected, none harmed; window days 3 to 5).

Measured. With the new interval, on the 40 held-out test batches of the chapter's seed chain, the 95% interval's coverage is 0.95, 0.97, 0.98, 1.00, 0.95 at days 1 to 5 (0.80 by day 9, where the model under-predicts replay batches by about 0.6 g/L). The funnel of one poor batch narrows from +/-4.0 g/L at day 0.5 to +/-1.75 at day 4, where the old interval was flat at about +/-0.85.

Version. 1.79.1 -> 1.80.0 (a new public method and a behavioural change of the dead band). main took 1.79.0 for #539 and 1.79.1 for #547 while this branch was open: the branch was restarted from main after #516's squash merge, and #547 was merged in afterwards; CITATION.cff and the changelog are synced to 1.80.0, with the 1.80.0 section above the 1.79.1 entry.

Gates. ruff check . and ruff format --check . clean; mypy src/process_improve clean; tests/batch/ (142 tests) and the two slow executed-simulator tests in test_batch_control.py pass on the merged branch.

Companion PRs: kgdunn/figures#83 (figures regenerated with this package) and kgdunn/pid-book#272 (the chapter).

🤖 Generated with Claude Code

https://claude.ai/code/session_01NZAyiPT8xURSdi24yFKCra

The dead band of MidCourseCorrector compared the projected shortfall with
an interval built from the model's full-row training error. That interval
cannot see that a score estimate resting on a few observed columns is far
less certain than one resting on the whole row, nor that the restricted
estimator can be ill-conditioned early in the batch: on the chapter's
bioreactor, class A's estimator has a condition number near 8e5 at day 2
and projects batches that finish at 8 g/L to -140 g/L, and the old
interval declared those projections precise. The decision-day sweep's
harmful early corrections came from exactly this.

limits_at(k) now also measures the model's prediction error at the
decision point, by comparing the training batches' quality predicted from
their candidate-pattern score estimates with their measured quality, and
reports the operators' condition numbers. A new public predict() answers
the monitoring question at a decision point (prediction, interval at that
point, SPE validity statistics, condition number) and correct() builds its
gates on it. On 40 held-out batches the interval's coverage is 95 to 100%
at days 1 to 5, and the funnel of one batch narrows from +/-4.0 g/L at day
0.5 to +/-1.75 at day 4, where the old interval was flat. The evaluation's
dead band default moves from 2.5, which compensated for the old interval,
to the class default of 1.0: correct only when the whole interval falls
short of the target.

Also: the no-change prediction and the movement penalty now use the
currently planned remainder rather than always the nominal schedule (the
same thing unless an earlier decision point corrected the batch); a
corrector built without a target can predict but not correct; and
midcourse_correction no longer fails with n_knots when a tag has a single
remaining free sample.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZAyiPT8xURSdi24yFKCra
Five of forty batches corrected (all class C), none harmed, mean gain
+1.92 g/L, sd 1.20 -> 0.78 g/L, 67% of the oracle improvement; the
decision-day sweep no longer harms early because the interval at the
decision point refuses the ill-conditioned projections.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZAyiPT8xURSdi24yFKCra
@kgdunn
kgdunn force-pushed the claude/batch-mid-course-correction-hdnkzp branch from 0356655 to ca5ccdf Compare September 5, 2026 06:19
@codecov

codecov Bot commented Sep 5, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 98.52941% with 1 line in your changes missing coverage. Please review.

Files with missing lines Patch % Lines
src/process_improve/batch/control.py 98.50% 0 Missing and 1 partial ⚠️

📢 Thoughts on this report? Let us know!

Codecov flagged the two lines of MidCourseCorrector.predict that no test
reached: the default of k to the number of samples handed in, and the
error raised when fewer samples than k are supplied. Both are now
exercised in the existing predict test.

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

Main took 1.79.1 for the documentation follow-up; this branch stays at
1.80.0 with its changelog section above the 1.79.1 entry.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NZAyiPT8xURSdi24yFKCra
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.

2 participants