Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

VAS Residuals

Dr. Thomas A. Bishop’s Variation Analysis System (VAS) decomposes total variation into meaningful components. ProcessBehavior calculates six residuals (R1-R6) that help answer different analytical questions. Equation numbers on this page refer to Bishop’s VAS documentation manual dated 10-12-2026.

The Residual Hierarchy

ResidualNameFormulaQuestions Answered
R1Response Centered at 0Y - Y̅How far is each point from the overall mean?
R2Unexplained (noise)Y - Y̅kt (DS 1); scaled difference of the effect-adjusted series (DS 2, 3) — see R2Is measurement variation stable?
R3Interaction(Y̅kt - Y̅k - Y̅t + Y̅) + R2Do factor effects change over time?
R4Time Main EffectY̅t - Y̅ + R2Are there time trends or shifts?
R5Design Condition Main EffectY̅k - Y̅ + R2Do process design conditions differ from each other?
R6Design Factor Main Effectαi + R2Does a specific design factor have a significant effect?

Where:

Accessing Residuals

Residuals are calculated during formulation and available after analysis:

study = pb.formulate(
    response=pb.cols.weight,
    factors=[pb.cols.lane],
    time=pb.cols.batch
)

# Access via study.dataset
print(study.dataset[['R1', 'R2', 'R3', 'R4', 'R5']].head())

# Access via result
result = study.execute()
print(result.residuals.head())

R1: Response Centered at 0

Purpose: View the original measurements as ± about zero, so the total range of variation is read directly rather than against an arbitrary process mean.

Formula: R1 = Y - Y̅ (Eq 14-2)

R1 is a pure location shift — it subtracts the overall mean and changes nothing else. It is also the building block the other residuals are derived from, which makes it the natural starting point when auditing a decomposition: if R1 doesn’t look like your raw data recentred, nothing downstream will be right either.

Chart: Xbar (subgroup means) or X (individual values). There is deliberately no S or mR chart for R1 — a constant shift leaves dispersion untouched, so those charts would be numerically identical to the response’s.

# Individual values, centred at zero
result = study.execute(chart='X', by=[], value='R1')
fig = result.plot(title='Response Centered at 0')

# Subgroup means, centred at zero
result = study.execute(chart='Xbar', value='R1')

Interpretation:

Availability: Bishop notes R1 can be calculated for data produced by all six sampling design states.

R2: Unexplained Residuals

Purpose: Assess the unexplained (noise) variation: what is left once the process mean, the condition and period effects and their interaction are accounted for (the interaction exactly in DS 1, approximately in DS 2 and 3, where differencing the adjusted series removes interaction that changes smoothly over time).

Formula by DS:

Because Z carries no condition or period effect, the step from one condition’s last period to the next condition’s first period no longer carries the level difference between conditions, and a trend common to all conditions is not read as noise. The scale factor puts R2 on the noise (sigma) scale, so the standard deviation of R2 estimates sigma directly. Internally this method is named ma2: Bishop’s step is a size-2 moving average of Z, and R2j is Zj minus that average, divided by c.

Chart: S chart with value='R2' (for replicated data) or X

# Chart the unexplained (noise) variation
result = study.execute(chart='S', value='R2')
fig = result.plot(show_zones=True, title='Unexplained Variation (R2)')

Interpretation:

R3: Interaction Residuals

Purpose: Detect factor × time interactions.

Formula: R3 = (Y̅kt - Y̅k - Y̅t + Y̅) + R2 (Eq 14-14)

This removes both main effects, leaving the interaction plus the unexplained noise. In DS 1, where R2 = Y - Y̅kt, it reduces to Y - Y̅k - Y̅t + Y̅.

Chart: X with value='R3'

result = study.execute(chart='X', by=['lane'], value='R3')
fig = result.plot(show_zones=True, title='Factor × Time Interactions')

Interpretation:

R4: Time Main Effect Residuals

