Recurrence Quantification Analysis with crqa — a guided tour of v2.1.0

Overview

crqa (Coco & Dale 2014; Coco et al. 2021) is the primary R package for recurrence quantification analysis (RQA) on behavioural, physiological, and linguistic time series. It provides:

Performance in context (v2.1.0)

Version 2.1.0 replaces the legacy spdiags-based pipeline with a fused Rcpp inner loop — meaning distance computation, threshold comparison, and line-statistic collection are merged into a single pass that never materialises the N×N distance matrix — and adds OpenMP parallelism on top. Figure 1 plots wall time vs. N on the Rössler-system benchmark of Marwan & Kraemer (2023), the standard reference for RQA package comparisons (m = 3, τ = 6, Euclidean norm, dense ~2 % recurrence).

Figure 1. Wall time per call for crqa v2.1.0 vs. other RQA implementations on the Rössler-system benchmark (Marwan & Kraemer 2023). Filled markers: measured on this hardware (4-core Ryzen-class CPU). Open markers: reference levels from Marwan & Kraemer 2023 Fig 3A. crqa v2.1.0 uses the fused C++ kernel with OpenMP enabled (default); thread count = all available cores. AccRQA 1.0.1 (Adámek et al. 2026) measured single-threaded — their library also offers OpenMP and CUDA paths not benchmarked here. The aRQA path is a fundamentally different algorithm (phase-space histogram) with O(N) memory and scales to N = 200 000 in seconds.

Figure 1. Wall time per call for crqa v2.1.0 vs. other RQA implementations on the Rössler-system benchmark (Marwan & Kraemer 2023). Filled markers: measured on this hardware (4-core Ryzen-class CPU). Open markers: reference levels from Marwan & Kraemer 2023 Fig 3A. crqa v2.1.0 uses the fused C++ kernel with OpenMP enabled (default); thread count = all available cores. AccRQA 1.0.1 (Adámek et al. 2026) measured single-threaded — their library also offers OpenMP and CUDA paths not benchmarked here. The aRQA path is a fundamentally different algorithm (phase-space histogram) with O(N) memory and scales to N = 200 000 in seconds.

The figure summarises crqa’s competitive position:

The two libraries are complementary: AccRQA is a GPU-aware kernel library for vanilla auto-RQA on long single time series; crqa is the R-native analytical toolbox for the full range of recurrence analyses used in behavioural, cognitive and linguistic research.

By default, crqa uses all available CPU cores transparently — you do not need to configure anything. The only situation in which this matters is if you are running many crqa() calls in parallel yourself (for example via the workers argument of wincrqa()), in which case see the “Parallelism: advanced control” section below.


Installation

install.packages("crqa")          # CRAN release
# devtools::install_github("morenococo/crqa")  # development version

Core concepts and the crqa() function

All analyses go through crqa() or one of its wrappers. The key arguments:

Argument Role Default
ts1, ts2 Time series (vectors or matrices)
delay Embedding delay τ 1
embed Embedding dimension m 1
radius Recurrence threshold ε 0.001
rescale Rescale distance matrix (0–4) 0
normalize Normalize series (0 = none, 1 = unit, 2 = z) 0
tw Theiler window 0
mindiagline Min diagonal-line length 2
minvertline Min vertical-line length 2
side "both", "upper", or "lower" "both"
method "rqa", "crqa", "mdcrqa", "aRQA" "rqa"
whiteline Compute white-line statistics? FALSE
rr_denom RR denominator: "full" (v2.0.7 compat.) or "valid" "full"
workers Parallel workers (wrappers only) 1

Return value

crqa() returns a named list:


Example 1 — Auto-recurrence on categorical data

A nursery rhyme (“The Wheels on the Bus”) encoded as a word vector. With categorical data set radius small so only identical tokens recur.

data(crqa)

ans_rqa <- crqa(text, text,
                delay = 1, embed = 1, rescale = 0, radius = 0.0001,
                normalize = 0, mindiagline = 2, minvertline = 2,
                tw = 1, whiteline = FALSE, recpt = FALSE,
                side = "both", method = "rqa",
                metric = "euclidean", datatype = "categorical")
#> Warning in crqa(text, text, delay = 1, embed = 1, rescale = 0, radius = 1e-04,
#> : Your input data was provided either as character or factor, and recoded as
#> numerical identifiers

