| Title: | Lagged Procrustean Randomization Test for Multivariate Time Series |
|---|---|
| Description: | Extends the Procrustean randomization test (PROTEST) to ordered multivariate series. Concordance between two configurations is scanned across candidate temporal lags. Significance is assessed with phase-randomized surrogates (default), cyclic shifts of the full series or free permutation of rows, and a global max-statistic p-value accounts for scanning several lags and selecting the best one. Returns the lag profile, the lag of maximal concordance and per-observation Procrustes residuals. |
| Authors: | Diogo B. Provete [aut, cre] (ORCID: <https://orcid.org/0000-0002-0097-0651>) |
| Maintainer: | Diogo B. Provete <[email protected]> |
| License: | MIT + file LICENSE |
| Version: | 0.3.0 |
| Built: | 2026-11-02 01:00:07 UTC |
| Source: | https://github.com/diogoprov/lagprotest |
Scans Procrustes concordance between two multivariate series across candidate temporal lags and tests it by permutation at each lag.
lagged_protest( X, Y, lags = 0:6, permutation = c("phase", "cyclic", "free"), nperm = 999, mirror = TRUE, bidirectional = FALSE, scale = TRUE, p.adjust.method = "none", period = NULL, seed = NULL )lagged_protest( X, Y, lags = 0:6, permutation = c("phase", "cyclic", "free"), nperm = 999, mirror = TRUE, bidirectional = FALSE, scale = TRUE, p.adjust.method = "none", period = NULL, seed = NULL )
X, Y
|
Numeric matrices or data frames with rows as ordered time
points and columns as variables, or |
lags |
Integer vector of non-negative candidate lags. |
permutation |
Permutation scheme, |
nperm |
Number of surrogates ( |
mirror |
Logical; with |
bidirectional |
Logical; if |
scale |
Logical; z-score the columns of |
p.adjust.method |
Correction of the per-lag p-values for multiple
lags: |
period |
Optional length of the seasonal cycle, in rows. If given,
|
seed |
Optional integer seed for the |
Rows of X and Y are ordered time points (e.g., monthly centroids) and
must correspond one to one. At lag k > 0, X at time t is paired with
Y at time t + k (X leads Y), and the series are truncated to the
overlapping window of n - k rows. Negative lags (Y leads X) are tested
when bidirectional = TRUE. The statistic at each lag is the symmetric
Procrustes sum of squares from vegan::procrustes(); the lag of
maximal concordance is the one with the smallest .
Three permutation schemes are available:
"phase" (default): nperm phase-randomized surrogates of Y are
drawn. Each surrogate keeps the amplitude spectrum of every column of
Y (hence its autocorrelation) and the cross-spectra among its columns,
but receives new Fourier phases, shared by all columns. Surrogates are
applied jointly to all lags and do not reproduce the observed alignment,
which makes this the recommended scheme for the global p-value. It
assumes a stationary Y and treats the series as periodic with period
n. In simulations, it was liberal for short, strongly autocorrelated
series (AR(1) of 0.7 or more with n of 48 or less).
"cyclic": the full Y series is cyclically shifted by
1, ..., n - 1 positions before lagging, and optionally also
time-reversed (mirror = TRUE). This keeps the temporal autocorrelation
of Y under the null. All n - 1 (or 2(n - 1)) shifts are enumerated,
so nperm and seed are ignored, and the smallest attainable p-value is
1 / (2(n - 1) + 1) (0.043 for n = 12 with mirror = TRUE). The scheme
assumes the series spans complete cycles, i.e., that its period matches
the number of rows (e.g., 12 monthly samples of an annual cycle).
Because a shift of Y combined with lag k is equivalent to another
lag, some shifts reproduce the observed alignment at other lags; the
global p-value (see below) is then valid but has low power.
"free": rows of Y are permuted at random within the overlapping
window, as in vegan::protest(). This assumes exchangeable rows and can
strongly inflate Type I error when both series are autocorrelated.
Both the phase and the cyclic schemes treat the series as periodic with
period n. When the series contains a seasonal cycle whose length does
not divide n (e.g., 18 monthly samples of an annual cycle), no scheme is
calibrated, because two series sharing a period are Procrustes-concordant
whatever their phase. Supplying period truncates both series to their
first floor(n / period) * period rows, so that the analysis spans whole
cycles. In simulations, this restored the Type I error of series that
already spanned complete cycles, whereas removing the seasonal component
by harmonic regression did not.
P-values are computed as (b + 1) / (N + 1), where b is the number of
permuted values smaller than or equal to the observed one and
N the number of permutations. Each permutation is applied jointly to all
lags.
The per-lag p-values (p.value) are not corrected for testing several
lags, nor for selecting the lag with the smallest . Reporting the
raw p-value at the optimal lag therefore inflates Type I error roughly to
the rate at which any lag is significant. The global p-value (p.global)
accounts for both: it compares the observed minimum across lags
with the distribution of the minimum across lags under each permutation
(a max-statistic test), so it is the p-value of the whole
scan-and-select procedure and is the one to report for the optimal lag.
With p.adjust.method = "maxT", every lag receives the corresponding
single-step max-statistic adjusted p-value, and the adjusted p-value at
the optimal lag equals p.global. Any method of stats::p.adjust() can be
requested instead; with few permutations, as in the cyclic scheme,
corrections such as Holm may make significance unattainable. Note that
values at different lags are based on different numbers of rows
(n - |k|).
An object of class "lagged_protest", a list with:
optimal_lagLag with the smallest .
m2 at the optimal lag.
rProcrustes correlation, , at the
optimal lag.
p.valueRaw permutation p-value at the optimal lag.
p.adjustedP-value at the optimal lag adjusted with
p.adjust.method (equal to p.value when "none").
p.globalMax-statistic p-value of the scan across lags, accounting for testing several lags and selecting the optimal one.
n_permNumber of permutations or surrogates at the optimal lag (the exact count for the cyclic scheme).
profileData frame with lag, n (rows used), m2, r,
p.value, p.adjusted and nperm for every lag.
residualsData frame of per-observation Procrustes residuals
at the optimal lag, with the paired row labels of X and Y.
procrustesThe vegan::procrustes() fit at the optimal lag.
nullList of permuted values, one element per lag;
element i of every vector comes from the same permutation.
permutation, p.adjust.method, n, period, call
Settings
used; n is the number of rows after any truncation to whole
cycles.
vegan::protest(), vegan::procrustes()
sim <- simulate_lagged_series(n_times = 12, true_lag = 2, noise_sd = 0.3, seed = 1) fit <- lagged_protest(sim$X, sim$Y, lags = 0:4, seed = 1) fit fit$p.global fit_cyclic <- lagged_protest(sim$X, sim$Y, lags = 0:4, permutation = "cyclic") fit_cyclic$profile plot(fit) residuals(fit)sim <- simulate_lagged_series(n_times = 12, true_lag = 2, noise_sd = 0.3, seed = 1) fit <- lagged_protest(sim$X, sim$Y, lags = 0:4, seed = 1) fit fit$p.global fit_cyclic <- lagged_protest(sim$X, sim$Y, lags = 0:4, permutation = "cyclic") fit_cyclic$profile plot(fit) residuals(fit)
Plots against lag. The dashed line is the alpha quantile of
the permutation null distribution at each lag (unadjusted); observed
values below it are significant at that level before correction. Filled
points mark lags whose p-value (adjusted, if p.adjust.method was not
"none") is below alpha.
## S3 method for class 'lagged_protest' plot(x, alpha = 0.05, ...)## S3 method for class 'lagged_protest' plot(x, alpha = 0.05, ...)
x |
A |
alpha |
Significance level. |
... |
Further arguments passed to |
x, invisibly.
Per-observation Procrustes residuals at the optimal lag
## S3 method for class 'lagged_protest' residuals(object, ...)## S3 method for class 'lagged_protest' residuals(object, ...)
object |
A |
... |
Unused. |
A data frame with the paired row labels of X and Y and the
residual of each pair after superimposition.
Generates X as a multivariate seasonal series (sinusoids with a random
common phase and staggered offsets among variables) plus AR(1) noise.
Under the null (true_lag = NULL), Y is an independent draw from the
same generator. Otherwise Y at time t is a linear map of X at time
t - true_lag plus AR(1) noise; X is generated over n_times + true_lag
points so that every row of Y has a source in X.
simulate_lagged_series( n_times = 24, n_vars_x = 5, n_vars_y = 5, true_lag = NULL, period = 12, amplitude = 1, autocorr = 0.5, noise_sd = 1, seed = NULL )simulate_lagged_series( n_times = 24, n_vars_x = 5, n_vars_y = 5, true_lag = NULL, period = 12, amplitude = 1, autocorr = 0.5, noise_sd = 1, seed = NULL )
n_times |
Number of time points. |
n_vars_x, n_vars_y
|
Number of variables in |
true_lag |
|
period |
Length of the seasonal cycle, in time steps. |
amplitude |
Amplitude of the seasonal sinusoids. |
autocorr |
AR(1) coefficient of the noise, in |
noise_sd |
Marginal standard deviation of the AR(1) noise. |
seed |
Optional integer seed. The global random number stream is left untouched. |
Setting amplitude = 0 removes the seasonal component and leaves pure
AR(1) series; setting autocorr = 0 gives white noise around the
seasonal signal (the design of the original simulation, Appendix S1).
A list with matrices X (n_times x n_vars_x) and Y
(n_times x n_vars_y), and true_lag.
null_pair <- simulate_lagged_series(n_times = 12, seed = 42) lagged_pair <- simulate_lagged_series(n_times = 24, true_lag = 3, seed = 42) str(lagged_pair)null_pair <- simulate_lagged_series(n_times = 12, seed = 42) lagged_pair <- simulate_lagged_series(n_times = 24, true_lag = 3, seed = 42) str(lagged_pair)
Summarize a lagged PROTEST
## S3 method for class 'lagged_protest' summary(object, ...)## S3 method for class 'lagged_protest' summary(object, ...)
object |
A |
... |
Unused. |
The lag profile (a data frame), with the optimal lag flagged.