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.

Stratified Analysis

Stratified analysis creates separate control charts for each factor level. This is powerful when:

  • Each stream (machine, lane, operator) has its own behavior

  • You want to detect changes within individual streams

  • Comparing streams directly would mask within-stream signals

What You’ll Learn

  1. Create stratified X/mR charts for multiple streams

  2. Understand when to use stratified vs. combined analysis

  3. Navigate between individual stream charts

  4. Detect signals within each stratum

Setup

import numpy as np
import pandas as pd
from processbehavior import ProcessBehavior

Create Multi-Stream Data

Simulate a filling line with 4 lanes, each with:

  • Different baseline performance

  • Different variation levels

  • Lane C has a special cause event at time 15

np.random.seed(42)

lanes = ['Lane_A', 'Lane_B', 'Lane_C', 'Lane_D']
n_times = 20

# Each lane has different characteristics
lane_config = {
    'Lane_A': {'mean': 100, 'std': 1.0},
    'Lane_B': {'mean': 101, 'std': 1.5},
    'Lane_C': {'mean': 99, 'std': 1.2},
    'Lane_D': {'mean': 100.5, 'std': 0.8}
}

data = []
for t in range(n_times):
    for lane in lanes:
        config = lane_config[lane]
        
        # Special cause: Lane C at time 15
        special = 6 if (lane == 'Lane_C' and t == 14) else 0
        
        value = config['mean'] + special + np.random.normal(0, config['std'])
        data.append({
            'batch': t + 1,
            'lane': lane,
            'fillweight': round(value, 2)
        })

df = pd.DataFrame(data)
print(f"Dataset: {len(df)} observations")
print(f"Structure: {len(lanes)} lanes x {n_times} batches")
df.head(8)
Dataset: 80 observations
Structure: 4 lanes x 20 batches
Loading...

Formulate the Study

pb = ProcessBehavior(df)

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

print(f"Observed: ODS {study.observed_design_state.sds} | Analytical: ADS {study.analytical_design_state.sds}")
print(f"Why: {study.ads_description}")
print(f"Valid charts: {study.valid_charts}")
print(f"Recommended: {study.recommended_chart}")
Observed: ODS 2 | Analytical: ADS 2
Why: No replication (all cells n=1)
Valid charts: ['Histogram', 'Xbar', 'S', 'X', 'mR']
Recommended: X

Combined vs. Stratified Analysis

With factors and time, you have two options:

Combined Analysis (Xbar-S)

  • Compares factor levels against each other

  • Uses pooled within-group variance

  • Good for detecting between-group differences

Stratified Analysis (X/mR per level)

  • Each factor level gets its own chart

  • Uses within-level variance for each

  • Good for detecting within-group changes over time

Create Stratified X Charts

# Execute an X chart stratified by lane - one chart per lane
result = study.execute(chart='X', by=['lane'])

print(f"Charts created: {result.all_charts}")
print(f"Stratified: {result.is_stratified}")
print(f"Strata: {result.list_strata()}")
Charts created: ['X']
Stratified: True
Strata: ['Lane_A', 'Lane_B', 'Lane_C', 'Lane_D']

View Individual Stream Charts

# Get chart data - all lanes in one DataFrame with 'rsg' column for the lane
chart_data = result.get_chart('X')
print("Stratified Chart Data (all lanes):")
print(f"Total observations: {len(chart_data)}")
print()

# Filter to just Lane A
lane_a_data = chart_data[chart_data['rsg'] == 'Lane_A']
print("Lane A Data:")
lane_a_data.head(10)
Stratified Chart Data (all lanes):
Total observations: 80

Lane A Data:
Loading...
# Statistics are stored per stratum
stats = result.get_statistics('X')

for lane in lanes:
    s = stats[lane]
    print(f"{lane}: CL={s['center']:.2f}, UCL={s['upl']:.2f}, LCL={s['lpl']:.2f}")
Lane_A: CL=99.92, UCL=102.25, LCL=97.58
Lane_B: CL=100.62, UCL=105.58, LCL=95.66
Lane_C: CL=98.98, UCL=104.41, LCL=93.56
Lane_D: CL=100.58, UCL=103.25, LCL=97.91