# Print all scalar measures (exclude the RP itself)
print(ans_rqa[!names(ans_rqa) %in% "RP"])
#> $RR
#> [1] 7.388889
#> 
#> $DET
#> [1] 52.63158
#> 
#> $NRLINE
#> [1] 140
#> 
#> $maxL
#> [1] 9
#> 
#> $L
#> [1] 4
#> 
#> $ENTR
#> [1] 1.523154
#> 
#> $rENTR
#> [1] 0.7324821
#> 
#> $LAM
#> [1] 14.28571
#> 
#> $TT
#> [1] 3.8
#> 
#> $catH
#> [1] 1.098612
#> 
#> $max_vertlength
#> [1] 9
#> 
#> $wmean
#> [1] NA
#> 
#> $wmax
#> [1] NA
#> 
#> $wENTR
#> [1] NA

Visualising the recurrence plot

plot_rp(ans_rqa$RP,
        title = "Auto-RP: Wheels on the Bus",
        xlabel = "Word index", ylabel = "Word index")


Example 2 — Cross-recurrence on categorical data

Eye-tracking data: narrator and listener looking at six screen locations during a joint description task. Cross-recurrence quantifies how temporally coupled their gaze patterns are.

listener <- eyemovement$listener
narrator <- eyemovement$narrator

ans_crqa <- crqa(narrator, listener,
                 delay = 1, embed = 1, rescale = 0, radius = 0.01,
                 normalize = 0, mindiagline = 2, minvertline = 2,
                 tw = 0, whiteline = FALSE, recpt = FALSE,
                 side = "both", method = "crqa",
                 metric = "euclidean", datatype = "categorical")

cat(sprintf("RR = %.2f%%   DET = %.2f%%   LAM = %.2f%%\n",
            ans_crqa$RR, ans_crqa$DET, ans_crqa$LAM))
#> RR = 12.52%   DET = 98.96%   LAM = 99.73%

White-line statistics (new in v2.1.0)

White vertical lines are gaps between recurrent stretches in the same column. Their distribution approximates the inter-recurrence time distribution; wENTR is the recurrence-time entropy (Faure & Korn 2004). Enabling whiteline = TRUE costs essentially nothing — the scan runs in the same sparse-index pass as the black-line statistics.

ans_wl <- crqa(narrator, narrator,
               delay = 1, embed = 1, rescale = 0, radius = 0.001,
               normalize = 0, mindiagline = 2, minvertline = 2,
               tw = 0, whiteline = TRUE, recpt = FALSE,
               side = "both", method = "rqa",
               metric = "euclidean", datatype = "categorical")

cat(sprintf("wmean = %.2f   wmax = %d   wENTR = %.3f\n",
            ans_wl$wmean, ans_wl$wmax, ans_wl$wENTR))
#> wmean = 86.72   wmax = 638   wENTR = 3.939

Example 3 — Multidimensional cross-recurrence (mdCRQA)

Hand-movement data from a joint LEGO construction task: two participants’ wrist positions in two spatial dimensions.


P1 <- cbind(handmovement$P1_TT_d, handmovement$P1_TT_n)
P2 <- cbind(handmovement$P2_TT_d, handmovement$P2_TT_n)

ans_md <- crqa(P1, P2,
               delay = 5, embed = 2, rescale = 0, radius = 0.3,
               normalize = 0, mindiagline = 2, minvertline = 2,
               tw = 0, whiteline = FALSE, recpt = FALSE,
               side = "both", method = "mdcrqa",
               metric = "euclidean", datatype = "continuous")

cat(sprintf("RR = %.2f%%   DET = %.2f%%   LAM = %.2f%%\n",
            ans_md$RR, ans_md$DET, ans_md$LAM))
#> RR = 54.37%   DET = 90.45%   LAM = 95.02%

Example 4 — Diagonal recurrence profile (leader–follower)

drpfromts() extracts the diagonal cross-recurrence profile: for each lag δ, it reports the mean recurrence along the diagonal at that offset. The peak lag identifies the temporal lead of one series over the other.

