Memory-based charts: EWMA and CUSUM

library(shewhartr)
library(ggplot2)
library(dplyr)

When the Shewhart chart is too slow

A Shewhart chart looks at one observation at a time. Each point is compared against the centre line and the 3-sigma limits, and that’s it — the chart has no memory of what came before (the runs rules add a little, but only a little). This makes Shewhart charts excellent at catching large shifts the moment they happen, but fairly slow at catching small persistent shifts that would only become visible after the data has had time to drift.

EWMA and CUSUM accumulate information across observations, trading a slightly less direct interpretation for substantially better sensitivity to small shifts.

# A 0.5-sigma sustained shift after observation 40
set.seed(2026)
n      <- 80
shift  <- c(rnorm(40, mean = 0, sd = 1),
            rnorm(40, mean = 0.5, sd = 1))
df     <- tibble::tibble(t = seq_len(n), y = shift)

imr  <- shewhart_i_mr(df,  value = y, index = t)
ewma <- shewhart_ewma(df,  value = y, index = t)
cusum <- shewhart_cusum(df, value = y, index = t)

# Position of the first alarm under each chart
first_alarm <- function(fit) {
  hits <- which(fit$augmented$.flag_any)
  if (length(hits) == 0L) NA_integer_ else min(hits)
}
tibble::tibble(
  chart  = c("I-MR", "EWMA", "CUSUM"),
  alarm  = c(first_alarm(imr), first_alarm(ewma), first_alarm(cusum))
)
#> # A tibble: 3 × 2
#>   chart alarm
#>   <chr> <int>
#> 1 I-MR     10
#> 2 EWMA     NA
#> 3 CUSUM    15

For a 0.5-sigma shift, the I-MR chart often fails to signal at all within 80 observations, while the EWMA and CUSUM typically catch it within a few points of the change.

EWMA — Exponentially Weighted Moving Average

The EWMA statistic is a recursive convex combination: \[z_i = \lambda x_i + (1 - \lambda) z_{i-1}, \qquad z_0 = \mu.\]

With lambda close to 1 the chart behaves like a Shewhart I chart (little memory). With lambda close to 0 it averages over a long history (heavy memory, slow but very sensitive). The classical default lambda = 0.2, L = 2.7 gives ARL_0 ≈ 370 and is well matched to detecting shifts of 0.5 to 1 sigma (Lucas & Saccucci 1990).

fit <- shewhart_ewma(df, value = y, index = t,
                     lambda = 0.2, L = 2.7)
fit
#> 
#> ── Shewhart chart ewma ─────────────────────────────────────────────────────────
#> • Observations / subgroups: 80
#> • Phase: "phase_1"
#> • Sigma estimate ("mr"): 1.014
#> 
#> 
#> ── Control limits ──
#> # A tibble: 3 × 3
#>   chart line            value
#>   <chr> <chr>           <dbl>
#> 1 EWMA  CL              0.165
#> 2 EWMA  UCL_asymptotic  1.08 
#> 3 EWMA  LCL_asymptotic -0.748
#> ── Rule violations ──
#> 
#> ✔ No violations across 1 rule: "nelson_1_beyond_3s".
autoplot(fit)

The control limits in an EWMA chart are time-varying by default. They start narrow and widen out to the asymptotic value L · σ · sqrt(λ / (2 - λ)) as the recursion warms up. This is the correct probability-symmetric calibration; setting steady_state = TRUE flattens them out at the asymptotic value (a common simplification once you have a long enough baseline).

shewhart_ewma(df, value = y, index = t, steady_state = TRUE) |>
  autoplot()

Choosing lambda

A practical guide (Montgomery 2019, Table 9.10):

Target shift size Reasonable lambda L ARL_0
0.25 σ 0.05 2.49 ~370
0.50 σ 0.10 2.70 ~370
0.75 σ 0.20 2.86 ~370
1.00 σ 0.40 3.00 ~370

Choose the smallest shift you care about, then use the matching row. You can validate the actual ARL with [shewhart_arl()] from the arl-simulation vignette.

CUSUM — Cumulative Sum

The tabular CUSUM keeps two non-negative running totals, one for upward drift and one for downward drift:

\[ C^+_i = \max\!\left(0,\; C^+_{i-1} + (x_i - \mu) - k\sigma\right) \]

\[ C^-_i = \max\!\left(0,\; C^-_{i-1} - (x_i - \mu) - k\sigma\right) \]

An alarm fires when either crosses the decision interval h · sigma. The reference value k is typically half the smallest shift size the chart should be sensitive to (so k = 0.5 for shifts of 1·σ); h controls the false-alarm rate.

fit <- shewhart_cusum(df, value = y, index = t, k = 0.5, h = 4)
fit
#> 
#> ── Shewhart chart cusum ────────────────────────────────────────────────────────
#> • Observations / subgroups: 80
#> • Phase: "phase_1"
#> • Sigma estimate ("mr"): 1.014
#> 
#> 
#> ── Control limits ──
#> # A tibble: 2 × 3
#>   chart line    value
#>   <chr> <chr>   <dbl>
#> 1 CUSUM h_upper  4.05
#> 2 CUSUM h_lower -4.05
#> ── Rule violations ──
#> 
#> ! 1 violation across 1 rule.
#> cusum_decision: 1 hit.
autoplot(fit)

The plot draws C+ as positive bars (potential upward drift) and C- as negative bars (potential downward drift), with the symmetric decision interval in dashed red. CUSUM’s main charm is interpretability once it signals: the bar’s height tells you how much accumulated deviation triggered the alarm.

Choosing k and h

Hawkins & Olwell (1998), Table 3.1, gives ARL profiles for k = 0.5 (the most common choice):

h ARL_0 (in control) ARL_1 at 1σ shift
4.00 ~168 ~8.4
4.77 ~370 ~10.4
5.00 ~465 ~10.9

The default h = 4 is appropriate for diagnostic monitoring. For a production chart matched to the typical Shewhart ARL_0 = 370, raise h to 4.77.

Phase II monitoring

Both charts accept target and sigma to override the in-data estimates — exactly what you need for Phase II monitoring against a clean Phase I baseline. The example below calibrates on a 60-point in-control baseline and then monitors a fresh 40-point period:

set.seed(1)
baseline <- tibble::tibble(t = 1:60, y = rnorm(60, mean = 100, sd = 2))
mu_hat   <- mean(baseline$y)
sd_hat   <- sd(baseline$y)

new_data <- tibble::tibble(
  t = 61:100,
  y = rnorm(40, mean = 100.8, sd = 2)   # 0.4 sigma shift
)

shewhart_ewma(new_data, value = y, index = t,
              target = mu_hat, sigma = sd_hat,
              lambda = 0.2, L = 2.7) |>
  autoplot()

A calibrate() / monitor() integration for memory-based charts is on the roadmap for a future release; for the moment the explicit target / sigma arguments give you the same workflow with one extra line.

Which one should I use?

Question EWMA CUSUM Shewhart
Best at 0.25–1 σ shifts?
Best at >2 σ shifts?
Plot reads like the original variable?
Communicates magnitude of accumulated drift?
Survives autocorrelated data well? (with care) (with care)
Pairs naturally with runs rules? (Nelson 1 only)

Many practitioners run both a Shewhart chart and an EWMA (or CUSUM) on the same series — the Shewhart catches large excursions, the memory-based catches the slow drifts.

References