SPC toolkit, a small Python package
XmR and p-chart limits, the four special-cause rules and funnel plot limits with overdispersion adjustment, packaged as typed, pure functions with 471 test cases and installable from a zip.
Synthetic data Every record here is generated. No real patient or organisational data is used.
- Test cases, from 51 test functions
- 471
- Public functions, all pure and typed
- 10
- Lines of code, not counting docstrings, comments or blank lines
- 301
spckit is a small Python package for the arithmetic behind statistical process control (SPC) charts and funnel plots: XmR and p-chart limits, the four special-cause rules from NHS England’s Making Data Count, and funnel plot limits with an overdispersion adjustment. It is the kind of library an analytics team shares so that every chart on every dashboard is built the same way.
It implements the methods in two articles on this site, SPC charts for operational metrics and funnel plots for comparing units of very different size, with the same constants and rules. Run against the scripts behind both articles, it reproduces their limits, flagged weeks, first signals, φ, τ and flagged practices exactly.
In short: 10 public functions in 301 lines of code, NumPy and pandas as the only dependencies, 471 test cases, and a wheel that builds and installs cleanly on Python 3.12. The three examples below use synthetic data, and every number comes from examples/run.py.
Why a package, not notebook cells
An XmR chart is a few lines of NumPy, so it is tempting to paste them into each notebook that needs one. The trouble starts with the second copy. One copy calculates the limits from every point instead of a baseline, another uses the standard deviation instead of the moving range, a third runs the shift rule with eight points instead of seven. Each is a plausible choice, and together they mean two dashboards can disagree about whether a service changed.
A package gives the calculation one home, with one set of tests, a changelog and a version number. A bug is fixed once and picked up everywhere with an upgrade. Packaging analysis code as a library covers when that trade is worth making; this project is the worked example.
The API
Ten functions:
| Function | Returns |
|---|---|
xmr_limits(values) |
XmRLimits: centre, upper, lower, mean moving range, moving range limit |
moving_ranges(values) |
array of absolute differences between consecutive values |
pooled_proportion(numerators, denominators) |
total over total, the centre line for a p-chart or funnel |
p_chart_limits(centre, denominators) |
PChartLimits: one upper and lower limit per period |
special_causes(values, limits) |
DataFrame with one True/False column per rule, keeping the input’s index |
first_signals(values, limits, start) |
the first position where each rule would have fired live |
funnel_limits(p0, denominators, z, phi, tau2) |
FunnelLimits: lower and upper limit per unit |
z_scores(numerators, denominators, p0) |
each unit’s standardised distance from p0 |
overdispersion(numerators, denominators, p0) |
Overdispersion: φ, τ², the chi-squared check |
between_unit_variance(denominators, p0, phi) |
τ² for additive limits |
A typical XmR use reads the data, freezes limits from a baseline, and flags the rest:
import pandas as pd
from spckit import first_signals, special_causes, xmr_limits
weekly = pd.read_csv("assessment-times-xmr.csv", index_col="week", parse_dates=True)
minutes = weekly["minutes"]
limits = xmr_limits(minutes.iloc[:20]) # baseline: the first 20 weeks
flags = special_causes(minutes, limits) # one boolean column per rule, same index
first = first_signals(minutes, limits, start=43) # 43 is w/c 3 November 2025
print(limits, first, sep="\n")
XmRLimits(centre=16.765, upper=20.335, lower=13.195, mr_bar=1.3421052631578947, mr_upper=4.384657894736842)
{'outside_limits': 45, 'shift': 47, 'trend': 45, 'two_of_three': 45}
special_causes flags every point in a pattern, so all seven points of a shift are marked. first_signals answers the manager’s question: using only the data we had each week, when would we have known?
Example 1: an XmR chart
The data is 78 weeks of mean minutes from arrival to initial assessment in a fictional emergency department. I planted a stable process around 17 minutes, one disrupted week (an IT outage, w/c 14 July 2025) and a rapid-assessment process from 3 November 2025 that lowers the mean to 14.5 minutes.
Limits from the first 20 weeks: a mean of 16.8 minutes and process limits of 13.2 to 20.3. Against them, the outage week (23.4 minutes) is outside the limits, and so are its two moving ranges, 6.2 and 6.7 against a moving range limit of 4.38. After the change, three rules fire live in week 3 (w/c 17 November): a point outside the limits, a trend of six falling points, and two of three in the outer third. The run of seven below the mean follows in week 5 (w/c 1 December).
Data table
| Week commencing | Minutes to assessment | Mean | Upper limit | Lower limit | Special cause |
|---|---|---|---|---|---|
| 2025-01-06 | 17.3 | 16.8 | 20.3 | 13.2 | |
| 2025-01-13 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-01-20 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-01-27 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-02-03 | 18.0 | 16.8 | 20.3 | 13.2 | |
| 2025-02-10 | 15.5 | 16.8 | 20.3 | 13.2 | |
| 2025-02-17 | 15.1 | 16.8 | 20.3 | 13.2 | |
| 2025-02-24 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-03-03 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-03-10 | 16.4 | 16.8 | 20.3 | 13.2 | |
| 2025-03-17 | 17.7 | 16.8 | 20.3 | 13.2 | |
| 2025-03-24 | 15.1 | 16.8 | 20.3 | 13.2 | |
| 2025-03-31 | 16.9 | 16.8 | 20.3 | 13.2 | |
| 2025-04-07 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-04-14 | 18.3 | 16.8 | 20.3 | 13.2 | |
| 2025-04-21 | 18.9 | 16.8 | 20.3 | 13.2 | |
| 2025-04-28 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-05-05 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-05-12 | 14.5 | 16.8 | 20.3 | 13.2 | |
| 2025-05-19 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-05-26 | 18.0 | 16.8 | 20.3 | 13.2 | |
| 2025-06-02 | 17.0 | 16.8 | 20.3 | 13.2 | |
| 2025-06-09 | 19.5 | 16.8 | 20.3 | 13.2 | |
| 2025-06-16 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-06-23 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-06-30 | 17.5 | 16.8 | 20.3 | 13.2 | |
| 2025-07-07 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-07-14 | 23.4 | 16.8 | 20.3 | 13.2 | 23.4 |
| 2025-07-21 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-07-28 | 16.8 | 16.8 | 20.3 | 13.2 | |
| 2025-08-04 | 17.9 | 16.8 | 20.3 | 13.2 | |
| 2025-08-11 | 15.6 | 16.8 | 20.3 | 13.2 | |
| 2025-08-18 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-08-25 | 17.9 | 16.8 | 20.3 | 13.2 | |
| 2025-09-01 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-09-08 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-09-15 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-09-22 | 16.0 | 16.8 | 20.3 | 13.2 | |
| 2025-09-29 | 18.5 | 16.8 | 20.3 | 13.2 | |
| 2025-10-06 | 15.6 | 16.8 | 20.3 | 13.2 | |
| 2025-10-13 | 17.6 | 16.8 | 20.3 | 13.2 | 17.6 |
| 2025-10-20 | 16.6 | 16.8 | 20.3 | 13.2 | 16.6 |
| 2025-10-27 | 16.5 | 16.8 | 20.3 | 13.2 | 16.5 |
| 2025-11-03 | 15.5 | 16.8 | 20.3 | 13.2 | 15.5 |
| 2025-11-10 | 13.9 | 16.8 | 20.3 | 13.2 | 13.9 |
| 2025-11-17 | 12.3 | 16.8 | 20.3 | 13.2 | 12.3 |
| 2025-11-24 | 12.7 | 16.8 | 20.3 | 13.2 | 12.7 |
| 2025-12-01 | 14.9 | 16.8 | 20.3 | 13.2 | 14.9 |
| 2025-12-08 | 13.2 | 16.8 | 20.3 | 13.2 | 13.2 |
| 2025-12-15 | 14.4 | 16.8 | 20.3 | 13.2 | 14.4 |
| 2025-12-22 | 15.6 | 16.8 | 20.3 | 13.2 | 15.6 |
| 2025-12-29 | 15.2 | 16.8 | 20.3 | 13.2 | 15.2 |
| 2026-01-05 | 14.9 | 16.8 | 20.3 | 13.2 | 14.9 |
| 2026-01-12 | 15.1 | 16.8 | 20.3 | 13.2 | 15.1 |
| 2026-01-19 | 13.4 | 16.8 | 20.3 | 13.2 | 13.4 |
| 2026-01-26 | 13.5 | 16.8 | 20.3 | 13.2 | 13.5 |
| 2026-02-02 | 14.2 | 16.8 | 20.3 | 13.2 | 14.2 |
| 2026-02-09 | 13.4 | 16.8 | 20.3 | 13.2 | 13.4 |
| 2026-02-16 | 14.4 | 16.8 | 20.3 | 13.2 | 14.4 |
| 2026-02-23 | 12.6 | 16.8 | 20.3 | 13.2 | 12.6 |
| 2026-03-02 | 14.4 | 16.8 | 20.3 | 13.2 | 14.4 |
| 2026-03-09 | 15.1 | 16.8 | 20.3 | 13.2 | 15.1 |
| 2026-03-16 | 14.4 | 16.8 | 20.3 | 13.2 | 14.4 |
| 2026-03-23 | 16.2 | 16.8 | 20.3 | 13.2 | 16.2 |
| 2026-03-30 | 15.1 | 16.8 | 20.3 | 13.2 | 15.1 |
| 2026-04-06 | 13.2 | 16.8 | 20.3 | 13.2 | 13.2 |
| 2026-04-13 | 14.5 | 16.8 | 20.3 | 13.2 | 14.5 |
| 2026-04-20 | 13.9 | 16.8 | 20.3 | 13.2 | 13.9 |
| 2026-04-27 | 13.8 | 16.8 | 20.3 | 13.2 | 13.8 |
| 2026-05-04 | 14.6 | 16.8 | 20.3 | 13.2 | 14.6 |
| 2026-05-11 | 13.8 | 16.8 | 20.3 | 13.2 | 13.8 |
| 2026-05-18 | 14.2 | 16.8 | 20.3 | 13.2 | 14.2 |
| 2026-05-25 | 13.0 | 16.8 | 20.3 | 13.2 | 13.0 |
| 2026-06-01 | 13.7 | 16.8 | 20.3 | 13.2 | 13.7 |
| 2026-06-08 | 15.0 | 16.8 | 20.3 | 13.2 | 15.0 |
| 2026-06-15 | 15.4 | 16.8 | 20.3 | 13.2 | 15.4 |
| 2026-06-22 | 13.7 | 16.8 | 20.3 | 13.2 | 13.7 |
| 2026-06-29 | 14.3 | 16.8 | 20.3 | 13.2 | 14.3 |
| Week commencing | Minutes to assessment | Mean | Upper limit | Lower limit | Special cause |
|---|---|---|---|---|---|
| 2025-01-06 | 17.3 | 16.8 | 20.3 | 13.2 | |
| 2025-01-13 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-01-20 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-01-27 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-02-03 | 18.0 | 16.8 | 20.3 | 13.2 | |
| 2025-02-10 | 15.5 | 16.8 | 20.3 | 13.2 | |
| 2025-02-17 | 15.1 | 16.8 | 20.3 | 13.2 | |
| 2025-02-24 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-03-03 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-03-10 | 16.4 | 16.8 | 20.3 | 13.2 | |
| 2025-03-17 | 17.7 | 16.8 | 20.3 | 13.2 | |
| 2025-03-24 | 15.1 | 16.8 | 20.3 | 13.2 | |
| 2025-03-31 | 16.9 | 16.8 | 20.3 | 13.2 | |
| 2025-04-07 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-04-14 | 18.3 | 16.8 | 20.3 | 13.2 | |
| 2025-04-21 | 18.9 | 16.8 | 20.3 | 13.2 | |
| 2025-04-28 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-05-05 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-05-12 | 14.5 | 16.8 | 20.3 | 13.2 | |
| 2025-05-19 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-05-26 | 18.0 | 16.8 | 20.3 | 13.2 | |
| 2025-06-02 | 17.0 | 16.8 | 20.3 | 13.2 | |
| 2025-06-09 | 19.5 | 16.8 | 20.3 | 13.2 | |
| 2025-06-16 | 16.1 | 16.8 | 20.3 | 13.2 | |
| 2025-06-23 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-06-30 | 17.5 | 16.8 | 20.3 | 13.2 | |
| 2025-07-07 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-07-14 | 23.4 | 16.8 | 20.3 | 13.2 | 23.4 |
| 2025-07-21 | 16.7 | 16.8 | 20.3 | 13.2 | |
| 2025-07-28 | 16.8 | 16.8 | 20.3 | 13.2 | |
| 2025-08-04 | 17.9 | 16.8 | 20.3 | 13.2 | |
| 2025-08-11 | 15.6 | 16.8 | 20.3 | 13.2 | |
| 2025-08-18 | 17.2 | 16.8 | 20.3 | 13.2 | |
| 2025-08-25 | 17.9 | 16.8 | 20.3 | 13.2 | |
| 2025-09-01 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-09-08 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-09-15 | 17.8 | 16.8 | 20.3 | 13.2 | |
| 2025-09-22 | 16.0 | 16.8 | 20.3 | 13.2 | |
| 2025-09-29 | 18.5 | 16.8 | 20.3 | 13.2 | |
| 2025-10-06 | 15.6 | 16.8 | 20.3 | 13.2 | |
| 2025-10-13 | 17.6 | 16.8 | 20.3 | 13.2 | |
| 2025-10-20 | 16.6 | 16.8 | 20.3 | 13.2 | |
| 2025-10-27 | 16.5 | 16.8 | 20.3 | 13.2 | |
| 2025-11-03 | 15.5 | 14.2 | 17.0 | 11.3 | |
| 2025-11-10 | 13.9 | 14.2 | 17.0 | 11.3 | |
| 2025-11-17 | 12.3 | 14.2 | 17.0 | 11.3 | |
| 2025-11-24 | 12.7 | 14.2 | 17.0 | 11.3 | |
| 2025-12-01 | 14.9 | 14.2 | 17.0 | 11.3 | |
| 2025-12-08 | 13.2 | 14.2 | 17.0 | 11.3 | |
| 2025-12-15 | 14.4 | 14.2 | 17.0 | 11.3 | |
| 2025-12-22 | 15.6 | 14.2 | 17.0 | 11.3 | |
| 2025-12-29 | 15.2 | 14.2 | 17.0 | 11.3 | |
| 2026-01-05 | 14.9 | 14.2 | 17.0 | 11.3 | |
| 2026-01-12 | 15.1 | 14.2 | 17.0 | 11.3 | |
| 2026-01-19 | 13.4 | 14.2 | 17.0 | 11.3 | |
| 2026-01-26 | 13.5 | 14.2 | 17.0 | 11.3 | |
| 2026-02-02 | 14.2 | 14.2 | 17.0 | 11.3 | |
| 2026-02-09 | 13.4 | 14.2 | 17.0 | 11.3 | |
| 2026-02-16 | 14.4 | 14.2 | 17.0 | 11.3 | |
| 2026-02-23 | 12.6 | 14.2 | 17.0 | 11.3 | |
| 2026-03-02 | 14.4 | 14.2 | 17.0 | 11.3 | |
| 2026-03-09 | 15.1 | 14.2 | 17.0 | 11.3 | |
| 2026-03-16 | 14.4 | 14.2 | 17.0 | 11.3 | |
| 2026-03-23 | 16.2 | 14.2 | 17.0 | 11.3 | |
| 2026-03-30 | 15.1 | 14.2 | 17.0 | 11.3 | |
| 2026-04-06 | 13.2 | 14.2 | 17.0 | 11.3 | |
| 2026-04-13 | 14.5 | 14.2 | 17.0 | 11.3 | |
| 2026-04-20 | 13.9 | 14.2 | 17.0 | 11.3 | |
| 2026-04-27 | 13.8 | 14.2 | 17.0 | 11.3 | |
| 2026-05-04 | 14.6 | 14.2 | 17.0 | 11.3 | |
| 2026-05-11 | 13.8 | 14.2 | 17.0 | 11.3 | |
| 2026-05-18 | 14.2 | 14.2 | 17.0 | 11.3 | |
| 2026-05-25 | 13.0 | 14.2 | 17.0 | 11.3 | |
| 2026-06-01 | 13.7 | 14.2 | 17.0 | 11.3 | |
| 2026-06-08 | 15.0 | 14.2 | 17.0 | 11.3 | |
| 2026-06-15 | 15.4 | 14.2 | 17.0 | 11.3 | |
| 2026-06-22 | 13.7 | 14.2 | 17.0 | 11.3 | |
| 2026-06-29 | 14.3 | 14.2 | 17.0 | 11.3 |
Synthetic data. Source: projects/spc-toolkit/examples/run.py.
The retrospective flags start earlier: the shift on 20 October, two weeks before the change, because those weeks happened to sit just below the mean and belong to the same run, and the trend on 13 October. That is why the package keeps flags and live signals apart.
Switch to the recalculated limits. The new process has a mean of 14.2 minutes and limits of 11.3 to 17.0, no week after the change breaks a rule against them, and the outage is the only special cause left. Each phase is judged separately, since a run shouldn’t carry across a recalculation:
phased = pd.concat([special_causes(minutes[~change], base), special_causes(after, new)])
Example 2: a p-chart when volumes change
Here the data is 30-day readmissions by week of discharge for a surgical specialty. Discharges run from 53 to 177 a week: about 70 a week at first (fewer in holiday weeks), then 148 a week on average once a second ward opens on 1 September 2025. I planted a true rate of 9.5%, and three bad weeks from 13 October at 17%.
The p-chart’s centre line is the pooled rate over the first 26 weeks, 8.96%, and each week gets limits for its own denominator: 0% to 19.2% at 70 discharges, 2.0% to 16.0% at 150. Switch to the XmR chart on the same rates to see the difference.
Data table
| Week commencing | Readmission rate | Centre line (limits shaded) | Centre line (limits shaded) (low) | Centre line (limits shaded) (high) | Special cause |
|---|---|---|---|---|---|
| 2025-01-06 | 5.9% | 9.0% | 0.0% | 19.3% | |
| 2025-01-13 | 14.5% | 9.0% | 0.0% | 19.3% | |
| 2025-01-20 | 7.7% | 9.0% | 0.0% | 17.9% | |
| 2025-01-27 | 13.2% | 9.0% | 0.0% | 19.3% | |
| 2025-02-03 | 11.8% | 9.0% | 0.0% | 18.8% | |
| 2025-02-10 | 11.3% | 9.0% | 0.0% | 19.1% | |
| 2025-02-17 | 8.5% | 9.0% | 0.0% | 18.4% | |
| 2025-02-24 | 6.0% | 9.0% | 0.0% | 19.4% | |
| 2025-03-03 | 12.9% | 9.0% | 0.0% | 19.2% | |
| 2025-03-10 | 0.0% | 9.0% | 0.0% | 20.7% | 0.0% |
| 2025-03-17 | 7.4% | 9.0% | 0.0% | 18.5% | |
| 2025-03-24 | 1.6% | 9.0% | 0.0% | 19.9% | 1.6% |
| 2025-03-31 | 12.7% | 9.0% | 0.0% | 18.6% | |
| 2025-04-07 | 13.8% | 9.0% | 0.0% | 20.2% | |
| 2025-04-14 | 6.8% | 9.0% | 0.0% | 20.1% | |
| 2025-04-21 | 6.6% | 9.0% | 0.0% | 19.9% | |
| 2025-04-28 | 11.1% | 9.0% | 0.0% | 19.1% | |
| 2025-05-05 | 8.3% | 9.0% | 0.0% | 20.0% | |
| 2025-05-12 | 3.7% | 9.0% | 0.0% | 18.5% | |
| 2025-05-19 | 5.8% | 9.0% | 0.0% | 19.3% | |
| 2025-05-26 | 11.8% | 9.0% | 0.0% | 19.3% | |
| 2025-06-02 | 9.3% | 9.0% | 0.0% | 18.2% | |
| 2025-06-09 | 8.3% | 9.0% | 0.0% | 20.0% | |
| 2025-06-16 | 13.6% | 9.0% | 0.0% | 19.5% | |
| 2025-06-23 | 12.5% | 9.0% | 0.0% | 19.1% | |
| 2025-06-30 | 5.6% | 9.0% | 0.0% | 19.1% | |
| 2025-07-07 | 9.1% | 9.0% | 0.0% | 18.7% | |
| 2025-07-14 | 21.9% | 9.0% | 0.0% | 19.7% | 21.9% |
| 2025-07-21 | 6.2% | 9.0% | 0.0% | 19.7% | |
| 2025-07-28 | 9.5% | 9.0% | 0.0% | 18.9% | |
| 2025-08-04 | 13.0% | 9.0% | 0.0% | 19.3% | |
| 2025-08-11 | 7.1% | 9.0% | 0.0% | 19.2% | |
| 2025-08-18 | 12.5% | 9.0% | 0.0% | 19.7% | |
| 2025-08-25 | 12.5% | 9.0% | 0.0% | 19.1% | |
| 2025-09-01 | 10.1% | 9.0% | 1.7% | 16.2% | |
| 2025-09-08 | 13.5% | 9.0% | 2.1% | 15.8% | |
| 2025-09-15 | 8.1% | 9.0% | 1.9% | 16.0% | |
| 2025-09-22 | 10.9% | 9.0% | 1.9% | 16.0% | |
| 2025-09-29 | 11.0% | 9.0% | 2.3% | 15.7% | |
| 2025-10-06 | 11.6% | 9.0% | 1.9% | 16.0% | |
| 2025-10-13 | 22.1% | 9.0% | 2.2% | 15.7% | 22.1% |
| 2025-10-20 | 16.7% | 9.0% | 1.8% | 16.1% | 16.7% |
| 2025-10-27 | 14.6% | 9.0% | 2.0% | 15.9% | 14.6% |
| 2025-11-03 | 7.0% | 9.0% | 2.1% | 15.8% | |
| 2025-11-10 | 7.1% | 9.0% | 2.1% | 15.9% | |
| 2025-11-17 | 9.2% | 9.0% | 2.2% | 15.7% | |
| 2025-11-24 | 10.4% | 9.0% | 2.1% | 15.9% | |
| 2025-12-01 | 12.7% | 9.0% | 2.3% | 15.6% | |
| 2025-12-08 | 7.2% | 9.0% | 2.0% | 15.9% | |
| 2025-12-15 | 9.5% | 9.0% | 1.9% | 16.0% | |
| 2025-12-22 | 13.8% | 9.0% | 0.0% | 20.2% | |
| 2025-12-29 | 7.0% | 9.0% | 0.0% | 19.1% | |
| 2026-01-05 | 8.7% | 9.0% | 1.7% | 16.2% | |
| 2026-01-12 | 7.3% | 9.0% | 1.6% | 16.3% | |
| 2026-01-19 | 10.1% | 9.0% | 2.2% | 15.8% | |
| 2026-01-26 | 7.3% | 9.0% | 1.3% | 16.7% | |
| 2026-02-02 | 7.1% | 9.0% | 2.1% | 15.8% | |
| 2026-02-09 | 11.5% | 9.0% | 2.3% | 15.6% | |
| 2026-02-16 | 5.8% | 9.0% | 2.1% | 15.8% | |
| 2026-02-23 | 10.4% | 9.0% | 1.8% | 16.1% | |
| 2026-03-02 | 9.6% | 9.0% | 2.5% | 15.4% | |
| 2026-03-09 | 11.2% | 9.0% | 2.0% | 15.9% | |
| 2026-03-16 | 11.6% | 9.0% | 1.9% | 16.1% | |
| 2026-03-23 | 13.0% | 9.0% | 2.2% | 15.7% | |
| 2026-03-30 | 6.1% | 9.0% | 0.4% | 17.6% | |
| 2026-04-06 | 9.9% | 9.0% | 2.2% | 15.7% | |
| 2026-04-13 | 6.8% | 9.0% | 2.2% | 15.7% | |
| 2026-04-20 | 10.7% | 9.0% | 2.0% | 16.0% | |
| 2026-04-27 | 6.8% | 9.0% | 1.9% | 16.0% | |
| 2026-05-04 | 6.1% | 9.0% | 1.9% | 16.0% | |
| 2026-05-11 | 8.1% | 9.0% | 2.4% | 15.5% | |
| 2026-05-18 | 7.5% | 9.0% | 1.9% | 16.1% | |
| 2026-05-25 | 10.4% | 9.0% | 2.1% | 15.9% | |
| 2026-06-01 | 3.4% | 9.0% | 1.9% | 16.1% | |
| 2026-06-08 | 9.9% | 9.0% | 2.0% | 15.9% | |
| 2026-06-15 | 12.5% | 9.0% | 2.4% | 15.6% | |
| 2026-06-22 | 16.0% | 9.0% | 2.2% | 15.7% | 16.0% |
| 2026-06-29 | 9.9% | 9.0% | 2.0% | 15.9% |
| Week commencing | Readmission rate | Centre line (limits shaded) | Centre line (limits shaded) (low) | Centre line (limits shaded) (high) | Special cause |
|---|---|---|---|---|---|
| 2025-01-06 | 5.9% | 8.9% | -3.6% | 21.4% | |
| 2025-01-13 | 14.5% | 8.9% | -3.6% | 21.4% | |
| 2025-01-20 | 7.7% | 8.9% | -3.6% | 21.4% | |
| 2025-01-27 | 13.2% | 8.9% | -3.6% | 21.4% | |
| 2025-02-03 | 11.8% | 8.9% | -3.6% | 21.4% | |
| 2025-02-10 | 11.3% | 8.9% | -3.6% | 21.4% | |
| 2025-02-17 | 8.5% | 8.9% | -3.6% | 21.4% | |
| 2025-02-24 | 6.0% | 8.9% | -3.6% | 21.4% | |
| 2025-03-03 | 12.9% | 8.9% | -3.6% | 21.4% | |
| 2025-03-10 | 0.0% | 8.9% | -3.6% | 21.4% | |
| 2025-03-17 | 7.4% | 8.9% | -3.6% | 21.4% | |
| 2025-03-24 | 1.6% | 8.9% | -3.6% | 21.4% | |
| 2025-03-31 | 12.7% | 8.9% | -3.6% | 21.4% | |
| 2025-04-07 | 13.8% | 8.9% | -3.6% | 21.4% | |
| 2025-04-14 | 6.8% | 8.9% | -3.6% | 21.4% | |
| 2025-04-21 | 6.6% | 8.9% | -3.6% | 21.4% | |
| 2025-04-28 | 11.1% | 8.9% | -3.6% | 21.4% | |
| 2025-05-05 | 8.3% | 8.9% | -3.6% | 21.4% | |
| 2025-05-12 | 3.7% | 8.9% | -3.6% | 21.4% | |
| 2025-05-19 | 5.8% | 8.9% | -3.6% | 21.4% | |
| 2025-05-26 | 11.8% | 8.9% | -3.6% | 21.4% | |
| 2025-06-02 | 9.3% | 8.9% | -3.6% | 21.4% | |
| 2025-06-09 | 8.3% | 8.9% | -3.6% | 21.4% | |
| 2025-06-16 | 13.6% | 8.9% | -3.6% | 21.4% | |
| 2025-06-23 | 12.5% | 8.9% | -3.6% | 21.4% | |
| 2025-06-30 | 5.6% | 8.9% | -3.6% | 21.4% | |
| 2025-07-07 | 9.1% | 8.9% | -3.6% | 21.4% | |
| 2025-07-14 | 21.9% | 8.9% | -3.6% | 21.4% | 21.9% |
| 2025-07-21 | 6.2% | 8.9% | -3.6% | 21.4% | |
| 2025-07-28 | 9.5% | 8.9% | -3.6% | 21.4% | |
| 2025-08-04 | 13.0% | 8.9% | -3.6% | 21.4% | |
| 2025-08-11 | 7.1% | 8.9% | -3.6% | 21.4% | |
| 2025-08-18 | 12.5% | 8.9% | -3.6% | 21.4% | |
| 2025-08-25 | 12.5% | 8.9% | -3.6% | 21.4% | |
| 2025-09-01 | 10.1% | 8.9% | -3.6% | 21.4% | |
| 2025-09-08 | 13.5% | 8.9% | -3.6% | 21.4% | |
| 2025-09-15 | 8.1% | 8.9% | -3.6% | 21.4% | |
| 2025-09-22 | 10.9% | 8.9% | -3.6% | 21.4% | |
| 2025-09-29 | 11.0% | 8.9% | -3.6% | 21.4% | |
| 2025-10-06 | 11.6% | 8.9% | -3.6% | 21.4% | |
| 2025-10-13 | 22.1% | 8.9% | -3.6% | 21.4% | 22.1% |
| 2025-10-20 | 16.7% | 8.9% | -3.6% | 21.4% | |
| 2025-10-27 | 14.6% | 8.9% | -3.6% | 21.4% | |
| 2025-11-03 | 7.0% | 8.9% | -3.6% | 21.4% | |
| 2025-11-10 | 7.1% | 8.9% | -3.6% | 21.4% | |
| 2025-11-17 | 9.2% | 8.9% | -3.6% | 21.4% | |
| 2025-11-24 | 10.4% | 8.9% | -3.6% | 21.4% | |
| 2025-12-01 | 12.7% | 8.9% | -3.6% | 21.4% | |
| 2025-12-08 | 7.2% | 8.9% | -3.6% | 21.4% | |
| 2025-12-15 | 9.5% | 8.9% | -3.6% | 21.4% | |
| 2025-12-22 | 13.8% | 8.9% | -3.6% | 21.4% | |
| 2025-12-29 | 7.0% | 8.9% | -3.6% | 21.4% | |
| 2026-01-05 | 8.7% | 8.9% | -3.6% | 21.4% | |
| 2026-01-12 | 7.3% | 8.9% | -3.6% | 21.4% | |
| 2026-01-19 | 10.1% | 8.9% | -3.6% | 21.4% | |
| 2026-01-26 | 7.3% | 8.9% | -3.6% | 21.4% | |
| 2026-02-02 | 7.1% | 8.9% | -3.6% | 21.4% | |
| 2026-02-09 | 11.5% | 8.9% | -3.6% | 21.4% | |
| 2026-02-16 | 5.8% | 8.9% | -3.6% | 21.4% | |
| 2026-02-23 | 10.4% | 8.9% | -3.6% | 21.4% | |
| 2026-03-02 | 9.6% | 8.9% | -3.6% | 21.4% | |
| 2026-03-09 | 11.2% | 8.9% | -3.6% | 21.4% | |
| 2026-03-16 | 11.6% | 8.9% | -3.6% | 21.4% | |
| 2026-03-23 | 13.0% | 8.9% | -3.6% | 21.4% | |
| 2026-03-30 | 6.1% | 8.9% | -3.6% | 21.4% | |
| 2026-04-06 | 9.9% | 8.9% | -3.6% | 21.4% | |
| 2026-04-13 | 6.8% | 8.9% | -3.6% | 21.4% | |
| 2026-04-20 | 10.7% | 8.9% | -3.6% | 21.4% | |
| 2026-04-27 | 6.8% | 8.9% | -3.6% | 21.4% | |
| 2026-05-04 | 6.1% | 8.9% | -3.6% | 21.4% | |
| 2026-05-11 | 8.1% | 8.9% | -3.6% | 21.4% | |
| 2026-05-18 | 7.5% | 8.9% | -3.6% | 21.4% | |
| 2026-05-25 | 10.4% | 8.9% | -3.6% | 21.4% | |
| 2026-06-01 | 3.4% | 8.9% | -3.6% | 21.4% | |
| 2026-06-08 | 9.9% | 8.9% | -3.6% | 21.4% | |
| 2026-06-15 | 12.5% | 8.9% | -3.6% | 21.4% | |
| 2026-06-22 | 16.0% | 8.9% | -3.6% | 21.4% | |
| 2026-06-29 | 9.9% | 8.9% | -3.6% | 21.4% |
Synthetic data. Source: projects/spc-toolkit/examples/run.py.
The p-chart flags all three planted weeks: two above the limits (22.1% and 16.7%) and all three under the two-of-three rule. The XmR chart, with limits from the smaller baseline weeks, flags only the first of them. Its lower limit is −3.6%, a rate that cannot happen, which is the tell that it is the wrong chart for this data.
The p-chart also flags weeks I didn’t plant: w/c 14 July 2025 (14 of 64, 21.9%, which the XmR chart flags too) and w/c 22 June 2026 (26 of 163, 16.0%) above the limits, and two low weeks in March 2025 under two-of-three. Some of that is chance, and some is the maths. With small denominators the binomial’s upper tail is longer than the normal approximation assumes: the exact chance of a point above the upper limit when nothing has changed is 0.33% at 70 discharges and 0.66% at 53, not the 0.13% the normal curve promises. The baseline rate also came in below the planted 9.5%, which makes high weeks look more unusual.
Example 3: a funnel plot with overdispersion
The last data set is a year of outpatient DNA (did not attend) rates for 45 clinics with 155 to 7,408 appointments each, 7.22% overall. I planted clinic-to-clinic variation, as case mix creates in real data, plus three clinics: 12 genuinely high (12%), 31 genuinely low (4.5%) and 40 genuinely high (13%) but small, at 180 appointments.
p0 = pooled_proportion(clinics["dna"], n)
od = overdispersion(clinics["dna"], n, p0=p0)
action = funnel_limits(p0, n, z=Z998, tau2=od.tau2)
The winsorised φ is 3.73, and k × φ = 167.8 against a chi-squared 95th percentile of 60.5 on 44 degrees of freedom, so the clinics vary far more than chance allows. τ is 0.94 percentage points. The chart switches between the three sets of limits.
Data table
| Appointments in the year | Clinics | Upper 99.8% | Upper 95% | Lower 95% | Lower 99.8% | Outside 99.8% |
|---|---|---|---|---|---|---|
| 155 | 13.6% | 13.6% | 11.3% | 3.1% | 0.8% | |
| 165 | 13.5% | 11.2% | 3.3% | 1.0% | ||
| 168 | 5.9% | |||||
| 177 | 7.3% | 13.2% | 11.0% | 3.4% | 1.2% | |
| 180 | 11.7% | |||||
| 186 | 3.8% | |||||
| 189 | 13.0% | 10.9% | 3.5% | 1.4% | ||
| 200 | 5.0% | |||||
| 201 | 12.9% | 10.8% | 3.6% | 1.6% | ||
| 215 | 12.7% | 10.7% | 3.8% | 1.8% | ||
| 230 | 6.1% | 12.5% | 10.6% | 3.9% | 1.9% | |
| 242 | 7.4% | |||||
| 245 | 12.3% | 10.5% | 4.0% | 2.1% | ||
| 247 | 10.1% | |||||
| 262 | 12.2% | 10.3% | 4.1% | 2.3% | ||
| 280 | 12.0% | 10.2% | 4.2% | 2.4% | ||
| 299 | 11.8% | 10.2% | 4.3% | 2.6% | ||
| 309 | 9.4% | |||||
| 318 | 8.2% | |||||
| 319 | 11.7% | 10.1% | 4.4% | 2.7% | ||
| 336 | 4.5% | |||||
| 340 | 11.6% | 10.0% | 4.5% | 2.9% | ||
| 363 | 11.4% | 9.9% | 4.6% | 3.0% | ||
| 388 | 11.3% | 9.8% | 4.6% | 3.2% | ||
| 414 | 11.2% | 9.7% | 4.7% | 3.3% | ||
| 442 | 11.0% | 9.6% | 4.8% | 3.4% | ||
| 461 | 8.0% | |||||
| 472 | 10.9% | 9.6% | 4.9% | 3.5% | ||
| 504 | 10.8% | 9.5% | 5.0% | 3.7% | ||
| 536 | 7.5% | |||||
| 538 | 10.7% | 9.4% | 5.0% | 3.8% | ||
| 575 | 10.5% | 9.3% | 5.1% | 3.9% | ||
| 586 | 7.5% | |||||
| 608 | 10.0% | |||||
| 614 | 10.4% | 9.3% | 5.2% | 4.0% | ||
| 655 | 10.3% | 9.2% | 5.2% | 4.1% | ||
| 661 | 7.0% | |||||
| 700 | 10.2% | 9.1% | 5.3% | 4.2% | ||
| 747 | 10.2% | 9.1% | 5.4% | 4.3% | ||
| 754 | 8.8% | |||||
| 781 | 8.3% | |||||
| 798 | 10.1% | 9.0% | 5.4% | 4.4% | ||
| 852 | 10.0% | 9.0% | 5.5% | 4.5% | ||
| 910 | 9.9% | 8.9% | 5.5% | 4.6% | ||
| 971 | 9.8% | 8.8% | 5.6% | 4.7% | ||
| 996 | 7.6% | |||||
| 1037 | 9.7% | 8.8% | 5.6% | 4.7% | ||
| 1096 | 10.4% | 10.4% | ||||
| 1107 | 9.6% | 8.7% | 5.7% | 4.8% | ||
| 1117 | 7.5% | |||||
| 1182 | 9.6% | 8.7% | 5.7% | 4.9% | ||
| 1198 | 9.6% | 9.6% | ||||
| 1262 | 9.5% | 8.6% | 5.8% | 5.0% | ||
| 1311 | 8.1% | |||||
| 1348 | 9.4% | 8.6% | 5.8% | 5.0% | ||
| 1439 | 9.3% | 8.6% | 5.9% | 5.1% | ||
| 1537 | 9.3% | 8.5% | 5.9% | 5.2% | ||
| 1641 | 9.2% | 8.5% | 6.0% | 5.2% | ||
| 1716 | 6.4% | |||||
| 1752 | 9.1% | 8.4% | 6.0% | 5.3% | ||
| 1871 | 9.1% | 8.4% | 6.0% | 5.4% | ||
| 1997 | 9.0% | 8.3% | 6.1% | 5.4% | ||
| 2008 | 6.3% | |||||
| 2109 | 6.3% | |||||
| 2132 | 8.9% | 8.3% | 6.1% | 5.5% | ||
| 2167 | 6.2% | |||||
| 2277 | 8.9% | 8.3% | 6.2% | 5.5% | ||
| 2316 | 8.1% | |||||
| 2431 | 8.8% | 8.2% | 6.2% | 5.6% | ||
| 2500 | 11.4% | 11.4% | ||||
| 2578 | 7.7% | |||||
| 2596 | 8.8% | 8.2% | 6.2% | 5.7% | ||
| 2772 | 8.7% | 8.2% | 6.3% | 5.7% | ||
| 2959 | 8.7% | 8.2% | 6.3% | 5.8% | ||
| 3006 | 4.7% | 4.7% | ||||
| 3019 | 8.1% | |||||
| 3160 | 8.6% | 8.1% | 6.3% | 5.8% | ||
| 3374 | 8.6% | 8.1% | 6.3% | 5.8% | ||
| 3602 | 8.6% | 8.1% | 6.4% | 5.9% | ||
| 3659 | 5.6% | 5.6% | ||||
| 3706 | 10.2% | 10.2% | ||||
| 3846 | 8.5% | 8.0% | 6.4% | 5.9% | ||
| 4000 | 4.6% | 4.6% | ||||
| 4075 | 7.9% | |||||
| 4107 | 8.5% | 8.0% | 6.4% | 6.0% | ||
| 4385 | 8.4% | 8.0% | 6.5% | 6.0% | ||
| 4682 | 8.4% | 8.0% | 6.5% | 6.0% | ||
| 4883 | 6.8% | |||||
| 4944 | 6.2% | |||||
| 4999 | 8.3% | 7.9% | 6.5% | 6.1% | ||
| 5142 | 7.0% | |||||
| 5338 | 8.3% | 7.9% | 6.5% | 6.1% | ||
| 5477 | 8.0% | |||||
| 5700 | 8.3% | 7.9% | 6.6% | 6.2% | ||
| 6044 | 8.7% | 8.7% | ||||
| 6086 | 8.2% | 7.9% | 6.6% | 6.2% | ||
| 6498 | 8.2% | 7.8% | 6.6% | 6.2% | ||
| 6938 | 8.2% | 7.8% | 6.6% | 6.3% | ||
| 7033 | 6.0% | 6.0% | ||||
| 7136 | 6.8% | |||||
| 7408 | 6.4% | 8.2% | 7.8% | 6.6% | 6.3% |
| Appointments in the year | Clinics | Upper 99.8% | Upper 95% | Lower 95% | Lower 99.8% | Outside 99.8% |
|---|---|---|---|---|---|---|
| 155 | 13.6% | 19.6% | 15.1% | 0.0% | 0.0% | |
| 165 | 19.2% | 14.8% | 0.0% | 0.0% | ||
| 168 | 5.9% | |||||
| 177 | 7.3% | 18.8% | 14.6% | 0.0% | 0.0% | |
| 180 | 11.7% | |||||
| 186 | 3.8% | |||||
| 189 | 18.4% | 14.3% | 0.1% | 0.0% | ||
| 200 | 5.0% | |||||
| 201 | 18.1% | 14.1% | 0.3% | 0.0% | ||
| 215 | 17.8% | 13.9% | 0.5% | 0.0% | ||
| 230 | 6.1% | 17.4% | 13.7% | 0.8% | 0.0% | |
| 242 | 7.4% | |||||
| 245 | 17.1% | 13.5% | 1.0% | 0.0% | ||
| 247 | 10.1% | |||||
| 262 | 16.8% | 13.3% | 1.2% | 0.0% | ||
| 280 | 16.4% | 13.1% | 1.4% | 0.0% | ||
| 299 | 16.2% | 12.9% | 1.6% | 0.0% | ||
| 309 | 9.4% | |||||
| 318 | 8.2% | |||||
| 319 | 15.9% | 12.7% | 1.7% | 0.0% | ||
| 336 | 4.5% | |||||
| 340 | 15.6% | 12.5% | 1.9% | 0.0% | ||
| 363 | 15.3% | 12.4% | 2.1% | 0.0% | ||
| 388 | 15.1% | 12.2% | 2.2% | 0.0% | ||
| 414 | 14.8% | 12.0% | 2.4% | 0.0% | ||
| 442 | 14.6% | 11.9% | 2.6% | 0.0% | ||
| 461 | 8.0% | |||||
| 472 | 14.3% | 11.7% | 2.7% | 0.1% | ||
| 504 | 14.1% | 11.6% | 2.9% | 0.3% | ||
| 536 | 7.5% | |||||
| 538 | 13.9% | 11.4% | 3.0% | 0.6% | ||
| 575 | 13.7% | 11.3% | 3.1% | 0.8% | ||
| 586 | 7.5% | |||||
| 608 | 10.0% | |||||
| 614 | 13.5% | 11.2% | 3.3% | 1.0% | ||
| 655 | 13.2% | 11.1% | 3.4% | 1.2% | ||
| 661 | 7.0% | |||||
| 700 | 13.1% | 10.9% | 3.5% | 1.4% | ||
| 747 | 12.9% | 10.8% | 3.6% | 1.6% | ||
| 754 | 8.8% | |||||
| 781 | 8.3% | |||||
| 798 | 12.7% | 10.7% | 3.8% | 1.8% | ||
| 852 | 12.5% | 10.6% | 3.9% | 1.9% | ||
| 910 | 12.3% | 10.5% | 4.0% | 2.1% | ||
| 971 | 12.2% | 10.4% | 4.1% | 2.3% | ||
| 996 | 7.6% | |||||
| 1037 | 12.0% | 10.3% | 4.2% | 2.4% | ||
| 1096 | 10.4% | |||||
| 1107 | 11.9% | 10.2% | 4.3% | 2.6% | ||
| 1117 | 7.5% | |||||
| 1182 | 11.7% | 10.1% | 4.4% | 2.7% | ||
| 1198 | 9.6% | |||||
| 1262 | 11.6% | 10.0% | 4.5% | 2.9% | ||
| 1311 | 8.1% | |||||
| 1348 | 11.4% | 9.9% | 4.5% | 3.0% | ||
| 1439 | 11.3% | 9.8% | 4.6% | 3.1% | ||
| 1537 | 11.2% | 9.7% | 4.7% | 3.3% | ||
| 1641 | 11.0% | 9.6% | 4.8% | 3.4% | ||
| 1716 | 6.4% | |||||
| 1752 | 10.9% | 9.6% | 4.9% | 3.5% | ||
| 1871 | 10.8% | 9.5% | 5.0% | 3.6% | ||
| 1997 | 10.7% | 9.4% | 5.0% | 3.8% | ||
| 2008 | 6.3% | |||||
| 2109 | 6.3% | |||||
| 2132 | 10.6% | 9.3% | 5.1% | 3.9% | ||
| 2167 | 6.2% | |||||
| 2277 | 10.5% | 9.3% | 5.2% | 4.0% | ||
| 2316 | 8.1% | |||||
| 2431 | 10.3% | 9.2% | 5.2% | 4.1% | ||
| 2500 | 11.4% | 11.4% | ||||
| 2578 | 7.7% | |||||
| 2596 | 10.2% | 9.1% | 5.3% | 4.2% | ||
| 2772 | 10.2% | 9.1% | 5.4% | 4.3% | ||
| 2959 | 10.1% | 9.0% | 5.4% | 4.4% | ||
| 3006 | 4.7% | |||||
| 3019 | 8.1% | |||||
| 3160 | 10.0% | 9.0% | 5.5% | 4.5% | ||
| 3374 | 9.9% | 8.9% | 5.5% | 4.6% | ||
| 3602 | 9.8% | 8.8% | 5.6% | 4.7% | ||
| 3659 | 5.6% | |||||
| 3706 | 10.2% | 10.2% | ||||
| 3846 | 9.7% | 8.8% | 5.6% | 4.7% | ||
| 4000 | 4.6% | 4.6% | ||||
| 4075 | 7.9% | |||||
| 4107 | 9.6% | 8.8% | 5.7% | 4.8% | ||
| 4385 | 9.6% | 8.7% | 5.7% | 4.9% | ||
| 4682 | 9.5% | 8.6% | 5.8% | 5.0% | ||
| 4883 | 6.8% | |||||
| 4944 | 6.2% | |||||
| 4999 | 9.4% | 8.6% | 5.8% | 5.0% | ||
| 5142 | 7.0% | |||||
| 5338 | 9.3% | 8.6% | 5.9% | 5.1% | ||
| 5477 | 8.0% | |||||
| 5700 | 9.3% | 8.5% | 5.9% | 5.2% | ||
| 6044 | 8.7% | |||||
| 6086 | 9.2% | 8.5% | 6.0% | 5.2% | ||
| 6498 | 9.1% | 8.4% | 6.0% | 5.3% | ||
| 6938 | 9.1% | 8.4% | 6.0% | 5.4% | ||
| 7033 | 6.0% | |||||
| 7136 | 6.8% | |||||
| 7408 | 6.4% | 9.0% | 8.4% | 6.1% | 5.4% |
| Appointments in the year | Clinics | Upper 99.8% | Upper 95% | Lower 95% | Lower 99.8% | Outside 99.8% |
|---|---|---|---|---|---|---|
| 155 | 13.6% | 14.3% | 11.7% | 2.8% | 0.2% | |
| 165 | 14.1% | 11.6% | 2.9% | 0.4% | ||
| 168 | 5.9% | |||||
| 177 | 7.3% | 13.9% | 11.5% | 3.0% | 0.5% | |
| 180 | 11.7% | |||||
| 186 | 3.8% | |||||
| 189 | 13.7% | 11.3% | 3.1% | 0.7% | ||
| 200 | 5.0% | |||||
| 201 | 13.6% | 11.2% | 3.2% | 0.9% | ||
| 215 | 13.4% | 11.1% | 3.3% | 1.0% | ||
| 230 | 6.1% | 13.2% | 11.0% | 3.4% | 1.2% | |
| 242 | 7.4% | |||||
| 245 | 13.1% | 10.9% | 3.5% | 1.3% | ||
| 247 | 10.1% | |||||
| 262 | 13.0% | 10.9% | 3.6% | 1.5% | ||
| 280 | 12.8% | 10.8% | 3.7% | 1.6% | ||
| 299 | 12.7% | 10.7% | 3.8% | 1.8% | ||
| 309 | 9.4% | |||||
| 318 | 8.2% | |||||
| 319 | 12.6% | 10.6% | 3.8% | 1.9% | ||
| 336 | 4.5% | |||||
| 340 | 12.4% | 10.5% | 3.9% | 2.0% | ||
| 363 | 12.3% | 10.5% | 4.0% | 2.1% | ||
| 388 | 12.2% | 10.4% | 4.0% | 2.2% | ||
| 414 | 12.1% | 10.3% | 4.1% | 2.3% | ||
| 442 | 12.0% | 10.3% | 4.2% | 2.4% | ||
| 461 | 8.0% | |||||
| 472 | 11.9% | 10.2% | 4.2% | 2.5% | ||
| 504 | 11.8% | 10.1% | 4.3% | 2.6% | ||
| 536 | 7.5% | |||||
| 538 | 11.7% | 10.1% | 4.4% | 2.7% | ||
| 575 | 11.7% | 10.0% | 4.4% | 2.8% | ||
| 586 | 7.5% | |||||
| 608 | 10.0% | |||||
| 614 | 11.6% | 10.0% | 4.5% | 2.9% | ||
| 655 | 11.5% | 9.9% | 4.5% | 2.9% | ||
| 661 | 7.0% | |||||
| 700 | 11.4% | 9.9% | 4.6% | 3.0% | ||
| 747 | 11.3% | 9.8% | 4.6% | 3.1% | ||
| 754 | 8.8% | |||||
| 781 | 8.3% | |||||
| 798 | 11.3% | 9.8% | 4.6% | 3.2% | ||
| 852 | 11.2% | 9.8% | 4.7% | 3.2% | ||
| 910 | 11.2% | 9.7% | 4.7% | 3.3% | ||
| 971 | 11.1% | 9.7% | 4.8% | 3.3% | ||
| 996 | 7.6% | |||||
| 1037 | 11.1% | 9.7% | 4.8% | 3.4% | ||
| 1096 | 10.4% | |||||
| 1107 | 11.0% | 9.6% | 4.8% | 3.4% | ||
| 1117 | 7.5% | |||||
| 1182 | 10.9% | 9.6% | 4.9% | 3.5% | ||
| 1198 | 9.6% | |||||
| 1262 | 10.9% | 9.6% | 4.9% | 3.5% | ||
| 1311 | 8.1% | |||||
| 1348 | 10.8% | 9.5% | 4.9% | 3.6% | ||
| 1439 | 10.8% | 9.5% | 4.9% | 3.6% | ||
| 1537 | 10.8% | 9.5% | 5.0% | 3.7% | ||
| 1641 | 10.7% | 9.4% | 5.0% | 3.7% | ||
| 1716 | 6.4% | |||||
| 1752 | 10.7% | 9.4% | 5.0% | 3.7% | ||
| 1871 | 10.7% | 9.4% | 5.0% | 3.8% | ||
| 1997 | 10.6% | 9.4% | 5.1% | 3.8% | ||
| 2008 | 6.3% | |||||
| 2109 | 6.3% | |||||
| 2132 | 10.6% | 9.4% | 5.1% | 3.8% | ||
| 2167 | 6.2% | |||||
| 2277 | 10.6% | 9.3% | 5.1% | 3.9% | ||
| 2316 | 8.1% | |||||
| 2431 | 10.5% | 9.3% | 5.1% | 3.9% | ||
| 2500 | 11.4% | 11.4% | ||||
| 2578 | 7.7% | |||||
| 2596 | 10.5% | 9.3% | 5.1% | 3.9% | ||
| 2772 | 10.5% | 9.3% | 5.1% | 3.9% | ||
| 2959 | 10.5% | 9.3% | 5.1% | 4.0% | ||
| 3006 | 4.7% | |||||
| 3019 | 8.1% | |||||
| 3160 | 10.5% | 9.3% | 5.2% | 4.0% | ||
| 3374 | 10.4% | 9.3% | 5.2% | 4.0% | ||
| 3602 | 10.4% | 9.2% | 5.2% | 4.0% | ||
| 3659 | 5.6% | |||||
| 3706 | 10.2% | |||||
| 3846 | 10.4% | 9.2% | 5.2% | 4.0% | ||
| 4000 | 4.6% | |||||
| 4075 | 7.9% | |||||
| 4107 | 10.4% | 9.2% | 5.2% | 4.0% | ||
| 4385 | 10.4% | 9.2% | 5.2% | 4.1% | ||
| 4682 | 10.4% | 9.2% | 5.2% | 4.1% | ||
| 4883 | 6.8% | |||||
| 4944 | 6.2% | |||||
| 4999 | 10.3% | 9.2% | 5.2% | 4.1% | ||
| 5142 | 7.0% | |||||
| 5338 | 10.3% | 9.2% | 5.2% | 4.1% | ||
| 5477 | 8.0% | |||||
| 5700 | 10.3% | 9.2% | 5.2% | 4.1% | ||
| 6044 | 8.7% | |||||
| 6086 | 10.3% | 9.2% | 5.3% | 4.1% | ||
| 6498 | 10.3% | 9.2% | 5.3% | 4.1% | ||
| 6938 | 10.3% | 9.2% | 5.3% | 4.2% | ||
| 7033 | 6.0% | |||||
| 7136 | 6.8% | |||||
| 7408 | 6.4% | 10.3% | 9.2% | 5.3% | 4.2% |
Synthetic data. Source: projects/spc-toolkit/examples/run.py.
- No adjustment: 15 clinics outside the 95% limits and 9 outside 99.8%, including both large planted clinics. The other seven differ from the average only by the ordinary clinic-to-clinic spread, which binomial limits, narrow at thousands of appointments, treat as a signal.
- Multiplicative (φ): 3 outside 99.8%: clinics 12 and 31, and clinic 11, whose planted rate is 9.1% but which recorded 10.1% on 3,706 appointments.
- Additive (τ²): 1 outside 99.8%: clinic 12. Clinic 31 recorded 183 DNAs in 4,000 appointments (4.575%), inside an additive lower limit of 4.05%.
In the funnel article’s data, the additive limits flagged exactly the two planted outliers. Here they flag one of two. Additive limits ask whether a clinic stands out from the spread of clinics, not just from the average, and neither planted clinic is extreme on that scale: clinics 12 and 31 sit 2.60 and 2.71 planted standard deviations from typical. Clinic 12 still crosses the line; clinic 31 doesn’t. Clinic 40 is inside every set of limits, because 11.7% on 180 appointments is not enough evidence. Pick the adjustment before you see which clinics it flags.
Design decisions
- Pure functions. No file access, no global state, no plotting. The same inputs always give the same outputs, so the functions are easy to test and safe to call from a notebook, a pipeline or a web service.
- Typed returns. Limits come back as frozen dataclasses with named fields (
limits.upper, notlimits["upper"]), so a typo fails in the type checker, not in a board paper. Apy.typedmarker lets users’ type checkers read the annotations. - One protocol for the rules.
special_causesaccepts anything withcentre,upperandlower, so the same rule code handles XmR limits (single numbers) and p-chart limits (arrays). - One binomial limit function. A p-chart and a funnel plot draw the same limits, p ± z × √(p(1 − p)/n), with different z. Both call one function, so a fix to one is a fix to both.
- No plotting in the core. The chart specs live in
examples/page_charts.py. The maths outlives any chart library, and the same numbers can go to Power BI as a CSV. - Loud failures. Missing values, zero denominators, numerators above their denominator, limits in the wrong order and φ below 1 raise a
ValueErrorwith a plain message. A silent NaN in a control limit is worse than an error. - Tested lower bounds. The dependencies are
numpy>=2.0andpandas>=2.2.2, and the tests pass with the newest versions and with the oldest the bounds allow (uv run --isolated --python 3.12 --resolution lowest-direct pytest).
Tests, and what they caught
The 471 test cases come from 51 test functions and run in seconds. They fall into five groups:
- Known answers worked by hand. Values 10, 12, 11, 13 and 14 have a mean of 12 and a mean moving range of 1.5, so the limits are 12 ± 2.66 × 1.5 = 8.01 to 15.99.
- Edge cases. Empty input, one point, NaN,
pd.NA, a flat line, zero denominators, a point exactly on a limit or on the centre line. - Invariants. Adding a constant moves the limits without widening them; additive limits are never narrower than binomial ones.
- An oracle. The vectorised rules are compared with the plain loops from the SPC article on 300 random series with uneven limits, and
first_signalswith rerunning the rules point by point on 100 of them. - SQL parity.
sql/xmr_limits.sqlcomputes the limits withLAGfor teams that work in the warehouse, and a test checks it againstxmr_limitsin an in-memory SQLite database.
What they caught:
- A wrong test. “Limits narrow as the denominator grows” failed on its first run. The code was right: the lower limit is clipped at zero for the smallest denominators, so it stays flat there. The test encoded a belief I hadn’t checked.
- A conservative check. I expected the chi-squared check to flag about 10 of 200 simulated data sets with no overdispersion. It flagged none. Winsorising pulls the extreme z-scores in, so without overdispersion φ averages about 0.68, not 1. The package keeps the articles’ method, and a test now pins the behaviour.
- A wrong lower bound. The first
pyproject.tomlsaidpandas>=2.2. The lowest-version run installed pandas 2.2.2, because 2.2.0 and 2.2.1 require NumPy below 2. The bound now says what was tested.
The oracle tests passed first time. They are there for the next change, not this one.
Install it from the zip
The download below unzips to a folder called spc-toolkit-python-package. Any of these works:
uv add --no-workspace ./spc-toolkit-python-package # into a uv project, as a path dependency
uv pip install ./spc-toolkit-python-package # into the active virtual environment
python -m pip install ./spc-toolkit-python-package # the same with pip
uv build ./spc-toolkit-python-package # a wheel to share, in its dist/ folder
From inside the folder, uv run pytest runs the tests and uv run python examples/run.py rewrites the charts and CSVs into output/. I built the wheel, installed it into a fresh Python 3.12 environment and imported it before publishing this page. The package isn’t on PyPI, and no licence has been chosen yet.
Limits
- Normal-approximation limits. They are too tight above small denominators, as example 2 shows.
- Crude rates only. For outcomes like readmission, a funnel should plot observed over expected from a case-mix model, which the package doesn’t build.
- A conservative overdispersion check. Mild overdispersion can pass it.
- No sense of direction. The rules say a signal happened, not whether it is good news; that depends on the measure.
- Three chart types. XmR (with its moving range chart), p-chart and funnel plot, nothing else yet.
What I’d do next
- Publish it. Choose a licence, then push the wheel to a private index so teams can
uv add spckitwith a version pin. - A documentation site. An API reference generated from the docstrings, with the three examples as tutorials.
- More chart types. Run charts for short series, c- and u-charts for counts, and Laney p′ charts, which adjust p-chart limits for overdispersion.
- Exact limits and direction. Exact binomial limits for small denominators, and an option to label each signal as an improvement or a concern.
- CI. Run the tests and the lowest-version check on every push, as in testing analytics code with pytest.
Run it yourself
From the root of the site repository:
uv run pytest projects/spc-toolkit/tests
uv run python projects/spc-toolkit/examples/run.py
The script regenerates the three data sets, prints every number quoted here, and rewrites the charts and CSVs on this page. A rerun gives byte-identical files.
Built with
- Python
- NumPy
- pandas
- pytest
- uv
- hatchling
Downloads
-
Weekly assessment times with XmR limits and flags
78 synthetic weeks: the value, its moving range, the baseline limits, and one True/False column per special-cause rule, exactly as special_causes returns them.
-
Clinic DNA rates with funnel limits
45 synthetic clinics: counts, rate, planted true rate, z-score, and 99.8% limits and flags with no adjustment, multiplicative and additive.
- Project code