res <- drpfromts(narrator, listener,
                 windowsize = 100,
                 radius = 0.001, delay = 1, embed = 1,
                 rescale = 0, normalize = 0,
                 mindiagline = 2, minvertline = 2,
                 tw = 0, whiteline = FALSE, recpt = FALSE,
                 side = "both", method = "crqa",
                 metric = "euclidean", datatype = "categorical")

timecourse <- seq_along(res$profile) - ceiling(length(res$profile) / 2)
plot(timecourse, res$profile * 100,
     type = "l", lwd = 2,
     xlab = "Lag (samples)", ylab = "Recurrence rate (%)",
     main = "Diagonal cross-recurrence profile")
abline(v = res$maxlag - ceiling(length(res$profile) / 2),
       col = "red", lty = 2)


Example 5 — Windowed RQA (time-varying dynamics)

wincrqa() slides a window over the time series and returns one row of RQA measures per window — useful for tracking how coupling evolves.

win_res <- wincrqa(narrator, listener,
                   windowstep = 20, windowsize = 80,
                   delay = 1, embed = 1,
                   radius = 0.001, rescale = 0, normalize = 0,
                   mindiagline = 2, minvertline = 2,
                   tw = 0, whiteline = FALSE, recpt = FALSE,
                   side = "both", method = "crqa",
                   metric = "euclidean", datatype = "categorical",
                   trend = FALSE, workers = 1L)

plot(win_res$win, win_res$RR,
     type = "l", lwd = 2,
     xlab = "Window", ylab = "RR (%)",
     main = "Windowed RR (narrator-listener gaze coupling)")

For very long series with many windows, parallel execution is available via workers > 1:

win_par <- wincrqa(narrator, listener,
                   windowstep = 20, windowsize = 80,
                   delay = 1, embed = 1, radius = 0.001,
                   workers = 4L)   # spread windows across 4 CPU cores

Worker spawn has a fixed overhead of a few seconds; use workers > 1 only when individual windows are themselves expensive (long series or many windows). See the “Parallelism: advanced control” section below for the one configuration step needed when combining workers > 1 with crqa’s built-in OpenMP parallelism.


Example 6 — Approximative RQA for very long series (new in v2.1.0)

For N > ~10 000, the exact methods run out of memory (O(N²) peak RAM). method = "aRQA" implements the phase-space histogram approach of Schultz, Spiegel, Marwan & Albayrak (2015, Phys. Lett. A 379:997) and runs in O(N) memory. The method was developed and validated in the literature for auto-recurrence on a single series (see example below); the implementation also accepts two distinct series (cross-RQA) and matrix inputs (multidimensional), but the approximation has only been benchmarked for the auto-RQA case. It is available directly via crqa():

# Simulate 5000 points of AR(1) data as a stand-in for a long series
set.seed(42)
x <- as.numeric(arima.sim(model = list(ar = 0.7), n = 5000))

res_arqa <- crqa(x, x,
                 delay = 1, embed = 3, radius = 0.5,
                 normalize = 2, mindiagline = 2,
                 method = "aRQA")

cat(sprintf("aRQA:  RR = %.3f%%   DET = %.2f%%   L = %.2f\n",
            res_arqa$RR, res_arqa$DET, res_arqa$L))
#> aRQA:  RR = 0.531%   DET = 36.67%   L = 2.48

When to use aRQA: for stochastic or weakly-deterministic data with N > 10 000 where exact computation is infeasible. The approximation error on DET is a few percentage points for stochastic data and larger for strongly deterministic systems (see the help page ?aRQA for details). For comparison at moderate N:

# Exact crqa for the same series at N = 1000 (comparable parameter set)
x_short <- x[1:1000]

res_exact <- crqa(x_short, x_short,
                  delay = 1, embed = 3, radius = 0.5,
                  normalize = 2, mindiagline = 2,
                  method = "rqa")

res_approx <- crqa(x_short, x_short,
                   delay = 1, embed = 3, radius = 0.5,
                   normalize = 2, mindiagline = 2,
                   method = "aRQA")

cat(sprintf("Exact:  RR = %.3f%%  DET = %.2f%%\n", res_exact$RR,  res_exact$DET))
cat(sprintf("aRQA:   RR = %.3f%%  DET = %.2f%%\n", res_approx$RR, res_approx$DET))

Example 7 — Parameter optimisation

optimizeParam() uses average mutual information (AMI) for delay, false nearest neighbours (FNN) for embedding dimension, and binary search for the radius that yields a target recurrence rate.

