timeseries.models.arima.Arima Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of java-timeseries Show documentation
Show all versions of java-timeseries Show documentation
Time Series Analysis in Java
The newest version!
/*
* Copyright (c) 2016 Jacob Rachiele
*
* Permission is hereby granted, free of charge, to any person obtaining a copy of this software
* and associated documentation files (the "Software"), to deal in the Software without restriction
* including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense
* and/or sell copies of the Software, and to permit persons to whom the Software is furnished to
* do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in all copies or
* substantial portions of the Software.
*
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED
* INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
* PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
* LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
* TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
* USE OR OTHER DEALINGS IN THE SOFTWARE.
*
* Contributors:
*
* Jacob Rachiele
*/
package timeseries.models.arima;
import data.DoubleFunctions;
import data.Operators;
import linear.doubles.Matrix;
import linear.doubles.Vector;
import lombok.EqualsAndHashCode;
import lombok.ToString;
import math.function.AbstractMultivariateFunction;
import optim.BFGS;
import stats.distributions.Distribution;
import stats.distributions.Normal;
import timeseries.TimePeriod;
import timeseries.TimeSeries;
import timeseries.models.Forecast;
import timeseries.models.Model;
import timeseries.operators.LagPolynomial;
import java.time.OffsetDateTime;
import java.time.ZoneOffset;
import java.util.Arrays;
import static data.DoubleFunctions.slice;
import static data.Operators.differenceOf;
import static stats.Statistics.sumOf;
import static stats.Statistics.sumOfSquared;
/**
* A seasonal autoregressive integrated moving average (ARIMA) model. This class is immutable and thread-safe.
*
* @author Jacob Rachiele
*/
public final class Arima implements Model {
private static final double MACHINE_EPSILON = Math.ulp(1.0);
private static final double DEFAULT_TOLERANCE = Math.sqrt(MACHINE_EPSILON);
private final TimeSeries observations;
private final TimeSeries differencedSeries;
private final TimeSeries fittedSeries;
private final TimeSeries residuals;
private final int seasonalFrequency;
private final ModelOrder order;
private final ModelInformation modelInfo;
private final ModelCoefficients modelCoefficients;
private final double mean;
private final double[] arSarCoeffs;
private final double[] maSmaCoeffs;
private final double[] stdErrors;
private Arima(final TimeSeries observations, final ModelOrder order, final TimePeriod seasonalCycle,
final FittingStrategy fittingStrategy) {
this.observations = observations;
this.order = order;
this.seasonalFrequency = (int) (observations.timePeriod().frequencyPer(seasonalCycle));
this.differencedSeries = observations.difference(1, order.d).difference(seasonalFrequency, order.D);
final double meanParScale = (order.constant == 1) ? 10 * differencedSeries.stdDeviation() /
Math.sqrt(differencedSeries.n()) : 1.0;
final Vector initParams;
final Matrix initHessian;
if (fittingStrategy == FittingStrategy.CSSML || fittingStrategy == FittingStrategy.USSML) {
final FittingStrategy subStrategy = (fittingStrategy == FittingStrategy.CSSML)
? FittingStrategy.CSS
: FittingStrategy.USS;
final Arima firstModel = new Arima(observations, order, seasonalCycle, subStrategy);
initParams = new Vector(firstModel.coefficients().getAllCoeffs());
initHessian = getInitialHessian(firstModel);
} else {
initParams = new Vector(getInitialParameters(meanParScale));
initHessian = getInitialHessian(initParams.size());
}
final AbstractMultivariateFunction function = new OptimFunction(differencedSeries, order, fittingStrategy,
seasonalFrequency, meanParScale);
final BFGS optimizer = new BFGS(function, initParams, DEFAULT_TOLERANCE, DEFAULT_TOLERANCE, initHessian);
final Vector optimizedParams = optimizer.parameters();
final Matrix inverseHessian = optimizer.inverseHessian();
this.stdErrors = DoubleFunctions.sqrt(Operators.scale(inverseHessian.diagonal(), 1.0 / differencedSeries.n()));
if (order.constant == 1) {
this.stdErrors[this.stdErrors.length - 1] *= meanParScale;
}
final double[] arCoeffs = getArCoeffs(optimizedParams);
final double[] maCoeffs = getMaCoeffs(optimizedParams);
final double[] sarCoeffs = getSarCoeffs(optimizedParams);
final double[] smaCoeffs = getSmaCoeffs(optimizedParams);
this.arSarCoeffs = expandArCoefficients(arCoeffs, sarCoeffs, seasonalFrequency);
this.maSmaCoeffs = expandMaCoefficients(maCoeffs, smaCoeffs, seasonalFrequency);
this.mean = (order.constant == 1)
? meanParScale * optimizedParams.at(order.p + order.q + order.P + order.Q)
: 0.0;
this.modelCoefficients = new ModelCoefficients(arCoeffs, maCoeffs, sarCoeffs, smaCoeffs, order.d, order.D,
this.mean);
if (fittingStrategy == FittingStrategy.CSS) {
this.modelInfo = fitCSS(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(), residuals));
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
} else if (fittingStrategy == FittingStrategy.USS) {
this.modelInfo = fitUSS(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(),
slice(residuals, 2 * arSarCoeffs.length,
residuals.length)));
final int diffs = order.d + order.D * seasonalFrequency;
final int offset = 2 * arSarCoeffs.length;
for (int i = 0; i < diffs && i >= (diffs - offset); i++) {
fittedArray[i] -= residuals[offset - diffs + i];
}
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
} else {
this.modelInfo = fitML(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(), residuals));
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
}
}
private Arima(final TimeSeries observations, final ModelCoefficients coeffs, final TimePeriod seasonalCycle,
final FittingStrategy fittingStrategy) {
this.observations = observations;
this.modelCoefficients = coeffs;
this.order = coeffs.extractModelOrder();
this.seasonalFrequency = (int) (observations.timePeriod().frequencyPer(seasonalCycle));
this.differencedSeries = observations.difference(1, order.d).difference(seasonalFrequency, order.D);
this.arSarCoeffs = expandArCoefficients(coeffs.arCoeffs, coeffs.sarCoeffs, seasonalFrequency);
this.maSmaCoeffs = expandMaCoefficients(coeffs.maCoeffs, coeffs.smaCoeffs, seasonalFrequency);
this.mean = coeffs.mean;
this.stdErrors = DoubleFunctions.fill(order.sumARMA() + order.constant, Double.POSITIVE_INFINITY);
if (fittingStrategy == FittingStrategy.CSS) {
this.modelInfo = fitCSS(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(), residuals));
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
} else if (fittingStrategy == FittingStrategy.USS) {
this.modelInfo = fitUSS(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(),
slice(residuals, 2 * arSarCoeffs.length,
residuals.length)));
final int diffs = order.d + order.D * seasonalFrequency;
final int offset = 2 * arSarCoeffs.length;
for (int i = 0; i < diffs && i >= (diffs - offset); i++) {
fittedArray[i] -= residuals[offset - diffs + i];
}
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
} else {
this.modelInfo = fitML(differencedSeries, arSarCoeffs, maSmaCoeffs, mean, order);
final double[] residuals = modelInfo.residuals;
final double[] fittedArray = integrate(differenceOf(differencedSeries.asArray(), residuals));
this.fittedSeries = new TimeSeries(observations.timePeriod(), observations.observationTimes(), fittedArray);
this.residuals = this.observations.minus(this.fittedSeries);
}
}
/**
* Create a new ARIMA model from the given observations and model order. This constructor sets the
* model {@link FittingStrategy} to unconditional sum-of-squares and the seasonal cycle to one year.
*
* @param observations the time series of observations.
* @param order the order of the ARIMA model.
* @return a new ARIMA model from the given observations and model order.
*/
public static Arima model(final TimeSeries observations, final ModelOrder order) {
return new Arima(observations, order, TimePeriod.oneYear(), FittingStrategy.USS);
}
/**
* Create a new ARIMA model from the given observations, model order, and seasonal cycle. This method sets the
* model {@link FittingStrategy} to unconditional sum-of-squares.
*
* @param observations the time series of observations.
* @param order the order of the ARIMA model.
* @param seasonalCycle the amount of time it takes for the seasonal pattern to complete one cycle. For example,
* monthly data usually has a cycle of one year, hourly data a cycle of one day, etc...
* However, a seasonal cycle may be an arbitrary amount of time.
* @return a new ARIMA model from the given observations, model order, and seasonal cycle.
*/
public static Arima model(final TimeSeries observations, final ModelOrder order, final TimePeriod seasonalCycle) {
return new Arima(observations, order, seasonalCycle, FittingStrategy.USS);
}
/**
* Create a new ARIMA model from the given observations, model order, and fitting strategy. This method sets the
* seasonal cycle to one year.
*
* @param observations the time series of observations.
* @param order the order of the ARIMA model.
* @param fittingStrategy the strategy to use to fit the model to the data. Maximum-likelihood estimates are
* typically preferred for greater precision and accuracy, but take longer to obtain than
* conditional or unconditional sum-of-squares estimates. Unconditional sum-of-squares tends
* to be more accurate, more precise, and quicker to obtain than conditional sum-of-squares.
* @return a new ARIMA model from the given observations, model order, and fitting strategy.
*/
public static Arima model(final TimeSeries observations, final ModelOrder order,
final FittingStrategy fittingStrategy) {
return new Arima(observations, order, TimePeriod.oneYear(), fittingStrategy);
}
/**
* Create a new ARIMA model from the given observations, model order, seasonal cycle, and fitting strategy.
*
* @param observations the time series of observations.
* @param order the order of the ARIMA model.
* @param seasonalCycle the amount of time it takes for the seasonal pattern to complete one cycle. For example,
* monthly data usually has a cycle of one year, hourly data a cycle of one day, etc...
* However, a seasonal cycle may be an arbitrary amount of time.
* @param fittingStrategy the strategy to use to fit the model to the data. Maximum-likelihood estimates are
* typically preferred for greater precision and accuracy, but take longer to obtain than
* conditional or unconditional sum-of-squares estimates. Unconditional sum-of-squares tends
* to be more accurate, more precise, and quicker to obtain than conditional sum-of-squares.
* @return a new ARIMA model from the given observations, model order, seasonal cycle, and fitting strategy.
*/
public static Arima model(final TimeSeries observations, final ModelOrder order, final TimePeriod seasonalCycle,
final FittingStrategy fittingStrategy) {
return new Arima(observations, order, seasonalCycle, fittingStrategy);
}
/**
* Create a new ARIMA model from the given observations, model coefficients, and fitting strategy. This constructor
* sets the seasonal cycle to one year.
*
* @param observations the time series of observations.
* @param coeffs the coefficients of the model.
* @param fittingStrategy the strategy to use to fit the model to the data. Maximum-likelihood estimates are
* typically preferred for greater precision and accuracy, but take longer to obtain than
* conditional or unconditional sum-of-squares estimates. Unconditional sum-of-squares tends
* to be more accurate, more precise, and quicker to obtain than conditional sum-of-squares.
* @return a new ARIMA model from the given observations, model coefficients, and fitting strategy.
*/
public static Arima model(final TimeSeries observations, final ModelCoefficients coeffs,
final FittingStrategy fittingStrategy) {
return new Arima(observations, coeffs, TimePeriod.oneYear(), fittingStrategy);
}
/**
* Create a new ARIMA model from the given observations, model coefficients, and seasonal cycle. This constructor
* sets
* the model {@link FittingStrategy} to unconditional sum-of-squares.
*
* @param observations the time series of observations.
* @param coeffs the coefficients of the model.
* @param seasonalCycle the amount of time it takes for the seasonal pattern to complete one cycle. For example,
* monthly data usually has a cycle of one year, hourly data a cycle of one day, etc...
* However, a seasonal cycle may be an arbitrary amount of time.
* @return a new ARIMA model from the given observations, model coefficients, and seasonal cycle.
*/
public static Arima model(final TimeSeries observations, final ModelCoefficients coeffs,
final TimePeriod seasonalCycle) {
return new Arima(observations, coeffs, seasonalCycle, FittingStrategy.USS);
}
/**
* Create a new ARIMA model from the given observations, model coefficients, seasonal cycle, and fitting strategy.
*
* @param observations the time series of observations.
* @param coeffs the coefficients of the model.
* @param seasonalCycle the amount of time it takes for the seasonal pattern to complete one cycle. For example,
* monthly data usually has a cycle of one year, hourly data a cycle of one day, etc...
* However, a seasonal cycle may be an arbitrary amount of time.
* @param fittingStrategy the strategy to use to fit the model to the data. Maximum-likelihood estimates are
* typically preferred for greater precision and accuracy, but take longer to obtain than
* conditional or unconditional sum-of-squares estimates. Unconditional sum-of-squares tends
* to be more accurate, more precise, and quicker to obtain than conditional sum-of-squares.
* @return a new ARIMA model from the given observations, model coefficients, seasonal cycle, and fitting strategy.
*/
public static Arima model(final TimeSeries observations, final ModelCoefficients coeffs,
final TimePeriod seasonalCycle, final FittingStrategy fittingStrategy) {
return new Arima(observations, coeffs, seasonalCycle, fittingStrategy);
}
/**
* Fit an ARIMA model using conditional sum-of-squares.
*
* @param differencedSeries the time series of observations to model.
* @param arCoeffs the autoregressive coefficients of the model.
* @param maCoeffs the moving-average coefficients of the model.
* @param mean the model mean.
* @param order the order of the model to be fit.
* @return information about the fitted model.
*/
private static ModelInformation fitCSS(final TimeSeries differencedSeries, final double[] arCoeffs,
final double[] maCoeffs, final double mean, final ModelOrder order) {
final int offset = arCoeffs.length;
final int n = differencedSeries.n();
final double[] fitted = new double[n];
final double[] residuals = new double[n];
for (int t = offset; t < fitted.length; t++) {
fitted[t] = mean;
for (int i = 0; i < arCoeffs.length; i++) {
fitted[t] += arCoeffs[i] * (differencedSeries.at(t - i - 1) - mean);
}
for (int j = 0; j < Math.min(t, maCoeffs.length); j++) {
fitted[t] += maCoeffs[j] * residuals[t - j - 1];
}
residuals[t] = differencedSeries.at(t) - fitted[t];
}
final int npar = order.sumARMA() + order.constant;
final int m = differencedSeries.n() - npar;
final double sigma2 = sumOfSquared(residuals) / m;
final double logLikelihood = (-n / 2.0) * (Math.log(2 * Math.PI * sigma2) + 1);
return new ModelInformation(npar, sigma2, logLikelihood, residuals, fitted);
}
/**
* Fit the model using unconditional sum-of-squares, utilizing back-forecasting to estimate the residuals for the
* first few observations. This method, compared to conditional sum-of-squares, often gives estimates much closer to
* those obtained from maximum-likelihood fitting, especially for shorter series.
*
* @param differencedSeries the time series of observations to model.
* @param arCoeffs the autoregressive coefficients of the model.
* @param maCoeffs the moving-average coefficients of the model.
* @param mean the model mean.
* @param order the order of the model to be fit.
* @return information about the fitted model.
*/
private static ModelInformation fitUSS(final TimeSeries differencedSeries, final double[] arCoeffs,
final double[] maCoeffs, final double mean, final ModelOrder order) {
int n = differencedSeries.n();
final int m = arCoeffs.length;
final double[] extendedFit = new double[2 * m + n];
final double[] extendedSeries = new double[2 * m + n];
final double[] residuals = new double[2 * m + n];
for (int i = 0; i < differencedSeries.n(); i++) {
extendedSeries[i] = differencedSeries.at(--n);
}
n = differencedSeries.n();
for (int t = n; t < n + 2 * m; t++) {
extendedSeries[t] = mean;
for (int i = 0; i < arCoeffs.length; i++) {
if (Math.abs(arCoeffs[i]) > 0) {
extendedSeries[t] += arCoeffs[i] * (extendedSeries[t - i - 1] - mean);
}
}
}
n = extendedSeries.length;
for (int i = 0; i < m; i++) {
extendedFit[i] = extendedSeries[n - i - 1];
}
for (int t = m; t < n; t++) {
extendedFit[t] = mean;
for (int i = 0; i < arCoeffs.length; i++) {
if (Math.abs(arCoeffs[i]) > 0) {
extendedFit[t] += arCoeffs[i] * (extendedSeries[n - t + i] - mean);
}
}
for (int j = 0; j < Math.min(t, maCoeffs.length); j++) {
if (Math.abs(maCoeffs[j]) > 0) {
extendedFit[t] += maCoeffs[j] * residuals[t - j - 1];
}
}
residuals[t] = extendedSeries[n - t - 1] - extendedFit[t];
}
//n = differencedSeries.n();
n = residuals.length;
final int npar = order.sumARMA() + order.constant;
final double sigma2 = sumOfSquared(residuals) / (n - npar);
final double logLikelihood = (-n / 2.0) * (Math.log(2 * Math.PI * sigma2) + 1);
return new ModelInformation(npar, sigma2, logLikelihood, residuals, extendedFit);
}
private static ModelInformation fitML(final TimeSeries differencedSeries, final double[] arCoeffs,
final double[] maCoeffs, final double mean, final ModelOrder order) {
final double[] series = Operators.subtract(differencedSeries.asArray(), mean);
ArmaKalmanFilter.KalmanOutput output = kalmanFit(differencedSeries, arCoeffs, maCoeffs, mean);
final double sigma2 = output.sigma2();
final double logLikelihood = output.logLikelihood();
final double[] residuals = output.residuals();
final double[] fitted = differenceOf(series, residuals);
final int npar = order.sumARMA() + order.constant + 1; // Add 1 for the variance estimate.
return new ModelInformation(npar, sigma2, logLikelihood, residuals, fitted);
}
private static ArmaKalmanFilter.KalmanOutput kalmanFit(final TimeSeries differencedSeries, final double[] arCoeffs,
final double[] maCoeffs, final double mean) {
final double[] series = Operators.subtract(differencedSeries.asArray(), mean);
StateSpaceARMA ss = new StateSpaceARMA(series, arCoeffs, maCoeffs);
ArmaKalmanFilter kalmanFilter = new ArmaKalmanFilter(ss);
return kalmanFilter.output();
}
/**
* Create and return a new non-seasonal model order with the given number of coefficients. A constant will be fit
* only if d is equal to 0.
*
* @param p the number of non-seasonal autoregressive coefficients.
* @param d the degree of non-seasonal differencing.
* @param q the number of non-seasonal moving-average coefficients.
* @return a new ARIMA model order.
*/
public static ModelOrder order(final int p, final int d, final int q) {
return new ModelOrder(p, d, q, 0, 0, 0, d == 0);
}
/**
* Create and return a new non-seasonal model order with the given number of coefficients and indication of
* whether or not to fit a constant.
*
* @param p the number of non-seasonal autoregressive coefficients.
* @param d the degree of non-seasonal differencing.
* @param q the number of non-seasonal moving-average coefficients.
* @param constant whether or not to fit a constant to the model.
* @return a new ARIMA model order.
*/
public static ModelOrder order(final int p, final int d, final int q, final boolean constant) {
return new ModelOrder(p, d, q, 0, 0, 0, constant);
}
/**
* Create a new ModelOrder using the provided number of autoregressive and moving-average parameters, as well as the
* degrees of differencing. A constant will be fit only if both d and D are equal to 0.
*
* @param p the number of non-seasonal autoregressive coefficients.
* @param d the degree of non-seasonal differencing.
* @param q the number of non-seasonal moving-average coefficients.
* @param P the number of seasonal autoregressive coefficients.
* @param D the degree of seasonal differencing.
* @param Q the number of seasonal moving-average coefficients.
* @return a new ARIMA model order.
*/
public static ModelOrder order(final int p, final int d, final int q, final int P, final int D, final int Q) {
return new ModelOrder(p, d, q, P, D, Q, d == 0 && D == 0);
}
/**
* Create a new ModelOrder using the provided number of autoregressive and moving-average parameters, as well as the
* degrees of differencing and indication of whether or not to fit a constant.
*
* @param p the number of non-seasonal autoregressive coefficients.
* @param d the degree of non-seasonal differencing.
* @param q the number of non-seasonal moving-average coefficients.
* @param P the number of seasonal autoregressive coefficients.
* @param D the degree of seasonal differencing.
* @param Q the number of seasonal moving-average coefficients.
* @param constant determines whether or not a constant is fitted with the model.
* @return a new ARIMA model order.
*/
public static ModelOrder order(final int p, final int d, final int q, final int P, final int D, final int Q,
final boolean constant) {
return new ModelOrder(p, d, q, P, D, Q, constant);
}
// Expand the autoregressive coefficients by combining the non-seasonal and seasonal coefficients into a single
// array, which takes advantage of the fact that a seasonal AR model is a special case of a non-seasonal
// AR model with zero coefficients at the non-seasonal indices.
private static double[] expandArCoefficients(final double[] arCoeffs, final double[] sarCoeffs,
final int seasonalFrequency) {
double[] arSarCoeffs = new double[arCoeffs.length + sarCoeffs.length * seasonalFrequency];
System.arraycopy(arCoeffs, 0, arSarCoeffs, 0, arCoeffs.length);
// Note that we take into account the interaction between the seasonal and non-seasonal coefficients,
// which arises because the model's ar and sar polynomials are multiplied together.
for (int i = 0; i < sarCoeffs.length; i++) {
arSarCoeffs[(i + 1) * seasonalFrequency - 1] = sarCoeffs[i];
for (int j = 0; j < arCoeffs.length; j++) {
arSarCoeffs[(i + 1) * seasonalFrequency + j] = -sarCoeffs[i] * arCoeffs[j];
}
}
return arSarCoeffs;
}
// Expand the moving average coefficients by combining the non-seasonal and seasonal coefficients into a single
// array, which takes advantage of the fact that a seasonal MA model is a special case of a non-seasonal
// MA model with zero coefficients at the non-seasonal indices.
private static double[] expandMaCoefficients(final double[] maCoeffs, final double[] smaCoeffs,
final int seasonalFrequency) {
double[] maSmaCoeffs = new double[maCoeffs.length + smaCoeffs.length * seasonalFrequency];
System.arraycopy(maCoeffs, 0, maSmaCoeffs, 0, maCoeffs.length);
// Note that we take into account the interaction between the seasonal and non-seasonal coefficients,
// which arises because the model's ma and sma polynomials are multiplied together.
// In contrast to the ar polynomial, the ma and sma product maintains a positive sign.
for (int i = 0; i < smaCoeffs.length; i++) {
maSmaCoeffs[(i + 1) * seasonalFrequency - 1] = smaCoeffs[i];
for (int j = 0; j < maCoeffs.length; j++) {
maSmaCoeffs[(i + 1) * seasonalFrequency + j] = smaCoeffs[i] * maCoeffs[j];
}
}
return maSmaCoeffs;
}
/**
* Compute point forecasts for the given number of steps ahead and return the result in a primitive array.
*
* @param steps the number of time periods ahead to forecast.
* @return point forecasts for the given number of steps ahead.
*/
public double[] fcst(final int steps) {
final int d = order.d;
final int D = order.D;
final int n = differencedSeries.n();
final int m = observations.n();
final double[] resid = this.residuals.asArray();
final double[] diffedFcst = new double[n + steps];
final double[] fcst = new double[m + steps];
System.arraycopy(differencedSeries.asArray(), 0, diffedFcst, 0, n);
System.arraycopy(observations.asArray(), 0, fcst, 0, m);
LagPolynomial diffPolynomial = LagPolynomial.differences(d);
LagPolynomial seasDiffPolynomial = LagPolynomial.seasonalDifferences(seasonalFrequency, D);
LagPolynomial lagPolyomial = diffPolynomial.times(seasDiffPolynomial);
for (int t = 0; t < steps; t++) {
diffedFcst[n + t] = mean;
fcst[m + t] = mean;
fcst[m + t] += lagPolyomial.fit(fcst, m + t);
for (int i = 0; i < arSarCoeffs.length; i++) {
diffedFcst[n + t] += arSarCoeffs[i] * (diffedFcst[n + t - i - 1] - mean);
fcst[m + t] += arSarCoeffs[i] * (diffedFcst[n + t - i - 1] - mean);
}
for (int j = maSmaCoeffs.length; j > 0 && t - j < 0; j--) {
diffedFcst[n + t] += maSmaCoeffs[j - 1] * resid[m + t - j];
fcst[m + t] += maSmaCoeffs[j - 1] * resid[m + t - j];
}
}
return slice(fcst, m, m + steps);
}
@Override
public TimeSeries pointForecast(final int steps) {
final int n = observations.n();
double[] fcst = fcst(steps);
TimePeriod timePeriod = observations.timePeriod();
final OffsetDateTime startTime = observations.observationTimes()
.get(n - 1)
.plus(timePeriod.periodLength() *
timePeriod.timeUnit().unitLength(),
timePeriod.timeUnit().temporalUnit());
return new TimeSeries(timePeriod, startTime, fcst);
}
@Override
public Forecast forecast(int steps, double alpha) {
return ArimaForecast.forecast(this, steps, alpha);
}
/**
* Create a forecast with 95% prediction intervals for the given number of steps ahead.
*
* @param steps the number of time periods ahead to forecast.
* @return a forecast with 95% prediction intervals for the given number of steps ahead.
*/
public Forecast forecast(final int steps) {
return forecast(steps, 0.05);
}
/*
* Start with the difference equation form of the model (Cryer and Chan 2008, 5.2): diffedSeries_t = ArmaProcess_t.
* Then, subtracting diffedSeries_t from both sides, we have ArmaProcess_t - diffedSeries_t = 0. Now add Y_t to both
* sides and rearrange terms to obtain Y_t = Y_t - diffedSeries_t + ArmaProcess_t. Thus, our in-sample estimate of
* Y_t, the "integrated" series, is Y_t(hat) = Y_t - diffedSeries_t + fittedArmaProcess_t.
*/
private double[] integrate(final double[] fitted) {
final int offset = this.order.d + this.order.D * this.seasonalFrequency;
final double[] integrated = new double[this.observations.n()];
for (int t = 0; t < offset; t++) {
integrated[t] = observations.at(t);
}
for (int t = offset; t < observations.n(); t++) {
integrated[t] = observations.at(t) - differencedSeries.at(t - offset) + fitted[t - offset];
}
return integrated;
}
private Matrix getInitialHessian(final int n) {
return new Matrix.IdentityBuilder(n).build();
// if (order.constant == 1) {
// final double meanParScale = 10 * differencedSeries.stdDeviation() / Math.sqrt(differencedSeries.n());
// return builder.set(n - 1, n - 1, 1.0).build();
// }
// return builder.build();
}
private Matrix getInitialHessian(final Arima model) {
double[] stdErrors = model.stdErrors;
Matrix.IdentityBuilder builder = new Matrix.IdentityBuilder(stdErrors.length);
for (int i = 0; i < stdErrors.length; i++) {
builder.set(i, i, stdErrors[i] * stdErrors[i] * differencedSeries.n());
}
return builder.build();
}
private double[] getInitialParameters(final double meanParScale) {
// Set initial constant to the mean and all other parameters to zero.
double[] initParams = new double[order.sumARMA() + order.constant];
if (order.constant == 1 && Math.abs(meanParScale) > Math.ulp(1.0)) {
initParams[initParams.length - 1] = differencedSeries.mean() / meanParScale;
}
return initParams;
}
private double[] getSarCoeffs(final Vector optimizedParams) {
final double[] sarCoeffs = new double[order.P];
for (int i = 0; i < order.P; i++) {
sarCoeffs[i] = optimizedParams.at(i + order.p + order.q);
}
return sarCoeffs;
}
private double[] getSmaCoeffs(final Vector optimizedParams) {
final double[] smaCoeffs = new double[order.Q];
for (int i = 0; i < order.Q; i++) {
smaCoeffs[i] = optimizedParams.at(i + order.p + order.q + order.P);
}
return smaCoeffs;
}
private double[] getArCoeffs(final Vector optimizedParams) {
final double[] arCoeffs = new double[order.p];
for (int i = 0; i < order.p; i++) {
arCoeffs[i] = optimizedParams.at(i);
}
return arCoeffs;
}
private double[] getMaCoeffs(final Vector optimizedParams) {
final double[] arCoeffs = new double[order.q];
for (int i = 0; i < order.q; i++) {
arCoeffs[i] = optimizedParams.at(i + order.p);
}
return arCoeffs;
}
@Override
public TimeSeries timeSeries() {
return this.observations;
}
@Override
public TimeSeries fittedSeries() {
return this.fittedSeries;
}
@Override
public TimeSeries residuals() {
return this.residuals;
}
/**
* Get the maximum-likelihood estimate of the model variance, equal to the sum of squared residuals divided by the
* number of observations.
*
* @return the maximum-likelihood estimate of the model variance, equal to the sum of squared residuals divided
* by the
* number of observations.
*/
public double sigma2() {
return modelInfo.sigma2;
}
/**
* Get the frequency of observations per seasonal cycle.
*
* @return the frequency of observations per seasonal cycle.
*/
public int seasonalFrequency() {
return this.seasonalFrequency;
}
/**
* Get the standard errors of the model parameters.
*
* @return the standard errors of the model parameters.
*/
public double[] stdErrors() {
return this.stdErrors.clone();
}
/**
* Get the coefficients of this ARIMA model.
*
* @return the coefficients of this ARIMA model.
*/
public ModelCoefficients coefficients() {
return this.modelCoefficients;
}
/**
* Get the order of this ARIMA model.
*
* @return the order of this ARIMA model.
*/
public ModelOrder order() {
return this.order;
}
/**
* Get the natural logarithm of the likelihood of the model parameters given the data.
*
* @return the natural logarithm of the likelihood of the model parameters given the data.
*/
public double logLikelihood() {
return modelInfo.logLikelihood;
}
/**
* Get the Akaike Information Criterion (AIC) for this model. The AIC is defined as 2k −
* 2L where k is the number of parameters in the model and L is the logarithm of the likelihood.
*
* @return the Akaike Information Criterion (AIC) for this model.
*/
public double aic() {
return modelInfo.aic;
}
double[] arSarCoefficients() {
return this.arSarCoeffs.clone();
}
double[] maSmaCoefficients() {
return this.maSmaCoeffs.clone();
}
@Override
public String toString() {
return "\norder: " + order + "\nmodelInfo: " + modelInfo + "\nmodelCoefficients: " + modelCoefficients;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Arima arima = (Arima) o;
if (seasonalFrequency != arima.seasonalFrequency) return false;
if (Double.compare(arima.mean, mean) != 0) return false;
if (!observations.equals(arima.observations)) return false;
if (!differencedSeries.equals(arima.differencedSeries)) return false;
if (!fittedSeries.equals(arima.fittedSeries)) return false;
if (!residuals.equals(arima.residuals)) return false;
if (!order.equals(arima.order)) return false;
if (!modelInfo.equals(arima.modelInfo)) return false;
if (!modelCoefficients.equals(arima.modelCoefficients)) return false;
if (!Arrays.equals(arSarCoeffs, arima.arSarCoeffs)) return false;
if (!Arrays.equals(maSmaCoeffs, arima.maSmaCoeffs)) return false;
return Arrays.equals(stdErrors, arima.stdErrors);
}
@Override
public int hashCode() {
int result;
long temp;
result = observations.hashCode();
result = 31 * result + differencedSeries.hashCode();
result = 31 * result + fittedSeries.hashCode();
result = 31 * result + residuals.hashCode();
result = 31 * result + seasonalFrequency;
result = 31 * result + order.hashCode();
result = 31 * result + modelInfo.hashCode();
result = 31 * result + modelCoefficients.hashCode();
temp = Double.doubleToLongBits(mean);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + Arrays.hashCode(arSarCoeffs);
result = 31 * result + Arrays.hashCode(maSmaCoeffs);
result = 31 * result + Arrays.hashCode(stdErrors);
return result;
}
/**
* The strategy to be used for fitting an ARIMA model.
*
* @author Jacob Rachiele
*/
public enum FittingStrategy {
/**
* Conditional sum-of-squares.
*/
CSS,
/**
* Unconditional sum-of-squares.
*/
USS,
/**
* Maximum likelihood.
*/
ML,
/**
* Conditional sum-of-squares followed by maximum likelihood.
*/
CSSML,
/**
* Unconditional sum-of-squares followed by maximum likelihood.
*/
USSML;
}
/**
* The order of an ARIMA model, consisting of the number of autoregressive and moving average parameters, along with
* the degree of differencing and a flag indicating whether or not the model includes a constant.
* This class is immutable and thread-safe.
*
* @author Jacob Rachiele
*/
public static class ModelOrder {
final int p;
final int d;
final int q;
final int P;
final int D;
final int Q;
final int constant;
private ModelOrder(final int p, final int d, final int q, final int P, final int D, final int Q,
final boolean constant) {
this.p = p;
this.d = d;
this.q = q;
this.P = P;
this.D = D;
this.Q = Q;
this.constant = (constant) ? 1 : 0;
}
// This returns the total number of nonseasonal and seasonal ARMA parameters.
private int sumARMA() {
return this.p + this.q + this.P + this.Q;
}
@Override
public String toString() {
boolean isSeasonal = P > 0 || Q > 0 || D > 0;
StringBuilder builder = new StringBuilder();
if (isSeasonal) {
builder.append("Seasonal ");
}
builder.append("ARIMA (").append(p).append(", ").append(d).append(", ").append(q);
if (isSeasonal) {
builder.append(") x (").append(P).append(", ").append(D).append(", ").append(Q);
}
builder.append(") with").append((constant == 1) ? " a constant" : " no constant");
return builder.toString();
}
@Override
public int hashCode() {
final int prime = 31;
int result = 1;
result = prime * result + D;
result = prime * result + P;
result = prime * result + Q;
result = prime * result + constant;
result = prime * result + d;
result = prime * result + p;
result = prime * result + q;
return result;
}
@Override
public boolean equals(Object obj) {
if (this == obj) return true;
if (obj == null) return false;
if (getClass() != obj.getClass()) return false;
ModelOrder other = (ModelOrder) obj;
if (D != other.D) return false;
if (P != other.P) return false;
if (Q != other.Q) return false;
if (constant != other.constant) return false;
if (d != other.d) return false;
return p == other.p && q == other.q;
}
}
/**
* Consists of the autoregressive and moving-average coefficients for a seasonal ARIMA model, along with the
* degrees of differencing and the model mean.
*
* @author Jacob Rachiele
*/
public static class ModelCoefficients {
private final double[] arCoeffs;
private final double[] maCoeffs;
private final double[] sarCoeffs;
private final double[] smaCoeffs;
private final int d;
private final int D;
private final double mean;
// The intercept is equal to mean * (1 - (sum of AR coefficients))
private final double intercept;
/**
* Create a structure holding the coefficients, the degrees of differencing, and the mean of a seasonal ARIMA
* model.
*
* @param arCoeffs the non-seasonal autoregressive coefficients.
* @param maCoeffs the non-seasonal moving-average coefficients.
* @param sarCoeffs the seasonal autoregressive coefficients.
* @param smaCoeffs the seasonal moving-average coefficients.
* @param d the non-seasonal degree of differencing.
* @param D the seasonal degree of differencing.
* @param mean the process mean.
*/
ModelCoefficients(final double[] arCoeffs, final double[] maCoeffs, final double[] sarCoeffs,
final double[] smaCoeffs, final int d, final int D, final double mean) {
this.arCoeffs = arCoeffs.clone();
this.maCoeffs = maCoeffs.clone();
this.sarCoeffs = sarCoeffs.clone();
this.smaCoeffs = smaCoeffs.clone();
this.d = d;
this.D = D;
this.mean = mean;
this.intercept = this.mean * (1 - sumOf(arCoeffs) - sumOf(sarCoeffs));
}
private ModelCoefficients(Builder builder) {
this.arCoeffs = builder.arCoeffs.clone();
this.maCoeffs = builder.maCoeffs.clone();
this.sarCoeffs = builder.sarCoeffs.clone();
this.smaCoeffs = builder.smaCoeffs.clone();
this.d = builder.d;
this.D = builder.D;
this.mean = builder.mean;
this.intercept = this.mean * (1 - sumOf(arCoeffs) - sumOf(sarCoeffs));
}
/**
* Create a new builder for a ModelCoefficients object.
*
* @return a new builder for a ModelCoefficients object.
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Get the autoregressive coefficients.
*
* @return the autoregressive coefficients.
*/
public final double[] arCoeffs() {
return arCoeffs.clone();
}
/**
* Get the moving-average coefficients.
*
* @return the moving-average coefficients.
*/
public final double[] maCoeffs() {
return maCoeffs.clone();
}
/**
* Get the seasonal autoregressive coefficients.
*
* @return the seasonal autoregressive coefficients.
*/
public final double[] seasonalARCoeffs() {
return sarCoeffs.clone();
}
/**
* Get the seasonal moving-average coefficients.
*
* @return the seasonal moving-average coefficients.
*/
public final double[] seasonalMACoeffs() {
return smaCoeffs.clone();
}
/**
* Get the degree of non-seasonal differencing.
*
* @return the degree of non-seasonal differencing.
*/
public final int d() {
return d;
}
/**
* Get the degree of seasonal differencing.
*
* @return the degree of seasonal differencing.
*/
public final int D() {
return D;
}
/**
* Get the model mean.
*
* @return the model mean.
*/
public final double mean() {
return mean;
}
/**
* Get the model intercept term. Note that this is not the model mean, as in R, but the actual
* intercept. The intercept is equal to μ × (1 - sum(AR)), where μ is the model mean and AR
* is a vector containing the non-seasonal and seasonal autoregressive coefficients.
*
* @return the model intercept term.
*/
public final double intercept() {
return this.intercept;
}
public final double[] getAllCoeffs() {
if (Math.abs(mean) < 1E-16) {
return DoubleFunctions.combine(arCoeffs, maCoeffs, sarCoeffs, smaCoeffs);
}
return DoubleFunctions.append(DoubleFunctions.combine(arCoeffs, maCoeffs, sarCoeffs, smaCoeffs), mean);
}
final boolean isSeasonal() {
return this.D > 0 || this.sarCoeffs.length > 0 || this.smaCoeffs.length > 0;
}
/**
* Computes and returns the order of the ARIMA model corresponding to the model coefficients.
*
* @return the order of the ARIMA model corresponding to the model coefficients.
*/
private ModelOrder extractModelOrder() {
return new ModelOrder(arCoeffs.length, d, maCoeffs.length, sarCoeffs.length, D, smaCoeffs.length,
(Math.abs(mean) > 1E-16));
}
@Override
public String toString() {
StringBuilder sb = new StringBuilder();
if (arCoeffs.length > 0) {
sb.append("\nautoregressive: ").append(Arrays.toString(arCoeffs));
}
if (maCoeffs.length > 0) {
sb.append("\nmoving-average: ").append(Arrays.toString(maCoeffs));
}
if (sarCoeffs.length > 0) {
sb.append("\nseasonal autoregressive: ").append(Arrays.toString(sarCoeffs));
}
if (smaCoeffs.length > 0) {
sb.append("\nseasonal moving-average: ").append(Arrays.toString(smaCoeffs));
}
sb.append("\nmean: ").append(mean);
sb.append("\nintercept: ").append(intercept);
return sb.toString();
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ModelCoefficients that = (ModelCoefficients) o;
if (d != that.d) return false;
if (D != that.D) return false;
if (Double.compare(that.mean, mean) != 0) return false;
if (Double.compare(that.intercept, intercept) != 0) return false;
if (!Arrays.equals(arCoeffs, that.arCoeffs)) return false;
if (!Arrays.equals(maCoeffs, that.maCoeffs)) return false;
if (!Arrays.equals(sarCoeffs, that.sarCoeffs)) return false;
return Arrays.equals(smaCoeffs, that.smaCoeffs);
}
@Override
public int hashCode() {
int result;
long temp;
result = Arrays.hashCode(arCoeffs);
result = 31 * result + Arrays.hashCode(maCoeffs);
result = 31 * result + Arrays.hashCode(sarCoeffs);
result = 31 * result + Arrays.hashCode(smaCoeffs);
result = 31 * result + d;
result = 31 * result + D;
temp = Double.doubleToLongBits(mean);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(intercept);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
/**
* A builder class for ARIMA model coefficients.
*
* @author Jacob Rachiele
*/
public static class Builder {
private double[] arCoeffs = new double[]{};
private double[] maCoeffs = new double[]{};
private double[] sarCoeffs = new double[]{};
private double[] smaCoeffs = new double[]{};
private int d = 0;
private int D = 0;
private double mean = 0.0;
private Builder() {
}
public Builder setARCoeffs(double... arCoeffs) {
this.arCoeffs = arCoeffs;
return this;
}
public Builder setSeasonalARCoeffs(double... sarCoeffs) {
this.sarCoeffs = sarCoeffs;
return this;
}
public Builder setMACoeffs(double... maCoeffs) {
this.maCoeffs = maCoeffs;
return this;
}
public Builder setSeasonalMACoeffs(double... smaCoeffs) {
this.smaCoeffs = smaCoeffs;
return this;
}
public Builder setDifferences(int d) {
this.d = d;
return this;
}
public Builder setSeasonalDifferences(int D) {
this.D = D;
return this;
}
public Builder setMean(double mean) {
this.mean = mean;
return this;
}
public ModelCoefficients build() {
return new ModelCoefficients(this);
}
}
}
/**
* A numerical description of an ARIMA model.
*
* @author Jacob Rachiele
*/
static class ModelInformation {
private final double sigma2;
private final double logLikelihood;
private final double aic;
private final double[] residuals;
private final double[] fitted;
/**
* Create new model information with the given data.
*
* @param npar the number of parameters in the model.
* @param sigma2 an estimate of the model variance.
* @param logLikelihood the natural logarithms of the likelihood of the model parameters.
* @param residuals the difference between the observations and the fitted values.
* @param fitted the values fitted by the model to the data.
*/
ModelInformation(final int npar, final double sigma2, final double logLikelihood, final double[] residuals,
final double[] fitted) {
this.sigma2 = sigma2;
this.logLikelihood = logLikelihood;
this.aic = 2 * npar - 2 * logLikelihood;
this.residuals = residuals.clone();
this.fitted = fitted.clone();
}
@Override
public String toString() {
return "sigma2: " + sigma2 + "\nlogLikelihood: " + logLikelihood + "\nAIC: " + aic;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
ModelInformation that = (ModelInformation) o;
if (Double.compare(that.sigma2, sigma2) != 0) return false;
if (Double.compare(that.logLikelihood, logLikelihood) != 0) return false;
if (Double.compare(that.aic, aic) != 0) return false;
if (!Arrays.equals(residuals, that.residuals)) return false;
return Arrays.equals(fitted, that.fitted);
}
@Override
public int hashCode() {
int result;
long temp;
temp = Double.doubleToLongBits(sigma2);
result = (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(logLikelihood);
result = 31 * result + (int) (temp ^ (temp >>> 32));
temp = Double.doubleToLongBits(aic);
result = 31 * result + (int) (temp ^ (temp >>> 32));
result = 31 * result + Arrays.hashCode(residuals);
result = 31 * result + Arrays.hashCode(fitted);
return result;
}
}
static class OptimFunction extends AbstractMultivariateFunction {
private final TimeSeries differencedSeries;
private final ModelOrder order;
private final FittingStrategy fittingStrategy;
private final int seasonalFrequency;
private final double[] arParams;
private final double[] maParams;
private final double[] sarParams;
private final double[] smaParams;
private final double meanScale;
OptimFunction(final TimeSeries differencedSeries, final ModelOrder order, final FittingStrategy fittingStrategy,
final int seasonalFrequency, final double meanScale) {
this.differencedSeries = differencedSeries;
this.order = order;
this.fittingStrategy = fittingStrategy;
this.seasonalFrequency = seasonalFrequency;
this.arParams = new double[order.p];
this.maParams = new double[order.q];
this.sarParams = new double[order.P];
this.smaParams = new double[order.Q];
this.meanScale = meanScale;
}
@Override
public final double at(final Vector point) {
functionEvaluations++;
final double[] params = point.elements();
System.arraycopy(params, 0, arParams, 0, order.p);
System.arraycopy(params, order.p, maParams, 0, order.q);
System.arraycopy(params, order.p + order.q, sarParams, 0, order.P);
System.arraycopy(params, order.p + order.q + order.P, smaParams, 0, order.Q);
final double[] arCoeffs = Arima.expandArCoefficients(arParams, sarParams, seasonalFrequency);
final double[] maCoeffs = Arima.expandMaCoefficients(maParams, smaParams, seasonalFrequency);
final double mean = (order.constant == 1) ? meanScale * params[params.length - 1] : 0.0;
if (fittingStrategy == FittingStrategy.ML || fittingStrategy == FittingStrategy.CSSML ||
fittingStrategy == FittingStrategy.USSML) {
final int n = differencedSeries.n();
ArmaKalmanFilter.KalmanOutput output = Arima.kalmanFit(differencedSeries, arCoeffs, maCoeffs, mean);
return 0.5 * (Math.log(output.sigma2()) + output.sumLog() / n);
}
final ModelInformation info = (fittingStrategy == FittingStrategy.CSS)
? Arima.fitCSS(differencedSeries, arCoeffs, maCoeffs, mean, order)
: Arima.fitUSS(differencedSeries, arCoeffs, maCoeffs, mean, order);
return 0.5 * Math.log(info.sigma2);
}
// static double[] transformParameters(final double[] arParams) {
// double phi;
// int p = arParams.length;
// double[] work = new double[p];
// double[] transformed = new double[p];
// for (int i = 0; i < p; i++) {
// transformed[i] = Math.tanh(arParams[i]);
// }
// for (int j = 1; j < p; j++) {
// phi = transformed[j];
// for (int k = 0; k < j; k++) {
// work[k] -= phi * transformed[j - k - 1];
// }
// System.arraycopy(work, 0, transformed, 0, j);
// }
// return transformed;
// }
@Override
public String toString() {
return "differencedSeries: " + differencedSeries + "\norder: " + order + "\nfittingStrategy: " +
fittingStrategy + "\nseasonalFrequency: " + seasonalFrequency + "\narParams: " +
Arrays.toString(arParams) + "\nmaParams: " + Arrays.toString(maParams) + "\nsarParams: " +
Arrays.toString(sarParams) + "\nsmaParams:" + " " + Arrays.toString(smaParams);
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
OptimFunction that = (OptimFunction) o;
if (seasonalFrequency != that.seasonalFrequency) return false;
if (Double.compare(that.meanScale, meanScale) != 0) return false;
if (!differencedSeries.equals(that.differencedSeries)) return false;
if (!order.equals(that.order)) return false;
if (fittingStrategy != that.fittingStrategy) return false;
if (!Arrays.equals(arParams, that.arParams)) return false;
if (!Arrays.equals(maParams, that.maParams)) return false;
if (!Arrays.equals(sarParams, that.sarParams)) return false;
return Arrays.equals(smaParams, that.smaParams);
}
@Override
public int hashCode() {
int result;
long temp;
result = differencedSeries.hashCode();
result = 31 * result + order.hashCode();
result = 31 * result + fittingStrategy.hashCode();
result = 31 * result + seasonalFrequency;
result = 31 * result + Arrays.hashCode(arParams);
result = 31 * result + Arrays.hashCode(maParams);
result = 31 * result + Arrays.hashCode(sarParams);
result = 31 * result + Arrays.hashCode(smaParams);
temp = Double.doubleToLongBits(meanScale);
result = 31 * result + (int) (temp ^ (temp >>> 32));
return result;
}
}
/**
* An ARIMA model simulation.
*/
@EqualsAndHashCode @ToString
public static class Simulation {
private final ModelCoefficients coefficients;
private final Distribution distribution;
private final TimePeriod period;
private final TimePeriod seasonalCycle;
private final int n;
private Simulation(Builder builder) {
this.coefficients = builder.coefficients;
this.distribution = builder.distribution;
this.period = builder.period;
this.seasonalCycle = builder.seasonalCycle;
this.n = builder.n;
}
/**
* Get a new builder for an ARIMA simulation.
*
* @return a new builder for an ARIMA simulation.
*/
public static Builder newBuilder() {
return new Builder();
}
/**
* Simulate the ARIMA model and return the resulting time series.
*
* @return the simulated time series.
*/
public TimeSeries sim() {
final int burnin = (int) (n / 2.0);
final int seasonalFrequency = (int) period.frequencyPer(seasonalCycle);
double[] arSarCoeffs = expandArCoefficients(coefficients.arCoeffs, coefficients.sarCoeffs,
seasonalFrequency);
double[] maSmaCoeffs = expandMaCoefficients(coefficients.maCoeffs, coefficients.smaCoeffs,
seasonalFrequency);
int diffOffset = coefficients.d + coefficients.D * seasonalFrequency;
int offset = Math.min(n, arSarCoeffs.length);
double[] series = new double[n + burnin];
double[] errors = new double[n + burnin];
for (int t = 0; t < offset; t++) {
series[t] = errors[t] = distribution.rand();
series[t] += coefficients.mean;
for (int j = 0; j < Math.min(t, maSmaCoeffs.length); j++) {
series[t] += maSmaCoeffs[j] * errors[t - j - 1];
}
}
int end;
for (int t = offset; t < n + burnin; t++) {
series[t] = errors[t] = distribution.rand();
series[t] += coefficients.mean;
end = Math.min(t, arSarCoeffs.length);
for (int j = 0; j < end; j++) {
series[t] += arSarCoeffs[j] * (series[t - j - 1] - coefficients.mean);
}
end = Math.min(t, maSmaCoeffs.length);
for (int j = 0; j < end; j++) {
series[t] += maSmaCoeffs[j] * errors[t - j - 1];
}
}
LagPolynomial poly = LagPolynomial.differences(coefficients.d)
.times(LagPolynomial.seasonalDifferences(seasonalFrequency,
coefficients.D));
end = n + burnin;
for (int t = diffOffset; t < end; t++) {
series[t] += poly.fit(series, t);
}
series = DoubleFunctions.slice(series, burnin, n + burnin);
return new TimeSeries(period, OffsetDateTime.of(1, 1, 1, 0, 0, 0, 0, ZoneOffset.ofHours(0)), series);
}
/**
* An ARIMA simulation builder.
*/
public static class Builder {
private ModelCoefficients coefficients = ModelCoefficients.newBuilder().build();
private Distribution distribution = new Normal();
private TimePeriod period = (coefficients.isSeasonal()) ? TimePeriod.oneMonth() : TimePeriod.oneYear();
private TimePeriod seasonalCycle = TimePeriod.oneYear();
private int n = 500;
private boolean periodSet = false;
/**
* Set the model coefficients to be used in simulating the ARIMA model.
*
* @param coefficients the model coefficients for the simulation.
* @return this builder.
*/
public Builder setCoefficients(ModelCoefficients coefficients) {
if (coefficients == null) {
throw new NullPointerException("The model coefficients cannot be null.");
}
this.coefficients = coefficients;
if (!periodSet) {
this.period = (coefficients.isSeasonal()) ? TimePeriod.oneMonth() : TimePeriod.oneYear();
}
return this;
}
/**
* Set the probability distribution to draw the ARIMA process random errors from.
*
* @param distribution the probability distribution to draw the random errors from.
* @return this builder.
*/
public Builder setDistribution(Distribution distribution) {
if (distribution == null) {
throw new NullPointerException("The distribution cannot be null.");
}
this.distribution = distribution;
return this;
}
/**
* Set the time period between simulated observations. The default is one year for
* non-seasonal model coefficients and one month for seasonal model coefficients.
*
* @param period the time period between simulated observations.
* @return this builder.
*/
public Builder setPeriod(TimePeriod period) {
if (period == null) {
throw new NullPointerException("The time period cannot be null.");
}
this.periodSet = true;
this.period = period;
return this;
}
/**
* Set the time cycle at which the seasonal pattern of the simulated time series repeats. This defaults
* to one year.
*
* @param seasonalCycle the time cycle at which the seasonal pattern of the simulated time series repeats.
* @return this builder.
*/
public Builder setSeasonalCycle(TimePeriod seasonalCycle) {
if (seasonalCycle == null) {
throw new NullPointerException("The seasonal cycle cannot be null.");
}
this.seasonalCycle = seasonalCycle;
return this;
}
/**
* Set the number of observations to be simulated.
*
* @param n the number of observations to simulate.
* @return this builder.
*/
public Builder setN(int n) {
if (n < 1) {
throw new IllegalArgumentException(
"the number of observations to simulate must be a positive integer.");
}
this.n = n;
return this;
}
/**
* Simulate the time series directly from this builder. This is equivalent to calling build on this builder,
* then sim on the returned Simulation object.
*
* @return the simulated time series.
*/
public TimeSeries sim() {
return new Simulation(this).sim();
}
/**
* Construct and return a new fully built and immutable Simulation object.
*
* @return a new fully built and immutable Simulation object.
*/
public Simulation build() {
return new Simulation(this);
}
}
}
}