A busy café measures how long a customer waits for their drink (wait_sec). Baristas
time four orders a day — two at the Peak rush (08:00, 13:00) and two at Off-peak
lulls (10:00, 16:00) — Monday through Saturday for 16 weeks.
Three real things happen along the way:
Week 8 — a new espresso machine is installed.
Week 12 — a new point-of-sale (POS) system goes live.
Weeks 14–16 — several new baristas are hired.
We’ll let the process behavior charts tell us what actually changed, when, and how — and see why you need both a location chart and a dispersion chart to catch it all.
Load and formulate¶
processbehavior models data as a factor × time grid. Here the process-design
factor is daypart (Peak vs Off-peak) and the time axis is date. Four readings a day
(two per daypart) give full replication, so this is a complete (SDS 1) design.
from processbehavior import Calibration, load_coffee_shop
pb = load_coffee_shop()
pb.data.head()study = pb.formulate(response='wait_sec', factors=['daypart'], time='date')
print(study.design())Design Report (1 factors)
Design-state lineage:
PDS (Planned): no plan supplied
SDS (Sampling): 1 (Full Replication)
ADS (Analytical): 1 (Full Replication)
Min cell size: 2 | K: 2 | T: 96 | R: 192 | N: (min=2, median=2.0, max=2)
Factors:
daypart: observed=['Off-peak', 'Peak']
Structure: Complete structure
Available analyses (ADS 1):
Primary: Histogram, Xbar *, S, X, mR
R2: S, X
R3: Xbar, S
R4: Xbar, S
R5: Xbar, S
R6: Xbar, S
Methods: Capability, Loss Function, Maximum Information
Location: what happened to the average wait?¶
The Xbar chart tracks the mean wait per subgroup over time.
result = study.execute(chart='Xbar', companion=True) # Xbar plus its S companion
fig = result.plot(chart='Xbar', show_zones=True, highlight_signals=True, show_stats=True)
fig.show()Two things jump out:
At week 8 the average wait drops to a new, lower level and stays there — the new espresso machine made the café faster. Not every signal is bad news; this is a process improvement, and the chart proves it wasn’t just luck.
Around week 12 there’s a run of high points for about a week, then it settles — the POS rollout: staff learning the new system slowed things down temporarily.
sig = result.detect_signals(chart='Xbar')
print(f'Xbar signals: {sig.count}')
sig.violations.head(10) if sig.has_signals else 'no signals'Xbar signals: 36
Dispersion: the change the average hides¶
Now the S chart — the within-subgroup spread over time. This is the one most people skip, and it’s where the most important lesson lives.
fig = result.plot(chart='S', show_zones=True, highlight_signals=True)
fig.show()
sig_s = result.detect_signals(chart='S')
print(f'S-chart signals: {sig_s.count}')S-chart signals: 3
From week 14 the spread balloons — while the average (the Xbar chart) barely moves. That’s the new-hire effect: consistency, not speed, degraded. A location chart alone would have declared the process fine. Averages hide variation; you need the dispersion chart too.
# Same story in the numbers: mean holds, within-subgroup SD roughly doubles.
cell = pb.data.groupby(['daypart', 'date', 'week'])['wait_sec'].agg(['mean', 'std'])
settled = cell.query('8 <= week <= 13 and week != 12')
newhire = cell.query('week >= 14')
print(f'settled (wk8-13): mean={settled["mean"].mean():5.1f}s within-cell SD={settled["std"].mean():4.1f}s')
print(f'new hires(wk14-16): mean={newhire["mean"].mean():5.1f}s within-cell SD={newhire["std"].mean():4.1f}s')settled (wk8-13): mean=200.5s within-cell SD=13.4s
new hires(wk14-16): mean=200.0s within-cell SD=24.5s
The daypart effect¶
Peak hours are genuinely slower than Off-peak — a real main effect the study separates from the noise.
print('Mean wait by daypart:')
print(pb.data.groupby('daypart')['wait_sec'].mean().round(1))Mean wait by daypart:
daypart
Off-peak 203.6
Peak 238.0
Name: wait_sec, dtype: float64
Calibration: hold the process to a known standard¶
Once the café settled into its post-machine rhythm (weeks 8–13, before the new hires), we
can freeze that as the expected standard and monitor everything against it — instead
of limits re-derived from the data. That’s what a Calibration does.
settled_vals = pb.data.query('8 <= week <= 13 and week != 12')['wait_sec']
post = Calibration(
label='post-machine normal',
mean=float(round(settled_vals.mean(), 1)),
sigma=float(round(settled_vals.std(ddof=1), 1)),
)
print(post)Calibration(label='post-machine normal', mean=200.5, sigma=20.4)
# Monitor the individual readings against the frozen standard.
fig = study.execute(chart='X', by=[], calibration=post).plot(chart='X', highlight_signals=True)
fig.show()Held to the standard the café earned in its settled weeks, the new-hire weeks stand out immediately — exactly the early warning a manager wants, before customers start complaining. Calibration turns “what do the data say?” into “are we still meeting the standard we set?”
Summary¶
| Event | Week | Signal | Which chart caught it |
|---|---|---|---|
| New espresso machine | 8 | sustained downward shift (improvement) | Xbar |
| New POS system | 12 | ~1-week run, then settles | Xbar |
| New baristas | 14–16 | spread doubles, mean flat | S |
One dataset, the whole toolkit: SDS 1 detection, location and dispersion charts, run rules, a real factor effect, and calibration to a standard. The espresso machine shows a good change; the new hires show why the S chart is not optional.