data(crqa)

P1 <- cbind(handmovement$P1_TT_d, handmovement$P1_TT_n)
P2 <- cbind(handmovement$P2_TT_d, handmovement$P2_TT_n)

par <- list(method = "mdcrqa", metric = "euclidean",
            maxlag = 20, radiusspan = 100, normalize = 0,
            rescale = 4, mindiagline = 10, minvertline = 10,
            tw = 0, whiteline = FALSE, recpt = FALSE,
            side = "both", datatype = "continuous",
            fnnpercent = NA, typeami = NA,
            nbins = 50, criterion = "firstBelow",
            threshold = 1, maxEmb = 20, numSamples = 500,
            Rtol = 10, Atol = 2)

results <- optimizeParam(P1, P2, par, min.rec = 2, max.rec = 5)
print(unlist(results))

Note on the rr_denom argument (v2.1.0)

Version 2.1.0 introduces the rr_denom argument with two options:

When tw == 0 and side == "both" (the most common case) both conventions give identical results.

r_valid <- crqa(narrator, listener,
                delay = 1, embed = 1, rescale = 0, radius = 0.001,
                normalize = 0, mindiagline = 2, minvertline = 2,
                tw = 2, side = "both", method = "crqa",
                metric = "euclidean", datatype = "categorical",
                rr_denom = "valid")

r_full <- crqa(narrator, listener,
               delay = 1, embed = 1, rescale = 0, radius = 0.001,
               normalize = 0, mindiagline = 2, minvertline = 2,
               tw = 2, side = "both", method = "crqa",
               metric = "euclidean", datatype = "categorical",
               rr_denom = "full")

cat(sprintf("rr_denom='valid': RR = %.4f%%\n", r_valid$RR))
#> rr_denom='valid': RR = 12.4996%
cat(sprintf("rr_denom='full':  RR = %.4f%%  (v2.0.7 convention)\n", r_full$RR))
#> rr_denom='full':  RR = 12.4808%  (v2.0.7 convention)

Parallelism: advanced control

Most users can ignore this section — by default, crqa() uses all available CPU cores and that is the right choice almost all the time.

There is exactly one situation where you may need to take action: when you are spawning multiple R workers yourself (e.g. via wincrqa(..., workers = 4)) and each worker is also calling crqa(). In that case the inner OpenMP parallelism multiplies with the outer worker count and can overload the CPU. To avoid this, set OMP_NUM_THREADS=1 in the shell before launching R, so each worker runs serially and the parallelism comes entirely from workers:

$ OMP_NUM_THREADS=1 R    # then run wincrqa(..., workers = 4) inside R

Setting it via Sys.setenv() from inside R is too late on most systems — the OpenMP thread count is cached the first time crqa() is called.

To run crqa strictly single-threaded for reproducibility, use the same mechanism: launch R with OMP_NUM_THREADS=1.


References

Adámek, K., Novotný, J., Marwan, N., & Pánis, R. (2026). AccRQA library: accelerating recurrence quantification analysis. European Physical Journal Special Topics. https://doi.org/10.1140/epjs/s11734-026-02338-3

Coco, M. I., & Dale, R. (2014). Cross-recurrence quantification analysis of categorical and continuous time series: An R package. Frontiers in Psychology, 5, 510.

Coco, M. I., et al. (2021). crqa: Recurrence quantification analysis for categorical and continuous time series in R. The R Journal, 13, 83–97.

Faure, P., & Korn, H. (2004). A new method to estimate the Kolmogorov entropy from recurrence plots. Physics Letters A, 332, 329–339.

Marwan, N., Romano, M. C., Thiel, M., & Kurths, J. (2007). Recurrence plots for the analysis of complex systems. Physics Reports, 438, 237–329.

Marwan, N., & Kraemer, K. H. (2023). Trends in recurrence analysis of dynamical systems. European Physical Journal Special Topics, 232, 5–27.

Schultz, D., Spiegel, S., Marwan, N., & Albayrak, S. (2015). Approximation of diagonal line based measures in recurrence quantification analysis. Physics Letters A, 379, 997–1011.

Wallot, S. (2018). Multidimensional cross-recurrence quantification analysis (MdCRQA). Multivariate Behavioral Research, 1–19.