Purpose: Detect time-related patterns (trends, shifts, cycles).

Formula: R4 = Y̅t - Y̅ + R2

This combines the time effect with the unexplained noise (R2).

Chart: X with value='R4' (stratified by factor), or Xbar with value='R4' (aggregated across factors).

# Stratified X — one chart per factor level
result = study.execute(chart='X', by=['lane'], value='R4')
fig = result.plot(show_zones=True, show_rules=True, title='Time Effects')

# Xbar — subgroup means across factor levels
result = study.execute(chart='Xbar', value='R4')
fig = result.plot(show_zones=True, title='Time Effects (Xbar)')

When charting R4 on Xbar, limits use R2’s Sbar (the noise), not R4’s own within-group std. See Chart Types: Xbar limits note for details.

Interpretation:

R5: Design Condition Main Effect Residuals

Purpose: Detect true differences between process design conditions.

Formula: R5 = Y̅k - Y̅ + R2

This combines the factor effect with the unexplained noise (R2).

Chart: X with value='R5' (stratified by factor), or Xbar with value='R5' (aggregated by factor).

# Stratified X — one chart per factor level
result = study.execute(chart='X', by=['lane'], value='R5')
fig = result.plot(show_zones=True, title='Design Condition Main Effects')

# Xbar — subgroup means by factor
result = study.execute(chart='Xbar', value='R5')
fig = result.plot(show_zones=True, title='Design Condition Main Effects (Xbar)')

When charting R5 on Xbar, limits use R2’s Sbar (the noise), not R5’s own within-group std. This prevents between-cell variance from collapsed dimensions from inflating limits. See Chart Types: Xbar limits note for details.

Interpretation:

R6: Design Factor Main Effect Residuals

Purpose: Isolate a specific design factor’s main effect combined with the unexplained noise (R2).

Formula: R6 = αi + R2

Where αi = mean(R5 | factor level(s)) — the mean of R5 for each level of the specified factor(s).

R6 differs from R5 in that it isolates the effect of individual factors when multiple factors exist. R5 contains the combined effect of all factors; R6 lets you examine one factor at a time.

Chart: Xbar with value='R6' and by specifying which factor(s) to examine.

# In a two-factor study, examine FACTOR 1's effect
result = study.execute(chart='Xbar', value='R6', by=['factor 1'])
fig = result.plot(show_zones=True, title='Factor 1 Main Effect')

# Re-centered on original measurement scale
result = study.execute(chart='Xbar', value='R6', by=['factor 1'], recentered=True)

Key details:

Interpretation:

How Effect Residuals Are Charted

When you chart an effect-carrying residual (R3, R4, or R5) on Xbar or S, the system substitutes R2 as the dispersion basis. Understanding this substitution is key to interpreting these charts correctly.

Why R2 sets the limits

R3, R4, and R5 contain structural effects by design — that’s what makes them useful. But if R4’s own standard deviation set the Xbar limits, the time effect would widen them, defeating the purpose of looking for signals beyond the expected variation. By substituting R2 (the unexplained noise: within-cell in DS 1, the scaled difference in DS 2 and 3), the limits reflect only unexplained variation, making structural effects visible as signals.

Chart-by-chart behavior

ChartWhat is plottedWhat sets the limits
XbarSubgroup means of the requested residualR2’s within-group Sbar
SR2’s within-group std (not the requested residual’s)R2’s Sbar for CL and limits
XIndividual residual valuesMoving range of the residual itself (no R2 substitution)

The S chart surprise

This is the most counterintuitive behavior: execute(chart='S', value='R3') plots R2’s within-group standard deviation, not R3’s. The S chart always answers “is the noise stable?” regardless of which residual you request. This is correct — the S chart’s job is to verify that the dispersion basis (R2) is stable before you interpret the Xbar chart above it.

When does this matter?

