diff --git a/analysis_pipelines.ts b/analysis_pipelines.ts new file mode 100644 index 0000000..a35ee7a --- /dev/null +++ b/analysis_pipelines.ts @@ -0,0 +1,133 @@ +// 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 }; + } +} diff --git a/api-documentation.html b/api-documentation.html index 6da21b0..67e690a 100644 --- a/api-documentation.html +++ b/api-documentation.html @@ -28,7 +28,7 @@