An Elixir implementation of the Ljung-Box test for autocorrelation in time series residuals.

Overview

The Ljung-Box test checks whether the residuals of a time series model (or any sequence of values) are independently distributed — i.e., whether they exhibit no autocorrelation up to a chosen number of lags.

Null hypothesis H₀: The data are independently distributed (white noise).
Alternative H₁: The data exhibit serial correlation at one or more lags.

Method

The Q-statistic is:

$$ Q = n(n+2)\sum_{k=1}^{h}\frac{\hat{\rho}_k^2}{n-k} $$

SymbolMeaning
$n$Number of observations
$h$Number of lags tested
$\hat{\rho}_k$Sample autocorrelation at lag $k$

Under H₀, $Q$ follows a chi-squared distribution with $h$ degrees of freedom. When testing ARMA(p, q) residuals, use $h - p - q$ degrees of freedom instead and adjust the lags count accordingly.

The p-value is computed using the regularised lower incomplete gamma function (Lanczos + Lentz continued-fraction method, pure Elixir, no FFI).

Interpretation of results

p-valueConclusion
< 0.05Reject H₀ — significant autocorrelation is present
≥ 0.05Fail to reject H₀ — series is consistent with white noise

Practical tips:

  • For ARIMA residuals the conventional lag choices are $h = \lfloor\ln n\rfloor$ or $h = 10$ (for non-seasonal data).
  • Use a higher lag count to detect slowly-decaying autocorrelation patterns.
  • The test has low power for very short series (n < 30); interpret with caution.
  • A series with structural breaks or heteroscedasticity can produce misleading results; combine with other diagnostics when in doubt.

Installation

Add ljung_box to your list of dependencies in mix.exs:

def deps do
  [
    {:ljung_box, "~> 0.1.0"}
  ]
end

Usage

Full test

# residuals from an ARIMA model
residuals = [0.12, -0.05, 0.03, 0.21, -0.18, 0.07, -0.02, 0.11, ...]

result = LjungBox.test(residuals, 10)
# %{statistic: 8.43, p_value: 0.587, lags: 10, n: 200}

if result.p_value < 0.05 do
  IO.puts("Residuals show significant autocorrelation — revisit the model.")
else
  IO.puts("Residuals are consistent with white noise.")
end

Q-statistic only

LjungBox.statistic([1, 2, 3, 4], 2)
# => 1.58

Autocorrelations

# Individual autocorrelation at a specific lag
LjungBox.autocorrelation([1, 2, 3, 4], 1)
# => 0.25

# All autocorrelations up to max_lag
LjungBox.autocorrelations([1, 2, 3, 4, 5, 6], 4)
# => [rho_1, rho_2, rho_3, rho_4]

API

FunctionDescription
LjungBox.test(series, lags \\ 10)Full test — returns %{statistic, p_value, lags, n}
LjungBox.statistic(series, lags)Q-statistic only
LjungBox.autocorrelation(series, lag)Sample autocorrelation at a single lag
LjungBox.autocorrelations(series, max_lag)Sample autocorrelations at lags 1..max_lag

References

  • Ljung, G. M. & Box, G. E. P. (1978). On a measure of lack of fit in time series models. Biometrika, 65(2), 297–303.
  • Box, G. E. P. & Pierce, D. A. (1970). Distribution of residual autocorrelations in autoregressive-integrated moving average time series models. Journal of the American Statistical Association, 65(332), 1509–1526.