The R2 substitution only matters when by collapses factors. At the full RSG level (all factors in by), the residual’s within-group standard deviation equals R2’s, so there is no visible difference. When you collapse — e.g., by=['factor1'] in a two-factor study — R2 still measures only the noise, while the residual’s own std would include between-cell variance from the collapsed dimension.

For X charts, there is no substitution. The moving range is always computed from the requested residual’s own values.

Re-centered Residuals

By default, residual charts are centered at zero. Use recentered=True to show on the original measurement scale:

# Zero-centered (default)
result = study.execute(chart='X', by=['lane'], value='R4')
# Centerline at 0, values show the time effect plus R2

# Re-centered on original scale
result = study.execute(chart='X', by=['lane'], value='R4', recentered=True)
# Centerline at grand mean, values on original measurement scale

Re-centering formulas:

So an effect chart of RCR5 by condition, RCR4 by period, or RCR3 by subgroup plots that effect plus R2 on the measurement scale. Chart period effects from R4: an Xbar of RCR3 by period is flat, because the interaction sums to zero over conditions. Re-centering shifts the chart by Y̅; the limit width still comes from R2 (Xbar/S) or the residual’s own moving ranges (X).

Note on recentered moving ranges: For recentered residuals on X charts, the moving range is computed from the non-recentered version (e.g., RCR3 uses MR from R3). For RCR1 and RCR3–RCR6 this changes nothing, since re-centering adds a constant; for RCR2 it keeps the steps between cell means out of the moving ranges.

Residual Availability by DS

DSR1R2R3R4R5R6
1 (Full Replication)✅✅ Within-cell✅✅✅✅
2 (No Replication)✅✅ Scaled difference✅✅✅✅
3 (Partial)✅✅ Scaled difference✅✅✅✅
4 (Incomplete, No Singletons) → ADS 1✅✅ Within-cell✅✅✅✅
5 (Incomplete, No Replication) → ADS 2✅✅ Scaled difference✅✅✅✅
6 (Incomplete, With Singletons) → ADS 3✅✅ Scaled difference✅✅✅✅

R6 requires factors (it is computed from R5 and R2 at execute() time).

Note on R2 calculation: R2 is the one residual whose formula depends on structure, and the choice is made on the analytical design state (ADS), after cleaning:

DS 4, 5 and 6 are sampling states; they collapse to ADS 1, 2 and 3 once empty cells are dropped.

When R2 is unavailable: in ADS 2 and 3 the R2 scale factor is undefined when there are fewer than 2 process design conditions or fewer than K + 2 observations (M < K + 2). R2 then has no value: formulate() warns, R2–R6 (and their re-centered forms) are not offered, and execute() and why_not() give the reason. loss_function() and maximum_information() raise ValidationError, and capability() reports potential capability as unavailable with the same reason. R1 is still available.

Analysis Workflow with Residuals

Step 1: Check R2 (Measurement Stability)

result = study.execute(chart='S', value='R2')
signals = result.detect_signals(chart='S')

if signals.has_signals:
    print("Within-cell variation is unstable!")
    print("Investigate measurement system before proceeding.")

Step 2: Check R3 (Interactions)

result = study.execute(chart='X', by=['lane'], value='R3')
signals = result.detect_signals(chart='X')

if signals.has_signals:
    print("Significant factor × time interactions detected.")
    print("Factor effects are not consistent over time.")

Step 3: Check R4 (Time Effects)

result = study.execute(chart='X', by=['lane'], value='R4')
fig = result.plot(show_zones=True, show_rules=True)

Step 4: Check R5 (Design Condition Main Effects)

result = study.execute(chart='X', by=['lane'], value='R5')
fig = result.plot(show_zones=True, highlight_signals=True)

Interpreting the Complete Picture

R2 StatusR3 StatusR4 StatusR5 StatusConclusion
StableStableStableStableProcess in control
Unstable---Fix measurement first
StableSignals--Investigate interactions
StableStableSignals-Time-related changes
StableStableStableSignalsTrue factor differences

