using StatsOrdinalPatterns, Random, Statistics, DataFrames, CairoMakieTutorial for sequential spatial ordinal pattern tests
Libraries
Sequential testing with spatial ordinal patterns
Here, the monitored stream is a sequence of images: at each time \(t\) a new \(M \times N\) image arrives, all its \(2 \times 2\) windows at delays \((d_1, d_2)\) are classified into SOP types, and the resulting type frequencies \(\hat{\mathbf{p}}_t\) are smoothed exponentially (EWMA),
\[ \hat{\mathbf{p}}_t^{\text{EWMA}} = \lambda\,\hat{\mathbf{p}}_t + (1 - \lambda)\,\hat{\mathbf{p}}_{t-1}^{\text{EWMA}}, \qquad \hat{\mathbf{p}}_0^{\text{EWMA}} = (1/3, 1/3, 1/3). \]
The chart statistic is computed from the smoothed frequencies and the chart signals as soon as \(\lvert\text{stat}\rvert > cl\). Unlike the time series case, each single time point already contributes \((M-d_1)(N-d_2)\) patterns, so the chart reacts quickly.
Designing the chart means fixing \(\lambda\) and choosing cl such that the in-control ARL matches a target L0. See Adämmer et al. (2026).
Key functions
- Critical limit
cl_sop(sop_dgp, lam, L0, cl_init, d1, d2; chart_choice, refinement=OrdinaryType(), reps_bracket, reps_final, bracket_step, seed)- Returns the
clfor which the in-control ARL equalsL0. sop_dgp::ICSTS(M_rows, N_cols, dist): in-control spatial process (spatial white noise of the given image size).cl_init: rough starting value; the search first brackets the root withreps_bracketreplications in steps ofbracket_step, then refines it withreps_finalreplications.d1,d2: row and column delays — note they are positional arguments here.
- Returns the
- Chart performance
arl_sop_ic(sop_dgp, lam, cl, d1, d2, reps; chart_choice, refinement=OrdinaryType())— in-control ARL, returns(ARL, standard error).arl_sop_oc(spatial_dgp, lam, cl, d1, d2, reps; chart_choice, refinement=OrdinaryType())— out-of-control ARL for a spatially dependent process (SAR11,SAR1,SQMA11,SINAR11,SQINMA11,BSQMA11).
- Monitoring observed data
stat_sop(data, lam, d1, d2; chart_choice, refinement=OrdinaryType())— for a 3D array (rows × columns × time), returns the sequentially computed EWMA chart statistics.monitor_sop(data, lam, cl, d1, d2; chart_choice, refinement=OrdinaryType())— applies the alarm rule to these statistics and returns aControlChartResultwith the statistics, the limit and the first alarm. With Makie loaded,plotdraws it.
Alarm rules
The four Tau/Kappa statistics are used for monitoring; all of them are two-sided, so the chart signals when \(\lvert\text{stat}\rvert > cl\).
| Statistic | Constructor | Definition | Under \(H_0\) |
|---|---|---|---|
| \(\hat{\tau}\) | TauHat() |
\(\hat{p}_1 - 1/3\) | \(0\) |
| \(\hat{\kappa}\) | KappaHat() |
\(\hat{p}_2 - \hat{p}_3\) | \(0\) |
| \(\tilde{\tau}\) | TauTilde() |
\(\tilde{p}_3 - 1/3\) | \(0\) |
| \(\tilde{\kappa}\) | KappaTilde() |
\(\tilde{p}_1 - \tilde{p}_2\) | \(0\) |
Setting refinement to RotationType(), DirectionType(), or DiagonalType() replaces the three classical SOP types by the corresponding six refined types. The refinement argument must be identical in cl_sop, arl_sop_ic, and arl_sop_oc — otherwise the critical limit does not belong to the monitored statistic.
Setup
M, N = 11, 11 # image dimensions
d1, d2 = 1, 1 # row and column delays
lam = 0.1 # EWMA smoothing parameter
L0 = 370.0 # target in-control ARL
ic = ICSTS(M, N, Normal(0, 1)) # in-control: spatial white noiseICSTS(11, 11, Distributions.Normal{Float64}(μ=0.0, σ=1.0))
Computing the critical limit
cl = cl_sop(
ic, lam, L0, 0.026, d1, d2;
chart_choice=TauTilde(),
reps_bracket=500, reps_final=2_000, bracket_step=0.002, seed=42,
verbose=true
)
============================================================
STEP 1: Bracket Search
reps = 500 | truncation cap = 18500 | step size = 0.002
============================================================
cl = 0.026 | ARL = 121.248
cl = 0.028 | ARL = 179.01
cl = 0.03 | ARL = 260.286
cl = 0.032 | ARL = 414.918
------------------------------------------------------------
Bracket found: [0.026, 0.032]
------------------------------------------------------------
============================================================
STEP 2: Root Finding via ITP
reps = 2000 | no truncation | tolerance = 0.0001
============================================================
cl = 0.026 | ARL = 116.428
cl = 0.032 | ARL = 393.329
cl = 0.031487 | ARL = 354.9945
cl = 0.031688 | ARL = 366.2585
cl = 0.031731 | ARL = 368.107
cl = 0.031751 | ARL = 370.267
Results of univariate zero finding:
* Converged to: 0.03175136751000527
* Algorithm: Roots.ITP{Float64, Int64}(0.2, 2, 1)
* iterations: 4
* function evaluations ≈ 6
* stopped as x_n ≈ x_{n-1} using atol=xatol, rtol=xrtol
Trace:
(a₀, b₀) = ( 0.025999999999999999, 0.032000000000000001 )
(a₁, b₁) = ( 0.031487298033593236, 0.032000000000000001 )
(a₂, b₂) = ( 0.031688040568479577, 0.032000000000000001 )
(a₃, b₃) = ( 0.031731176938555358, 0.032000000000000001 )
(a₄, b₄) = ( 0.031731176938555358, 0.03175136751000527 )
0.03175136751000527
verbose=true prints cl and the associated ARL at every evaluation, so both phases of the search can be followed: the bracketing steps away from cl_init until the ARL crosses L0, then the ITP algorithm refines the bracket. cl_init = 0.026 only has to be in the right ballpark, but it should sit below the root, and bracket_step should be of the order of the remaining distance — otherwise the bracketing crawls.
Increase reps_final for a more precise limit, and pass verbose=false (the default) to get the limit alone.
Verifying the in-control ARL
arl_sop_ic(ic, lam, cl, d1, d2, 5_000; chart_choice=TauTilde())(376.8928, 5.247841269439258)
The returned tuple is (ARL, standard error); the ARL is within a few standard errors of L0.
Out-of-control performance
The same cl is used to monitor spatially dependent images. Note that the out-of-control DGP must generate images of the same size as the in-control process.
oc_dgps = [
("SAR(1,1), α = (0.1, 0.1, 0.0)", SAR11((0.1, 0.1, 0.0), M, N, Normal(0, 1), nothing, 20)),
("SAR(1,1), α = (0.2, 0.2, 0.0)", SAR11((0.2, 0.2, 0.0), M, N, Normal(0, 1), nothing, 20)),
("SAR(1,1), α = (0.3, 0.3, 0.1)", SAR11((0.3, 0.3, 0.1), M, N, Normal(0, 1), nothing, 20)),
("SQMA(1,1), β = (0.5, 0.3, 0.2)", SQMA11((0.5, 0.3, 0.2), (1, 1, 2), M, N, Normal(0, 1), nothing)),
]
oc_res = [
arl_sop_oc(dgp, lam, cl, d1, d2, 5_000; chart_choice=TauTilde())
for (_, dgp) in oc_dgps
]DataFrame(
Process=[n for (n, _) in oc_dgps],
ARL=round.([r[1] for r in oc_res], digits=2),
se=round.([r[2] for r in oc_res], digits=3),
)| Row | Process | ARL | se |
|---|---|---|---|
| String | Float64 | Float64 | |
| 1 | SAR(1,1), α = (0.1, 0.1, 0.0) | 17.51 | 0.142 |
| 2 | SAR(1,1), α = (0.2, 0.2, 0.0) | 6.98 | 0.036 |
| 3 | SAR(1,1), α = (0.3, 0.3, 0.1) | 5.29 | 0.022 |
| 4 | SQMA(1,1), β = (0.5, 0.3, 0.2) | 3.75 | 0.012 |
Even weak spatial dependence is detected within a few images, because every image contributes \((M - d_1)(N - d_2) = 100\) patterns.
Monitoring an observed image sequence
monitor_sop applied to a 3D array computes the EWMA chart statistics of an observed image sequence (the same ones stat_sop returns), applies the two sided alarm rule and records the first signal. The sequence below is spatial white noise up to \(t = 20\) and spatially dependent afterwards.
Random.seed!(42)
T, T_change = 40, 20
images = randn(M, N, T)
φ = 0.4
for t in (T_change+1):T, i in 2:M, j in 2:N
images[i, j, t] = φ * (images[i-1, j, t] + images[i, j-1, t]) / 2 + randn()
end
res = monitor_sop(images, lam, cl, d1, d2; chart_choice=TauTilde())ControlChartResult Family: spatial ordinal patterns (SOP) Chart: TauTilde() λ: 0.1 Control limit: 0.0318 Alarm rule: |stat| > cl Statistics: 40 (images 1 to 40) First alarm: image 23 (statistic -0.0392)
Each image contributes one statistic, so res.alarm is the index of the first image at which the chart signals:
(first_alarm=res.alarm, delay=res.alarm - T_change,
stat_at_alarm=round(res.stats[res.alarm], digits=4))(first_alarm = 23, delay = 3, stat_at_alarm = -0.0392)
The chart passes through the white-noise part without a false alarm and signals shortly after the change. With CairoMakie loaded, plot shows the statistics against the control limits, shades the rejection region and marks the first alarm. See the plotting tutorial for the options.
plot(res)