/api/series/auto-arima-find: find parameters of SARIMA model automatically /api/series/manual-forecast: use determined model with parameters to forecast next values /api/series/identify-correlations: Calculate ACF and PACF for a time series /api/series/decompose-stl: Applies Seasonal-Trend-Loess (STL) decomposition to separate the series into trend, seasonal, and residual components.
133 lines
4.6 KiB
TypeScript
133 lines
4.6 KiB
TypeScript
// analysis_pipelines.ts - High-level workflows for common analysis tasks.
|
|
|
|
import { SignalProcessor } from './signal_processing_convolution';
|
|
import { TimeSeriesAnalyzer, STLDecomposition } from './timeseries';
|
|
|
|
/**
|
|
* The comprehensive result of a denoise and detrend operation.
|
|
*/
|
|
export interface DenoiseAndDetrendResult {
|
|
original: number[];
|
|
smoothed: number[];
|
|
decomposition: STLDecomposition;
|
|
}
|
|
|
|
/**
|
|
* The result of an automatic SARIMA parameter search.
|
|
*/
|
|
export interface AutoArimaResult {
|
|
bestModel: {
|
|
p: number;
|
|
d: number;
|
|
q: number;
|
|
P: number;
|
|
D: number;
|
|
Q: number;
|
|
s: number; // Correctly included
|
|
aic: number;
|
|
};
|
|
searchLog: { p: number; d: number; q: number; P: number; D: number; Q: number; s: number; aic: number }[];
|
|
}
|
|
|
|
|
|
/**
|
|
* A class containing high-level analysis pipelines that combine
|
|
* functions from various processing libraries.
|
|
*/
|
|
export class AnalysisPipelines {
|
|
|
|
/**
|
|
* A full pipeline to take a raw signal, smooth it to remove noise,
|
|
* and then decompose it into trend, seasonal, and residual components.
|
|
* @param series The original time series data.
|
|
* @param period The seasonal period for STL decomposition.
|
|
* @param smoothWindow The window size for the initial smoothing (denoising) pass.
|
|
* @returns An object containing the original, smoothed, and decomposed series.
|
|
*/
|
|
static denoiseAndDetrend(series: number[], period: number, smoothWindow: number = 5): DenoiseAndDetrendResult {
|
|
// Ensure window is odd for symmetry
|
|
if (smoothWindow > 1 && smoothWindow % 2 === 0) {
|
|
smoothWindow++;
|
|
}
|
|
const smoothed = SignalProcessor.smooth(series, {
|
|
method: 'gaussian',
|
|
windowSize: smoothWindow
|
|
});
|
|
|
|
const decomposition = TimeSeriesAnalyzer.stlDecomposition(smoothed, period);
|
|
|
|
return {
|
|
original: series,
|
|
smoothed: smoothed,
|
|
decomposition: decomposition,
|
|
};
|
|
}
|
|
|
|
/**
|
|
* [FINAL CORRECTED VERSION] Performs a full grid search to find the optimal SARIMA parameters.
|
|
* This version now correctly includes 's' in the final result object.
|
|
* @param series The original time series data.
|
|
* @param seasonalPeriod The seasonal period of the data (e.g., 7 for weekly, 12 for monthly).
|
|
* @returns An object containing the best model parameters and a log of the search.
|
|
*/
|
|
static findBestArimaParameters(
|
|
series: number[],
|
|
seasonalPeriod: number,
|
|
maxD: number = 1,
|
|
maxP: number = 2,
|
|
maxQ: number = 2,
|
|
maxSeasonalD: number = 1,
|
|
maxSeasonalP: number = 2,
|
|
maxSeasonalQ: number = 2
|
|
): AutoArimaResult {
|
|
|
|
const searchLog: any[] = [];
|
|
let bestModel: any = { aic: Infinity };
|
|
|
|
const calculateAIC = (residuals: number[], numParams: number): number => {
|
|
const n = residuals.length;
|
|
if (n === 0) return Infinity;
|
|
const sse = residuals.reduce((sum, r) => sum + r * r, 0);
|
|
if (sse < 1e-9) return -Infinity; // Perfect fit
|
|
const logLikelihood = -n / 2 * (Math.log(2 * Math.PI) + Math.log(sse / n)) - n / 2;
|
|
return 2 * numParams - 2 * logLikelihood;
|
|
};
|
|
|
|
// Grid search over all parameter combinations
|
|
for (let d = 0; d <= maxD; d++) {
|
|
for (let p = 0; p <= maxP; p++) {
|
|
for (let q = 0; q <= maxQ; q++) {
|
|
for (let D = 0; D <= maxSeasonalD; D++) {
|
|
for (let P = 0; P <= maxSeasonalP; P++) {
|
|
for (let Q = 0; Q <= maxSeasonalQ; Q++) {
|
|
// Skip trivial models where nothing is done
|
|
if (p === 0 && d === 0 && q === 0 && P === 0 && D === 0 && Q === 0) continue;
|
|
|
|
const options = { p, d, q, P, D, Q, s: seasonalPeriod };
|
|
try {
|
|
const { residuals } = TimeSeriesAnalyzer.arimaForecast(series, options, 0);
|
|
const numParams = p + q + P + Q;
|
|
const aic = calculateAIC(residuals, numParams);
|
|
|
|
// Construct the full model info object, ensuring 's' is included
|
|
const modelInfo = { p, d, q, P, D, Q, s: seasonalPeriod, aic };
|
|
searchLog.push(modelInfo);
|
|
|
|
if (modelInfo.aic < bestModel.aic) {
|
|
bestModel = modelInfo;
|
|
}
|
|
} catch (error) {
|
|
// Skip invalid parameter combinations that cause errors
|
|
}
|
|
} } } } } }
|
|
|
|
if (bestModel.aic === Infinity) {
|
|
throw new Error("Could not find a suitable SARIMA model. The data may be too short or complex.");
|
|
}
|
|
|
|
// Sort the log by AIC for easier reading
|
|
searchLog.sort((a, b) => a.aic - b.aic);
|
|
|
|
return { bestModel, searchLog };
|
|
}
|
|
}
|