ts-arima-forecast
Version:
TypeScript library for ARIMA, SARIMA (soon), and SARIMAX (soon) forecasting
160 lines • 6.43 kB
JavaScript
"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.ARIMA = void 0;
const statistics_1 = __importDefault(require("../utils/statistics"));
const preprocessing_1 = __importDefault(require("../utils/preprocessing"));
class ARIMA {
constructor(params) {
this.params = params;
this.data = [];
}
fit(data) {
this.data = [...data];
let workingData = [...data];
for (let i = 0; i < this.params.d; i++) {
workingData = preprocessing_1.default.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_1.default.variance(residuals);
const logLikelihood = this.calculateLogLikelihood(residuals, sigma2);
const numParams = p + q + 1;
const aic = statistics_1.default.akaike(logLikelihood, numParams, n);
const bic = statistics_1.default.bayesian(logLikelihood, numParams, n);
this.fitResult = {
coefficients: {
ar: arParams,
ma: maParams
},
residuals,
fittedValues,
aic,
bic,
logLikelihood,
sigma2
};
return this.fitResult;
}
estimateARParameters(data, p) {
if (p === 0)
return [];
const n = data.length;
const X = [];
const y = [];
for (let i = p; i < n; i++) {
const row = [];
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_1.default.leastSquares(X, y);
}
estimateMAParameters(data, arParams, q) {
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_1.default.autocorrelation(residuals, i + 1) * 0.8;
}
}
}
return maParams;
}
calculateResiduals(data, arParams, maParams) {
const p = arParams.length;
const q = maParams.length;
const n = data.length;
const residuals = [];
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;
}
calculateFittedValues(data, arParams, maParams) {
const residuals = this.calculateResiduals(data, arParams, maParams);
return data.slice(Math.max(arParams.length, maParams.length)).map((val, i) => val - residuals[i]);
}
calculateLogLikelihood(residuals, sigma2) {
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, confidenceLevel = 0.95) {
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 = [];
const variance = [];
let workingData = [...this.data];
for (let i = 0; i < this.params.d; i++) {
workingData = preprocessing_1.default.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_1.default.undifference(this.data.slice(-(this.params.d - i)), finalForecast);
}
// calculate confidence intervals
const zScore = statistics_1.default.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() {
return { ...this.params };
}
getFitResult() {
return this.fitResult;
}
}
exports.ARIMA = ARIMA;
//# sourceMappingURL=arima.js.map