Time Series Analysis
The smile.timeseries package models and forecasts univariate time series. It supplies diagnostics and transformations alongside autoregressive and autoregressive moving-average models.
Java examples call the static factories on TimeSeries, AR, ARMA, and BoxTest. Scala and Kotlin prefer package helpers such as diff, diffStages, acf, ar, and ljungBox. Financial examples start from smile.datasets.BitcoinPrice, take its log closing prices, and difference them into log returns.
| Class | Purpose |
|---|---|
TimeSeries |
Differencing, autocovariance, ACF, and PACF utilities |
BoxTest |
Box–Pierce and Ljung–Box portmanteau tests |
AR |
AR(p) models fitted by Yule–Walker or OLS |
ARMA |
ARMA(p, q) models fitted by Hannan–Rissanen |
Foundations
Stationarity
These models assume weak stationarity: a time-invariant mean and autocovariance. Trending prices and seasonal series usually violate that assumption. Differencing removes polynomial trends, while a log transform often stabilizes variance. For asset prices, log(pt) − log(pt−1) is a common stationary starting point.
AR, MA, and ARMA
| Model | Definition | Diagnostic pattern |
|---|---|---|
| AR(p) | xt = b + Σ φixt−i + εt | PACF cuts off; ACF tails off |
| MA(q) | xt = b + εt + Σ θiεt−i | ACF cuts off; PACF tails off |
| ARMA(p, q) | Combines AR and MA terms | Both ACF and PACF tail off |
Here εt is white noise with variance σ² and b is the intercept. ACF/PACF patterns are order-selection heuristics, not proof; compare candidate models out of sample when possible.
TimeSeries Utilities
TimeSeries is an interface containing static methods; it is never instantiated. diff(x, lag) shortens a series by the lag. The three-argument overload performs repeated differencing and returns each intermediate series.
import java.util.Arrays
import smile.timeseries.TimeSeries
double[] x = {1, 2, 4, 8, 16}
double[][] d = TimeSeries.diff(x, 1, 2)
System.out.println(Arrays.toString(d[0]))
System.out.println(Arrays.toString(d[1]))
System.out.printf("cov1=%.2f acf1=%.4f pacf2=%.4f%n",
TimeSeries.cov(x, 1), TimeSeries.acf(x, 1), TimeSeries.pacf(x, 2))
[1.0, 2.0, 4.0, 8.0]
[1.0, 2.0, 4.0]
import java.util.Arrays
import smile.timeseries._
val x = Array(1.0, 2.0, 4.0, 8.0, 16.0)
val d = diffStages(x, 1, 2)
println(Arrays.toString(d(0)))
println(Arrays.toString(d(1)))
println(f"cov1=${cov(x, 1)}%.2f " +
f"acf1=${acf(x, 1)}%.4f pacf2=${pacf(x, 2)}%.4f")
[1.0, 2.0, 4.0, 8.0]
[1.0, 2.0, 4.0]
import java.util.Arrays
import smile.timeseries.*
val x = doubleArrayOf(1.0, 2.0, 4.0, 8.0, 16.0)
val d = x.diffStages(1, 2)
println(Arrays.toString(d[0]))
println(Arrays.toString(d[1]))
println("cov1=%.2f acf1=%.4f pacf2=%.4f".format(
x.cov(1), x.acf(1), x.pacf(2)))
[1.0, 2.0, 4.0, 8.0]
[1.0, 2.0, 4.0]
BoxTest: Residual Diagnostics
Portmanteau tests ask whether the first several autocorrelations are jointly zero. pierce computes Q = nΣr²; ljung applies the small-sample correction n(n+2)Σr²/(n−lag) and is usually preferred for short series.
Read type, df, q, and pvalue directly. A large p-value means the data do not provide evidence against the white-noise null; it does not prove independence.
import smile.timeseries.BoxTest
double[] residuals = {
0.12, -0.08, 0.03, 0.14, -0.11, 0.02, -0.04, 0.09,
-0.06, 0.01, 0.05, -0.10, 0.07, -0.02, 0.04, -0.03
}
BoxTest pierce = BoxTest.pierce(residuals, 5)
BoxTest ljung = BoxTest.ljung(residuals, 5)
System.out.println(pierce)
System.out.println(ljung)
System.out.println(ljung.pvalue > 0.05)
import smile.timeseries._
val residuals = Array(
0.12, -0.08, 0.03, 0.14, -0.11, 0.02, -0.04, 0.09,
-0.06, 0.01, 0.05, -0.10, 0.07, -0.02, 0.04, -0.03)
val pierce = boxPierce(residuals, 5)
val ljung = ljungBox(residuals, 5)
println(pierce)
println(ljung)
println(ljung.pvalue > 0.05)
import smile.timeseries.*
val residuals = doubleArrayOf(
0.12, -0.08, 0.03, 0.14, -0.11, 0.02, -0.04, 0.09,
-0.06, 0.01, 0.05, -0.10, 0.07, -0.02, 0.04, -0.03)
val pierce = residuals.boxPierce(5)
val ljung = residuals.ljungBox(5)
println(pierce)
println(ljung)
println(ljung.pvalue > 0.05)
AR: Autoregressive Models
Yule–Walker vs OLS
AR.fit(x, p) uses Yule–Walker: it is fast, depends on the ACF, and guarantees a stationary fitted model. AR.ols(x, p) uses least squares and normally provides more accurate coefficients plus standard errors, but does not guarantee stationarity. AR.ols(x, p, false) skips standard errors when only point estimates are needed.
Bitcoin Log Returns
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries.AR
import smile.timeseries.BoxTest
import smile.timeseries.TimeSeries
var bitcoin = new BitcoinPrice()
double[] returns = TimeSeries.diff(bitcoin.logPrice(), 1)
AR yw = AR.fit(returns, 6)
AR ols = AR.ols(returns, 6)
System.out.printf("p=%d intercept=%g R2=%g%n",
ols.p(), ols.intercept(), ols.R2())
System.out.println(Arrays.toString(ols.ar()))
System.out.println(Arrays.toString(ols.forecast(5)))
System.out.println(BoxTest.ljung(ols.residuals(), 20))
System.out.println(yw.ttest() == null)
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries._
val bitcoin = new BitcoinPrice()
val returns = diff(bitcoin.logPrice, 1)
val yw = ar(returns, 6)
val ols = AR.ols(returns, 6)
println(s"p=${ols.p} intercept=${ols.intercept} R2=${ols.R2}")
println(Arrays.toString(ols.ar))
println(Arrays.toString(ols.forecast(5)))
println(ljungBox(ols.residuals, 20))
println(yw.ttest == null)
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries.*
val bitcoin = BitcoinPrice()
val returns = bitcoin.logPrice().diff(1)
val yw = ar(returns, 6)
val ols = AR.ols(returns, 6)
println("p=${ols.p()} intercept=${ols.intercept()} R2=${ols.R2()}")
println(Arrays.toString(ols.ar()))
println(Arrays.toString(ols.forecast(5)))
println(ols.residuals().ljungBox(20))
println(yw.ttest() == null)
Inspect PACF lags before choosing p. Multi-step forecasts feed each prediction back as a lagged value and therefore tend toward the model’s long-run mean. For OLS, ttest() has one row per AR coefficient; the intercept is not included.
ARMA: Autoregressive Moving-Average Models
ARMA.fit(x, p, q) uses the two-stage Hannan–Rissanen procedure: first fit a long AR model to obtain proxy residuals, then regress on the requested AR and MA lags with SVD-based least squares. The MA terms can capture short-memory dependence that would otherwise require a high-order AR model.
The series must satisfy x.length > p + q + 20 + max(p, q). Accessors mirror AR, with additional q() and ma(). ttest() has p + q rows: AR rows first, then MA.
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries.ARMA
import smile.timeseries.BoxTest
import smile.timeseries.TimeSeries
double[] returns = TimeSeries.diff(new BitcoinPrice().logPrice(), 1)
ARMA model = ARMA.fit(returns, 6, 3)
System.out.printf("ARMA(%d,%d), variance=%g%n",
model.p(), model.q(), model.variance())
System.out.println(Arrays.toString(model.ar()))
System.out.println(Arrays.toString(model.ma()))
System.out.println(model.forecast())
System.out.println(Arrays.toString(model.forecast(3)))
System.out.println(BoxTest.ljung(model.residuals(), 20))
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries._
val returns = diff(new BitcoinPrice().logPrice, 1)
val model = arma(returns, 6, 3)
println(s"ARMA(${model.p},${model.q}), variance=${model.variance}")
println(Arrays.toString(model.ar))
println(Arrays.toString(model.ma))
println(model.forecast)
println(Arrays.toString(model.forecast(3)))
println(ljungBox(model.residuals, 20))
import java.util.Arrays
import smile.datasets.BitcoinPrice
import smile.timeseries.*
val returns = BitcoinPrice().logPrice().diff(1)
val model = arma(returns, 6, 3)
println("ARMA(${model.p()},${model.q()}), variance=${model.variance()}")
println(Arrays.toString(model.ar()))
println(Arrays.toString(model.ma()))
println(model.forecast())
println(Arrays.toString(model.forecast(3)))
println(model.residuals().ljungBox(20))
A one-step ARMA forecast uses the latest observed values and residuals. Unknown future residuals are assigned their expectation of zero, so the MA contribution disappears after q steps and longer forecasts are driven by the AR recursion.
Modeling Workflow
- Plot the raw series and assess stationarity. Apply logs,
TimeSeries.diff, or seasonal differencing as needed. - Inspect ACF and PACF. Start with small
pandq, then compare plausible candidates. - Use
ARwhen PACF cuts off and ACF tails off; useARMAwhen both tail off and an MA component gives a more parsimonious fit. - For AR, prefer OLS when inference and coefficient accuracy matter. Choose Yule–Walker when a stationary fitted model is required.
- Inspect coefficients and, where available, their t-tests. Validate forecasts out of sample; information criteria are external to this API.
- Run
BoxTest.ljung(model.residuals(), lag). If it rejects white noise, revisit transformations or model order. - Forecast only after diagnostics are satisfactory, and communicate uncertainty separately because these methods return point forecasts.
| AR estimator | Yule–Walker | OLS |
|---|---|---|
| Factory | AR.fit | AR.ols |
| Stationary fitted model | Guaranteed | Not guaranteed |
| Coefficient behavior | Slightly biased | Consistent |
| Standard errors / t-tests | No | Yes, unless disabled |