using StatsOrdinalPatterns, Random, Statistics, Printf, DataFramesReplication: Weiß (2022)
This page reproduces the DGP 1 block of Table IV in Weiß (2022). Table IV reports simulated rejection rates of the seven asymptotic ordinal pattern tests at level \(\alpha = 0.05\), together with the classical ACF test as a benchmark, for growing sample size \(T\).
Libraries
Statistics of the paper and their types in the package
Each column of Table IV corresponds to one chart_choice of test_op, which returns the statistic, the asymptotic critical value, the \(p\) value and the reject decision.
| Table IV column | chart_choice |
Rejects \(H_0\) when | Null distribution |
|---|---|---|---|
| \(H(\hat{\mathbf{p}}^{(d)})\) | Shannon(base=exp(1)) |
\(\widehat{H} < c\) | generalized \(\chi^2\) |
| \(H_{\text{ex}}(\hat{\mathbf{p}}^{(d)})\) | ShannonExtropy(base=exp(1)) |
\(\widehat{H}_{\text{ex}} < c\) | generalized \(\chi^2\) |
| \(\Delta^2(\hat{\mathbf{p}}^{(d)})\) | DistanceToWhiteNoise() |
\(\widehat{\Delta} > c\) | generalized \(\chi^2\) |
| \(\hat{\beta}^{(d)}\) | UpDownBalance() |
\(\lvert\hat{\beta}\rvert > c\) | normal, \(\sigma^2 = \tfrac{1}{3n}\) |
| \(\hat{\tau}^{(d)}\) | Persistence() |
\(\lvert\hat{\tau}\rvert > c\) | normal, \(\sigma^2 = \tfrac{8}{45n}\) |
| \(\hat{\gamma}^{(d)}\) | RotationalAsymmetry() |
\(\lvert\hat{\gamma}\rvert > c\) | normal, \(\sigma^2 = \tfrac{2}{5n}\) |
| \(\hat{\delta}^{(d)}\) | UpDownScaling() |
\(\lvert\hat{\delta}\rvert > c\) | normal, \(\sigma^2 = \tfrac{2}{3n}\) |
| ACF at lag \(d\) | test_acf |
\(\lvert\hat{\rho}(d)\rvert > c\) | normal, \(\sigma^2 = \tfrac{1}{T}\) |
Here \(n = T - (m-1)d\) is the number of patterns in a series of length \(T\). The critical value \(c\) of each row is available on its own through crit_val_op(chart_choice, m, n) and crit_val_acf(T). Two conventions matter for the match:
- The package indexes patterns by their Lehmer code (see
perm_to_lehm_idx). For \(m = 3\) this is the lexicographic numbering \(\hat p_1, \ldots, \hat p_6\) of the paper, so \(\hat\beta\), \(\hat\tau\), \(\hat\gamma\) and \(\hat\delta\) pick the same components. UpDownBalance()andUpDownScaling()return \(-\hat\beta\) and \(-\hat\delta\). All four Bandt statistics are tested two sided, so the rejection rates are unaffected.
Setup
DGP 1 is the Gaussian AR(1) process \(X_t = \alpha X_{t-1} + \epsilon_t\) with \(\epsilon_t \sim \text{i.i.d. } N(0,1)\) and \(\alpha = 0.5\), tested at delay \(d = 1\). The DGP types of the package drive the sequential run length simulations. A classical test needs a complete series, so the process is written out directly and started from its stationary distribution.
function sim_ar1!(x, α)
x[1] = randn() / sqrt(1 - α^2) # stationary start
for t in 2:length(x)
x[t] = α * x[t-1] + randn()
end
return x
end
α = 0.5 # DGP 1
m, d = 3, 1 # pattern length and delay
level = 0.05 # nominal significance level
Ts = (100, 250, 500, 1000, 1500, 2500) # sample sizes of Table IV
reps = 10_000 # Monte Carlo replications
charts = [
("H", Shannon(base=exp(1))),
("Hex", ShannonExtropy(base=exp(1))),
("Δ²", DistanceToWhiteNoise()),
("β", UpDownBalance()),
("τ", Persistence()),
("γ", RotationalAsymmetry()),
("δ", UpDownScaling()),
]A single test
Random.seed!(1)
x = sim_ar1!(zeros(500), α)
test_op(x; chart_choice=Persistence(), m=m, d=d, alpha=level)OPTestResult Chart: Persistence() Statistic: 0.0663 ───────────────────────────── Asymptotic test Critical value: 0.037 p-value: 0.0005 Reject H₀: true
test_acf(x, d; alpha=level)ACFTestResult
Statistic: 0.4671
─────────────────────────────
Asymptotic test
Critical value: 0.0877
p-value: 0.0
Reject H₀: true
Running the power study
crit_val_op returns the critical value alone. It depends on the chart, on \(m\) and on the number of patterns \(n\), but not on the data, so for a fixed \(T\) it is the same in all replications. test_op additionally computes the \(p\) value, which does depend on the data and which for the three omnibus statistics means evaluating a generalized \(\chi^2\) distribution numerically. A power study only needs the decision, so the critical values are taken once per sample size and each replication then costs one pass over the series. That is about 300 times faster than a plain loop over test_op.
# rejection rules of the table above
rejects(::Union{Shannon,ShannonExtropy}, stat, crit) = stat < crit
rejects(::DistanceToWhiteNoise, stat, crit) = stat > crit
rejects(_, stat, crit) = abs(stat) > crit
function power(T, α, reps; m=3, d=1, level=0.05)
n = T - (m - 1) * d
crits = [crit_val_op(cc, m, n; alpha=level) for (_, cc) in charts]
crit_acf = crit_val_acf(T; alpha=level)
tasks = map(Iterators.partition(1:reps, cld(reps, Threads.nthreads()))) do idx
Threads.@spawn begin
x = Vector{Float64}(undef, T)
hits = zeros(Int, length(charts) + 1)
for _ in idx
sim_ar1!(x, α)
p̂ = stat_op(x; chart_choice=Shannon(base=exp(1)), m=m, d=d)[2]
for (j, (_, cc)) in enumerate(charts)
hits[j] += rejects(cc, chart_stat_op(p̂, cc), crits[j])
end
hits[end] += abs(stat_acf(x, d)) > crit_acf
end
hits
end
end
return sum(fetch.(tasks)) ./ reps
endThe shortcut has to give the same statistic and the same decision as test_op and test_acf, not merely a similar one:
let x = sim_ar1!(zeros(400), α), n = 400 - (m - 1) * d
p̂ = stat_op(x; chart_choice=Shannon(base=exp(1)), m=m, d=d)[2]
ok = all(
begin
r = test_op(x; chart_choice=cc, m=m, d=d, alpha=level)
s = chart_stat_op(p̂, cc)
s ≈ r.stat && crit_val_op(cc, m, n; alpha=level) ≈ r.asymp_crit &&
rejects(cc, s, r.asymp_crit) == r.asymp_reject
end for (_, cc) in charts
)
ok &= (abs(stat_acf(x, d)) > crit_val_acf(400; alpha=level)) == test_acf(x, d; alpha=level).asymp_reject
(shortcut_matches_test_op=ok,)
end(shortcut_matches_test_op = true,)
With reps = 10_000 the page runs in a few seconds and the standard error of a rejection rate is at most \(\sqrt{0.25 / 10^4} = 0.005\).
Table IV: DGP 1, \(d = 1\)
res = Dict(T => power(T, α, reps; m=m, d=d, level=level) for T in Ts)| Row | T | H | Hex | Δ² | β | τ | γ | δ | ACF |
|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | Float64 | |
| 1 | 100 | 0.259 | 0.278 | 0.269 | 0.056 | 0.545 | 0.042 | 0.045 | 0.997 |
| 2 | 250 | 0.611 | 0.628 | 0.625 | 0.065 | 0.881 | 0.047 | 0.036 | 1.0 |
| 3 | 500 | 0.93 | 0.935 | 0.934 | 0.055 | 0.992 | 0.053 | 0.041 | 1.0 |
| 4 | 1000 | 1.0 | 1.0 | 1.0 | 0.064 | 1.0 | 0.047 | 0.042 | 1.0 |
| 5 | 1500 | 1.0 | 1.0 | 1.0 | 0.059 | 1.0 | 0.054 | 0.04 | 1.0 |
| 6 | 2500 | 1.0 | 1.0 | 1.0 | 0.061 | 1.0 | 0.05 | 0.045 | 1.0 |
Comparison with the published rates
Published rates and comparison table
# published values, ordered (H, Hex, Δ², β, τ, γ, δ, ACF)
published = Dict(
100 => (0.250, 0.266, 0.256, 0.051, 0.556, 0.043, 0.044, 0.998),
250 => (0.614, 0.634, 0.630, 0.065, 0.882, 0.052, 0.039, 1.000),
500 => (0.929, 0.935, 0.934, 0.057, 0.992, 0.046, 0.038, 1.000),
1000 => (0.999, 0.999, 0.999, 0.057, 1.000, 0.046, 0.042, 1.000),
1500 => (1.000, 1.000, 1.000, 0.066, 1.000, 0.053, 0.043, 1.000),
2500 => (1.000, 1.000, 1.000, 0.056, 1.000, 0.052, 0.042, 1.000),
)
names = vcat([n for (n, _) in charts], "ACF")
cmp = DataFrame(T=Int[], statistic=String[], paper=Float64[], replicated=Float64[], dev=Float64[])
for T in Ts, (j, nm) in enumerate(names)
push!(cmp, (T, nm, published[T][j], round(res[T][j], digits=3),
round(res[T][j] - published[T][j], digits=3)))
end
cmp| Row | T | statistic | paper | replicated | dev |
|---|---|---|---|---|---|
| Int64 | String | Float64 | Float64 | Float64 | |
| 1 | 100 | H | 0.25 | 0.259 | 0.009 |
| 2 | 100 | Hex | 0.266 | 0.278 | 0.012 |
| 3 | 100 | Δ² | 0.256 | 0.269 | 0.013 |
| 4 | 100 | β | 0.051 | 0.056 | 0.005 |
| 5 | 100 | τ | 0.556 | 0.545 | -0.011 |
| 6 | 100 | γ | 0.043 | 0.042 | -0.001 |
| 7 | 100 | δ | 0.044 | 0.045 | 0.001 |
| 8 | 100 | ACF | 0.998 | 0.997 | -0.001 |
| 9 | 250 | H | 0.614 | 0.611 | -0.003 |
| 10 | 250 | Hex | 0.634 | 0.628 | -0.006 |
| 11 | 250 | Δ² | 0.63 | 0.625 | -0.005 |
| 12 | 250 | β | 0.065 | 0.065 | -0.0 |
| 13 | 250 | τ | 0.882 | 0.881 | -0.001 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 37 | 1500 | τ | 1.0 | 1.0 | 0.0 |
| 38 | 1500 | γ | 0.053 | 0.054 | 0.001 |
| 39 | 1500 | δ | 0.043 | 0.04 | -0.003 |
| 40 | 1500 | ACF | 1.0 | 1.0 | 0.0 |
| 41 | 2500 | H | 1.0 | 1.0 | 0.0 |
| 42 | 2500 | Hex | 1.0 | 1.0 | 0.0 |
| 43 | 2500 | Δ² | 1.0 | 1.0 | 0.0 |
| 44 | 2500 | β | 0.056 | 0.061 | 0.005 |
| 45 | 2500 | τ | 1.0 | 1.0 | 0.0 |
| 46 | 2500 | γ | 0.052 | 0.05 | -0.001 |
| 47 | 2500 | δ | 0.042 | 0.045 | 0.003 |
| 48 | 2500 | ACF | 1.0 | 1.0 | 0.0 |
largest absolute deviation: 0.013
mean absolute deviation: 0.003
Monte Carlo standard error: at most 0.005
All 48 entries are reproduced, with a mean absolute deviation below the Monte Carlo standard error of a single run. The largest deviations sit in the \(T = 100\) row of the three omnibus statistics, which are strongly correlated and therefore drift together.
Reading the table
- \(\hat\beta\), \(\hat\gamma\) and \(\hat\delta\) stay at the nominal 5 % for every \(T\). A Gaussian AR(1) process is reversible and symmetric, so it produces none of the asymmetries these statistics detect. Their columns verify the size of the tests, not their power.
- \(\hat\tau\) is the sharpest ordinal pattern test here, at 0.55 for \(T = 100\) where the omnibus statistics reach about 0.25. Persistence is exactly what a positive AR(1) coefficient creates.
- \(H\), \(H_{\text{ex}}\) and \(\Delta^2\) are nearly identical throughout. All three measure the distance of \(\hat{\mathbf{p}}\) from uniformity under the same generalized \(\chi^2\) null.
- The ACF test reaches 0.998 already at \(T = 100\). DGP 1 is a linear Gaussian process, the model the sample autocorrelation is efficient for, so it is the natural upper benchmark in this scenario.