Skip to content

Phased limits on Xbar and S charts (#144, PR 2) - #150

Merged
cnicholas merged 1 commit into
feat/phased-xbar-s-refactorfrom
feat/phased-xbar-s
Oct 18, 2026
Merged

cnicholas merged 1 commit into
feat/phased-xbar-s-refactorfrom
feat/phased-xbar-s

Conversation

@cnicholas

Copy link
Copy Markdown
Owner

Closes #144 (PR 2 of 4). Stacked on #149: the base is feat/phased-xbar-s-refactor, so the diff shows only the feature; it retargets when #149 merges.

Summary

  • What: execute(chart='Xbar' | 'S', phased=True) gives each factor combination its own centre line and limits on one chart, drawn as stepped lines with a labelled divider at each change, the way phased X/mR does for its lanes. New result.phase_statistics(chart) returns one row per phase; chart data and chart_table gain a phase column.
  • Why: Dr. Bishop asked for staging on Xbar/S on 10/13 (Wheeler's staged average charts recompute the limits after a known change; a staged X chart is no substitute when the data come in subgroups). He answered the two design questions on 10/18: each stage has its own S-bar and sigma-hat ("treat each stage separately as if it were a single analysis"), and with other factors pooled into the subgroup the centre is "the average of the rational subgroup averages for that stage".
  • Scope: each phase is computed exactly as by=[time] computes one per-condition chart, through the helpers Extract the per-group Xbar/S estimator for phased charts (#144, PR 1) #149 extracted, and the phases are concatenated in display order. by=None (or all factors + time) phases on every combination; by=['stage', time] pools the other factors into the subgroup and phases on the stage. by=[time], residual values and calibrations are refused with messages that say what to use instead.

Contract / invariants

  • Defaults unchanged — validation/xbar_s_sweep.py: identical fingerprints for all 504 phased=False Xbar/S requests on PM SDS 1 and 3, before and after
  • Residuals unaffected — residuals never recomputed; residual values are refused on phased Xbar/S
  • Row/index alignment preserved — unphased output untouched; the phased frame has the same rows, in the same order, as the unphased chart (test_unphased_output_is_unchanged)
  • Output schema compatible — additive: phase column, phases payload key, new metadata keys; the four-key statistics shape holds (None + limits_vary, as phased X/mR). Existing golden masters untouched; one new scenario (phased_xbar_s) added
  • Pinned error strings — test_phased_requires_xmr_or_r became test_phased_requires_a_limits_chart: Histogram is refused with the same only valid for fragment, Xbar is accepted

Behaviour changes

  • phased=True is accepted on Xbar and S (it raised only valid for X or mR charts).
  • Phased Xbar/S: statistics {'N': None, 'center': None, 'lpl': None, 'upl': None, 'limits_vary': True}; metadata phased, run_rules_applicable=False, phase_by, lane_boundaries, single_point_phases, insufficient_phases, group_col; no n_avg (per-phase N is in phase_statistics). detect_signals runs Rule 1 against each phase's limits.
  • A phase with no replicated subgroup keeps its points with NaN limits and is named in insufficient_phases; the request raises only when no phase has one.
  • Lane dividers on a single Xbar/S render are labelled (these charts have no time column, so no cell band names the lanes).
  • execute() docstring for phased rewritten (it still said "requires by=[]").

Methodology

  • Methodology change, validated against Bishop's reference data: each phase is the per-condition chart (10-12 manual Eq 10-9 within the phase for the centre; Eqs 11-5, 11-8, 11-9, 11-16, 11-17, 11-20 for sigma-hat and limits), so the gate's VAS comparisons of by=[time] charts cover the arithmetic. Tom's cavity data: the phased chart reproduces every per-cavity value in his file and Minitab screenshot (A 14.65 / 12.677 / 16.623, S̄ 1.382, S UPL 2.888; B 8.97; C 7.47; D 7.86).

Key design decisions

  • Centre of a pooled phase: the mean of the plotted (pooled) subgroup means, per Tom, not the mean of the fine cell means the issue proposed. The two agree when the design is balanced or the stage is the only factor; test_pooled_phase_equals_reformulated_study pins the unbalanced case, where phased by=['FACTOR 1', time] equals a study formulated without FACTOR 2. The unphased pooled chart keeps its fine-cell centre (invariant); whether it should also follow Tom's rule is a question for him, outside this PR.
  • Per-phase loop over the aggregated subgroup frame (one row per subgroup, not per observation), reusing _fit_xbar_group / _apply_xbar_limits / _apply_s_limits, so a phase equals focus(k) to the last digit, including the round-then-slice order the stratified S chart uses. 1,000 phases run in well under 5 s (slow-marked test).
  • phase is the lane label ('A', '1_2'), the same string as the focus() key and the divider label.
  • by=[time] refused, not treated as a no-op: it would return a stratified result with nested statistics, the opposite of what the caller asked for; the message points to by=None.

Tests

  • tests/test_phased_xbar_s.py (45): oracles — phased frames equal the concatenated by=[time] focus(k) frames on PM SDS 1 and 3 for Xbar and S, phase_statistics equals the per-stratum statistics, pooled phases equal the re-formulated study (balanced and unbalanced), by=None = [] = full by in any order, Tom's cavity file against its VAS columns; hand-computed two-stage example (c4/b3/b4), Eq 11-5 within a phase, n_mode='average' per phase, n_sigma=2; edge cases — one-reading subgroups on Xbar only, a single-subgroup phase, an insufficient phase, no replicated phase; contracts — solo = companion, statistics/metadata/column shapes, lane positions, chart_table, phase_statistics refusals, Rule 1 per phase, stage order (strings, sortable labels, ordered Categorical), unphased output unchanged, 1,000 phases under 5 s; one test per refused request and the accepted shapes; calibration context 'phased Xbar' / 'phased S'; plotting smoke (stepped CL/UPL/LPL, the note, labelled dividers, no zones)
  • Updated: test_phased_limits.py (Histogram refused, Xbar accepted), test_supports_calibration.py (grid runs phased for all four charts; new context test), test_result_tabular.py (phase in _NON_VALUE_COLS), golden scenario phased_xbar_s
  • pytest tests/ — 2628 passed, 7 skipped
  • ruff check .
  • mypy processbehavior — 14 errors, all pre-existing, none in the touched code
  • python validation/e2e_bishop_report.py — 1547 passed, 0 failed, 4 pending; docs/reference/validation.md unchanged
  • validation/xbar_s_sweep.py — 0 differing lines against the pre-Extract the per-group Xbar/S estimator for phased charts (#144, PR 1) #149 baseline

Notes

  • Docs: chart-types.md gains "Staged Xbar/S" plus rows in the order-free and summary tables; api.md gains "Phases" and the phased comment; CHANGELOG Added / Changed / Fixed.
  • Fixture: tests/fixtures/data/wheeler_cavity_thickness.csv is Tom's file verbatim, including VAS's per-cavity chart columns, which the oracle test reads.
  • Follow-ups (not in this PR): PR 3 (phase column and phase_statistics on phased X/mR, companion mR single_point_phases), PR 4 (ordered Categoricals on X/mR lane order); ask Tom about the unphased pooled centre; the app's "Phased limits" option for Xbar/S.

execute(chart='Xbar' | 'S', phased=True) gives each factor combination its own
centre line and limits on one chart, drawn as stepped lines with a labelled
divider at each change, as phased X/mR does for its lanes. Each phase is
computed as the per-condition chart of by=[time] computes one stratum: the
mean of its subgroup means as centre, its own S-bar and sigma-hat, its own
limits (10-12 manual Eqs 11-5, 11-8, 11-9, 11-16, 11-17). With other factors
pooled into the subgroup (by=['stage', time]) the centre is the mean of the
pooled subgroup averages, as Dr. Bishop specified on 2026-10-18. On Wheeler's
four-cavity thickness data the per-cavity centres and limits match Minitab's
staged Xbar-S chart to the digit.

New result.phase_statistics(chart); chart data and chart_table gain a phase
column; metadata carries phase_by, lane_boundaries, single_point_phases and
insufficient_phases. by=[time], residual values and calibrations are refused
with messages that say what to use instead. phased=False output is unchanged
(validation/xbar_s_sweep.py: 0 differing lines); gate 1547 / 0 / 4; full
suite 2628 passed.
@cnicholas
cnicholas force-pushed the feat/phased-xbar-s-refactor branch from 732512f to 57f499c Compare October 18, 2026 22:21
@cnicholas
cnicholas merged commit dc7ceef into feat/phased-xbar-s-refactor Oct 18, 2026
17 checks passed
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