Example: Complete Residual Analysis

from processbehavior import ProcessBehavior

pb = ProcessBehavior(df)
study = pb.formulate(
    response=pb.cols.weight,
    factors=[pb.cols.lane],
    time=pb.cols.batch
)

# Check available residuals
print(f"Available residuals: {study.residuals}")

# Analyze R2 on S chart
result_r2 = study.execute(chart='S', value='R2')
signals_r2 = result_r2.detect_signals(chart='S')
print(f"R2 on S: {signals_r2.count} signals")

# Analyze R3-R5 on stratified X charts
for residual in ['R3', 'R4', 'R5']:
    result = study.execute(chart='X', by=['lane'], value=residual)
    signals = result.detect_signals(chart='X')
    print(f"{residual} on X: {signals.count} signals")

Best Practices

  1. Always check R2 first - Measurement stability is foundational

  2. Use DS 1 when possible - Full replication gives exact residuals

  3. Interpret in sequence - R2 → R3 → R4 → R5

  4. Consider re-centering - Easier to explain on original scale

  5. Document findings - Record which residuals showed signals

Mathematical Details

R1: Total Deviation

R1 = Y - Ȳ

R1 averages to zero over the cells: the mean of the cell means of R1 is 0, because Ȳ is the unweighted mean of the cell means. The sum over all observations is 0 only when every cell has the same number of observations.

R2: Within-Cell (DS 1)

R2 = Y - Ȳ_kt

Where Y̅kt is the mean of observations in cell (k, t) (Eq 14-3).

R2: Scaled Difference (DS 2 and 3, any one-observation cell)

Z    = Y - Ȳ_k - Ȳ_t + Ȳ
R2_j = (Z_j - Z_{j-1}) / (2 · c(K, M))      j = 2 … M, condition-then-time order
c(K, M) = sqrt( (K - 1) / (2K) · (1 - K / (M - 1)) )

K is the number of process design conditions present and M the number of observations (M = KT in DS 2); c(K, M) is the R2 scale factor (Eqs 14-4..14-8 for DS 2, 14-9..14-13 for DS 3, 5 and 6). The first observation in the sequence has no value (NaN). The scale factor is undefined, and R2 unavailable, when K < 2 or M < K + 2.

R3: Interaction + Unexplained

R3 = (Ȳ_kt - Ȳ_k - Ȳ_t + Ȳ) + R2
   = InteractionEffect + R2

(Eq 14-14.) In DS 1 this equals Y - Ȳ_k - Ȳ_t + Ȳ, since Y = Ȳ_kt + R2 there.

R4: Time Main Effect + Unexplained

R4 = Ȳ_t - Ȳ + R2
   = TimeEffect + R2

R5: Design Condition Main Effect + Unexplained

R5 = Ȳ_k - Ȳ + R2
   = FactorEffect + R2

Maximum Information Analysis

The Maximum Information analysis examines the noise floor of your process by analyzing R2 residuals via an X chart and percentage histogram. It answers: What variation is inherent to the system, and is it predictable?

The X chart of R2 has limits mean ± (3/1.128)·mR̄ and σ̂ = mR̄/1.128. The values are whatever R2 is at your design state (the scaled difference in ADS 2 and 3), and maximum_information() raises ValidationError when R2 is unavailable.

mi = study.maximum_information()

# Key statistics
print(f"Noise floor sigma: {mi.sigma_hat}")   # mR-bar / 1.128 of R2
print(f"Natural process limits: [{mi.lpl}, {mi.upl}]")
print(f"Signals in noise: {mi.n_signals}")     # Points beyond limits

# Visualize
mi.plot()                        # Combined X + histogram
mi.plot(view='xmr')              # X chart of R2 only
mi.plot(view='histogram', bins=15)  # Percentage histogram only

Interpretation:

Next Steps