Visualize All Lanes Together

Use faceting to see all lanes in one view:

# Faceted plot shows all lanes
fig = result.plot(
    chart='X',
    show_zones=True,
    highlight_signals=True
)
fig.show()
Loading...

Focus on One Lane

focus() narrows a stratified result to a single stratum — a FocusedAnalysisResult with the same interface, filtered to that lane:

# Lane C carries the special cause - focus on it
focused = result.focus('Lane_C')

print(f"Focused on: {focused.focused_stratum}")
print(f"Stats: {focused.get_statistics('X')}")

fig = focused.plot(chart='X', show_zones=True, highlight_signals=True, show_rules=True)
fig.show()
Focused on: Lane_C
Stats: {'N': 1, 'center': np.float64(98.983), 'lpl': np.float64(93.555), 'upl': np.float64(104.411)}
Loading...

Detect Signals Per Lane

Each stratum has its own limits, so signal detection runs per lane — focus a stratum and detect:

# Focus each lane and detect signals against that lane's own limits
for lane in result.list_strata():
    sig = result.focus(lane).detect_signals(chart='X')
    print(f"{lane}: {sig.count} signal(s)")

# Drill into the lane that fired
lane_c = result.focus('Lane_C').detect_signals(chart='X')
print(f"\nLane_C flagged observations: {sorted(lane_c.flagged_observations)}")
Lane_A: 0 signal(s)
Lane_B: 0 signal(s)
Lane_C: 1 signal(s)
Lane_D: 0 signal(s)

Lane_C flagged observations: [np.int64(14)]

Iterate Through Charts

iter_charts() yields (name, data, statistics) for programmatic access; per-stratum statistics live under the stratum key:

for name, data, stats in result.iter_charts():
    print(f"chart {name}: {len(data)} points")
    for lane in result.list_strata():
        s = stats[lane]
        print(f"  {lane}: center={s['center']:.2f}, upl={s['upl']:.2f}, lpl={s['lpl']:.2f}")
chart X: 80 points
  Lane_A: center=99.92, upl=102.25, lpl=97.58
  Lane_B: center=100.62, upl=105.58, lpl=95.66
  Lane_C: center=98.98, upl=104.41, lpl=93.56
  Lane_D: center=100.58, upl=103.25, lpl=97.91

Compare with a Combined Chart

What if we ignore the lanes? by=[] explicitly collapses the factors into one interleaved stream — one X chart of every measurement in time order:

# Explicit collapse: one combined X chart across all lanes
result_combined = study.execute(chart='X', by=[])

sig = result_combined.detect_signals(chart='X')
print(f"Combined-chart signals: {sig.count}")

fig = result_combined.plot(chart='X', show_zones=True, highlight_signals=True,
                           title='Combined X Chart (all lanes interleaved)')
fig.show()
Combined-chart signals: 9
Loading...

Key Difference

The combined chart fires far more signals than the stratified charts — and that’s the problem. Interleaving four lanes with different baselines makes every lane-to-lane jump look like a time signal: the between-lane structure aliases as within-stream behavior, and the one real special cause (Lane C at batch 15) drowns in the noise.

The stratified approach gives each lane its own center and limits, so the Lane C shift stands out alone.

When to Use Each Approach

Use Combined (Xbar-S, or a collapsed X chart) When:

  • Comparing lanes/machines/operators to each other

  • Looking for systematic differences between groups

  • Setting up initial process capability

  • (Xbar-S itself needs replicated cells — design state 1)

Use Stratified (X/mR) When:

  • Monitoring individual streams over time

  • Each stream has different inherent variation

  • Want maximum sensitivity to within-stream changes

  • Historical data shows streams behave differently

Summary

In this tutorial, you learned:

  • A stratified X chart creates one chart per factor level

  • Each stratum has its own control limits

  • Use faceted plots to view all strata together; focus() narrows to one

  • Stratified analysis is more sensitive to within-stream changes

  • Choose stratified vs. combined based on your question

Next Steps