ts-arima-forecast
Version:
TypeScript library for ARIMA, SARIMA (soon), and SARIMAX (soon) forecasting
197 lines (156 loc) • 5.89 kB
text/typescript
import Statistics from '../utils/statistics';
import Preprocessing from '../utils/preprocessing';
import { ARIMAParams, ForecastResult, ModelFitResult } from '../types';
export class ARIMA {
private params: ARIMAParams;
private data: number[];
private fitResult?: ModelFitResult;
constructor(params: ARIMAParams) {
this.params = params;
this.data = [];
}
fit(data: number[]): ModelFitResult {
this.data = [...data];
let workingData = [...data];
for (let i = 0; i < this.params.d; i++) {
workingData = Preprocessing.difference(workingData);
}
const { p, q } = this.params;
const n = workingData.length;
const arParams = this.estimateARParameters(workingData, p);
const maParams = this.estimateMAParameters(workingData, arParams, q);
const fittedValues = this.calculateFittedValues(workingData, arParams, maParams);
const residuals = workingData.slice(Math.max(p, q)).map((val, i) => val - fittedValues[i]);
const sigma2 = Statistics.variance(residuals);
const logLikelihood = this.calculateLogLikelihood(residuals, sigma2);
const numParams = p + q + 1;
const aic = Statistics.akaike(logLikelihood, numParams, n);
const bic = Statistics.bayesian(logLikelihood, numParams, n);
this.fitResult = {
coefficients: {
ar: arParams,
ma: maParams
},
residuals,
fittedValues,
aic,
bic,
logLikelihood,
sigma2
};
return this.fitResult;
}
private estimateARParameters(data: number[], p: number): number[] {
if (p === 0) return [];
const n = data.length;
const X: number[][] = [];
const y: number[] = [];
for (let i = p; i < n; i++) {
const row: number[] = [];
for (let j = 1; j <= p; j++) {
row.push(data[i - j]);
}
X.push(row);
y.push(data[i]);
}
// Solve using normal equations: (X'X)^-1 X'y
return Statistics.leastSquares(X, y);
}
private estimateMAParameters(data: number[], arParams: number[], q: number): number[] {
if (q === 0) return [];
const maParams = new Array(q).fill(0.1);
for (let iter = 0; iter < 10; iter++) {
const residuals = this.calculateResiduals(data, arParams, maParams);
for (let i = 0; i < q && i < residuals.length - 1; i++) {
if (residuals.length > i + 1) {
maParams[i] = -Statistics.autocorrelation(residuals, i + 1) * 0.8;
}
}
}
return maParams;
}
private calculateResiduals(data: number[], arParams: number[], maParams: number[]): number[] {
const p = arParams.length;
const q = maParams.length;
const n = data.length;
const residuals: number[] = [];
for (let i = Math.max(p, q); i < n; i++) {
let prediction = 0;
for (let j = 0; j < p; j++) {
prediction += arParams[j] * data[i - j - 1];
}
for (let j = 0; j < q && j < residuals.length; j++) {
prediction += maParams[j] * residuals[residuals.length - j - 1];
}
residuals.push(data[i] - prediction);
}
return residuals;
}
private calculateFittedValues(data: number[], arParams: number[], maParams: number[]): number[] {
const residuals = this.calculateResiduals(data, arParams, maParams);
return data.slice(Math.max(arParams.length, maParams.length)).map((val, i) => val - residuals[i]);
}
private calculateLogLikelihood(residuals: number[], sigma2: number): number {
const n = residuals.length;
const sumSquares = residuals.reduce((sum, r) => sum + r * r, 0);
return -n / 2 * Math.log(2 * Math.PI) - n / 2 * Math.log(sigma2) - sumSquares / (2 * sigma2);
}
forecast(steps: number, confidenceLevel: number = 0.95): ForecastResult {
if (!this.fitResult) {
throw new Error('Model must be fitted before forecasting');
}
const { ar: arParams = [], ma: maParams = [] } = this.fitResult.coefficients;
const { residuals, sigma2 } = this.fitResult;
const forecast: number[] = [];
const variance: number[] = [];
let workingData = [...this.data];
for (let i = 0; i < this.params.d; i++) {
workingData = Preprocessing.difference(workingData);
}
const lastResiduals = residuals.slice(-Math.max(maParams.length, 1));
for (let h = 1; h <= steps; h++) {
let prediction = 0;
let predVariance = sigma2;
for (let i = 0; i < arParams.length; i++) {
if (h - i - 1 >= 0 && h - i - 1 < forecast.length) {
prediction += arParams[i] * forecast[h - i - 2];
} else if (workingData.length - i - 1 >= 0) {
prediction += arParams[i] * workingData[workingData.length - i - 1];
}
}
for (let i = 0; i < maParams.length && h <= i + 1; i++) {
if (lastResiduals.length - (h - i - 1) - 1 >= 0) {
prediction += maParams[i] * lastResiduals[lastResiduals.length - (h - i - 1) - 1];
}
}
forecast.push(prediction);
variance.push(predVariance);
}
let finalForecast = [...forecast];
for (let i = 0; i < this.params.d; i++) {
finalForecast = Preprocessing.undifference(
this.data.slice(-(this.params.d - i)),
finalForecast
);
}
// calculate confidence intervals
const zScore = Statistics.getZScore(confidenceLevel);
const lowerBound = finalForecast.map((f, i) => f - zScore * Math.sqrt(variance[i]));
const upperBound = finalForecast.map((f, i) => f + zScore * Math.sqrt(variance[i]));
return {
forecast: finalForecast,
lowerBound,
upperBound,
residuals: this.fitResult.residuals,
aic: this.fitResult.aic,
bic: this.fitResult.bic,
logLikelihood: this.fitResult.logLikelihood
};
}
getParams(): ARIMAParams {
return { ...this.params };
}
getFitResult(): ModelFitResult | undefined {
return this.fitResult;
}
}