ts-arima-forecast
Version:
TypeScript library for ARIMA, SARIMA (soon), and SARIMAX (soon) forecasting
311 lines • 12.4 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.Adaptive = exports.RollingWindow = exports.Stepwise = exports.ForecastStrategy = void 0;
const arima_1 = require("../models/arima");
class ForecastStrategy {
constructor(baseModel, initialData) {
this.baseModel = baseModel;
this.initialData = [...initialData];
}
}
exports.ForecastStrategy = ForecastStrategy;
class Stepwise extends ForecastStrategy {
constructor(baseModel, initialData, options = {}) {
super(baseModel, initialData);
this.lastForecast = null;
this.refitModel = options.refitModel ?? true;
this.verbose = options.verbose ?? false;
this.currentData = [...initialData];
this.currentModel = baseModel;
}
forecast(steps) {
const result = this.currentModel.forecast(steps);
return {
forecasts: result.forecast,
confidenceIntervals: {
lower: result.lowerBound,
upper: result.upperBound
}
};
}
forecastWithRealTimeData(actualObservations) {
const steps = actualObservations.length;
const forecasts = [];
const lowerBounds = [];
const upperBounds = [];
const updatedModels = [];
const errors = [];
let workingData = [...this.currentData];
let workingModel = this.currentModel;
for (let step = 0; step < steps; step++) {
const oneStepForecast = workingModel.forecast(1);
const forecastValue = oneStepForecast.forecast[0];
const lowerBound = oneStepForecast.lowerBound[0];
const upperBound = oneStepForecast.upperBound[0];
forecasts.push(forecastValue);
lowerBounds.push(lowerBound);
upperBounds.push(upperBound);
const actualObservation = actualObservations[step];
const error = Math.abs(forecastValue - actualObservation);
errors.push(error);
workingData.push(actualObservation);
// refit model if enabled
if (this.refitModel) {
const updatedModel = new arima_1.ARIMA(workingModel.getParams());
updatedModel.fit(workingData);
workingModel = updatedModel;
updatedModels.push(updatedModel);
}
if (this.verbose) {
console.log(`Step ${step + 1}: Forecast=${forecastValue.toFixed(3)}, Actual=${actualObservation.toFixed(3)}, Error=${error.toFixed(3)}`);
}
}
this.currentData = workingData;
this.currentModel = workingModel;
return {
forecasts,
confidenceIntervals: { lower: lowerBounds, upper: upperBounds },
updatedModels,
errors,
actualValues: actualObservations
};
}
addObservationAndForecast(newObservation) {
let error;
if (this.lastForecast) {
error = Math.abs(this.lastForecast.forecast - newObservation);
if (this.verbose) {
console.log(`Previous forecast: ${this.lastForecast.forecast.toFixed(3)}, Actual observation: ${newObservation.toFixed(3)}, Error: ${error.toFixed(3)}`);
}
}
this.currentData.push(newObservation);
if (this.refitModel) {
const updatedModel = new arima_1.ARIMA(this.currentModel.getParams());
updatedModel.fit(this.currentData);
this.currentModel = updatedModel;
}
const nextForecast = this.currentModel.forecast(1);
const forecastResult = {
forecast: nextForecast.forecast[0],
lowerBound: nextForecast.lowerBound[0],
upperBound: nextForecast.upperBound[0]
};
this.lastForecast = forecastResult;
if (this.verbose) {
console.log(`Next forecast: ${forecastResult.forecast.toFixed(3)}`);
}
return {
...forecastResult,
error
};
}
reset(newInitialData) {
this.currentData = [...newInitialData];
this.currentModel = new arima_1.ARIMA(this.baseModel.getParams());
this.currentModel.fit(this.currentData);
this.lastForecast = null;
}
getCurrentData() {
return [...this.currentData];
}
getCurrentModel() {
return this.currentModel;
}
}
exports.Stepwise = Stepwise;
class RollingWindow extends ForecastStrategy {
constructor(baseModel, initialData, options = {}) {
super(baseModel, initialData);
this.lastForecast = null;
this.windowSize = options.windowSize ?? initialData.length;
this.verbose = options.verbose ?? false;
this.currentData = [...initialData];
}
forecast(steps) {
// Use current window to forecast multiple steps ahead
const windowData = this.currentData.slice(-this.windowSize);
const model = new arima_1.ARIMA(this.baseModel.getParams());
model.fit(windowData);
const result = model.forecast(steps);
return {
forecasts: result.forecast,
confidenceIntervals: {
lower: result.lowerBound,
upper: result.upperBound
}
};
}
forecastWithRealTimeData(actualObservations) {
const steps = actualObservations.length;
const forecasts = [];
const lowerBounds = [];
const upperBounds = [];
const errors = [];
let workingData = [...this.currentData];
for (let step = 0; step < steps; step++) {
// Keep only the most recent observations (rolling window)
const windowData = workingData.slice(-this.windowSize);
// Fit model on current window
const model = new arima_1.ARIMA(this.baseModel.getParams());
model.fit(windowData);
// Forecast one step ahead
const oneStepForecast = model.forecast(1);
const forecastValue = oneStepForecast.forecast[0];
const lowerBound = oneStepForecast.lowerBound[0];
const upperBound = oneStepForecast.upperBound[0];
// Store results
forecasts.push(forecastValue);
lowerBounds.push(lowerBound);
upperBounds.push(upperBound);
// Use actual observation and calculate error
const actualObservation = actualObservations[step];
const error = Math.abs(forecastValue - actualObservation);
errors.push(error);
workingData.push(actualObservation);
if (this.verbose) {
console.log(`Rolling Step ${step + 1}: Forecast=${forecastValue.toFixed(3)}, Actual=${actualObservation.toFixed(3)}, Error=${error.toFixed(3)}, Window Size=${windowData.length}`);
}
}
// Update internal state
this.currentData = workingData;
return {
forecasts,
confidenceIntervals: { lower: lowerBounds, upper: upperBounds },
errors,
actualValues: actualObservations
};
}
addObservationAndForecast(newObservation) {
let error;
// Calculate error if we have a previous forecast
if (this.lastForecast) {
error = Math.abs(this.lastForecast.forecast - newObservation);
if (this.verbose) {
console.log(`Previous forecast: ${this.lastForecast.forecast.toFixed(3)}, Actual observation: ${newObservation.toFixed(3)}, Error: ${error.toFixed(3)}`);
}
}
// Add new observation
this.currentData.push(newObservation);
// Keep only the most recent observations (rolling window)
const windowData = this.currentData.slice(-this.windowSize);
// Fit model on current window
const model = new arima_1.ARIMA(this.baseModel.getParams());
model.fit(windowData);
// Get next forecast
const nextForecast = model.forecast(1);
const forecastResult = {
forecast: nextForecast.forecast[0],
lowerBound: nextForecast.lowerBound[0],
upperBound: nextForecast.upperBound[0]
};
// Store this forecast for next error calculation
this.lastForecast = forecastResult;
if (this.verbose) {
console.log(`Next forecast: ${forecastResult.forecast.toFixed(3)}, Window Size: ${windowData.length}`);
}
return {
...forecastResult,
error
};
}
setWindowSize(newWindowSize) {
this.windowSize = newWindowSize;
}
getWindowSize() {
return this.windowSize;
}
getCurrentData() {
return [...this.currentData];
}
reset(newInitialData) {
this.currentData = [...newInitialData];
this.lastForecast = null;
}
}
exports.RollingWindow = RollingWindow;
class Adaptive extends ForecastStrategy {
constructor(baseModel, initialData, options = {}) {
super(baseModel, initialData);
this.stepwiseStrategy = new Stepwise(baseModel, initialData, {
refitModel: true,
verbose: options.verbose
});
this.rollingStrategy = new RollingWindow(baseModel, initialData, {
windowSize: options.windowSize,
verbose: options.verbose
});
this.adaptationThreshold = options.adaptationThreshold ?? 2.0;
this.maxErrorWindowSize = options.maxErrorWindowSize ?? 10;
this.errorWindow = [];
this.currentStrategy = 'stepwise';
this.verbose = options.verbose ?? false;
}
forecast(steps) {
if (this.currentStrategy === 'stepwise') {
return this.stepwiseStrategy.forecast(steps);
}
else {
return this.rollingStrategy.forecast(steps);
}
}
forecastWithRealTimeData(actualObservations) {
const results = this.currentStrategy === 'stepwise'
? this.stepwiseStrategy.forecastWithRealTimeData(actualObservations)
: this.rollingStrategy.forecastWithRealTimeData(actualObservations);
if (results.errors) {
results.errors.forEach(error => this.addError(error));
if (this.shouldSwitchStrategy()) {
this.switchStrategy();
}
}
return results;
}
addObservationAndForecast(newObservation) {
const result = this.currentStrategy === 'stepwise'
? this.stepwiseStrategy.addObservationAndForecast(newObservation)
: this.rollingStrategy.addObservationAndForecast(newObservation);
if (result.error !== undefined) {
this.addError(result.error);
if (this.shouldSwitchStrategy()) {
this.switchStrategy();
}
}
if (this.verbose) {
const errorText = result.error !== undefined ? `, Error: ${result.error.toFixed(3)}` : '';
console.log(`Adaptive strategy (${this.currentStrategy}): New observation: ${newObservation.toFixed(3)}, Next forecast: ${result.forecast.toFixed(3)}${errorText}`);
}
return result;
}
shouldSwitchStrategy() {
if (this.errorWindow.length < this.maxErrorWindowSize) {
return false;
}
const recentAvgError = this.errorWindow.slice(-5).reduce((a, b) => a + b, 0) / 5;
return recentAvgError > this.adaptationThreshold;
}
addError(error) {
this.errorWindow.push(error);
if (this.errorWindow.length > this.maxErrorWindowSize) {
this.errorWindow.shift();
}
}
switchStrategy() {
const oldStrategy = this.currentStrategy;
this.currentStrategy = this.currentStrategy === 'stepwise' ? 'rolling' : 'stepwise';
if (this.verbose) {
console.log(`Switching strategy from ${oldStrategy} to ${this.currentStrategy}`);
}
this.errorWindow = [];
}
getCurrentStrategy() {
return this.currentStrategy;
}
reset(newInitialData) {
this.stepwiseStrategy.reset(newInitialData);
this.rollingStrategy.reset(newInitialData);
this.errorWindow = [];
this.currentStrategy = 'stepwise';
}
}
exports.Adaptive = Adaptive;
//# sourceMappingURL=strategies.js.map