add timeseries endpoints and update server.ts

/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.
This commit is contained in:
raymond 2025-09-12 02:46:52 +00:00
parent 13f3c7b053
commit b1b2dcf18c
3 changed files with 682 additions and 26 deletions

229
server.ts
View file

@ -8,19 +8,20 @@ import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import * as math from 'mathjs';
import * as _ from 'lodash';
import cors from 'cors'; // <-- 1. IMPORT THE CORS PACKAGE
// These imports assume the files exist in the same directory
// Assuming these files exist in the same directory
// import { KMeans, KMeansOptions } from './kmeans';
// import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
// import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction';
import { SignalProcessor, SmoothingOptions, EdgeDetectionOptions } from './signal_processing_convolution';
import { convolve1D, ConvolutionKernels } from './convolution'; // Direct import for new functions
import { TimeSeriesAnalyzer, ARIMAOptions } from './timeseries';
import { AnalysisPipelines } from './analysis_pipelines';
import { convolve1D, convolve2D, ConvolutionKernels } from './convolution';
// Dummy interfaces/classes if the files are not present, to prevent compile errors
interface KMeansOptions {}
class KMeans {
constructor(p: any, n: any, o: any) {}
run = () => ({ clusters: [] })
}
class KMeans { constructor(p: any, n: any, o: any) {}; run = () => ({ clusters: [] }) }
const getWeekNumber = (d: string) => 1;
const getSameWeekDayLastYear = (d: string) => new Date().toISOString();
interface ForecastResult {}
@ -28,25 +29,27 @@ const calculateLinearRegression = (v: any) => ({slope: 1, intercept: 0});
const generateForecast = (m: any, l: any, p: any) => [];
const calculatePredictionIntervals = (v: any, m: any, f: any) => [];
const app = express();
app.use(express.json());
app.use(cors()); // <-- 2. ENABLE CORS FOR ALL ROUTES
const PORT = process.env.PORT || 3000;
const swaggerOptions = {
swaggerDefinition: {
openapi: '3.0.0',
info: {
title: 'My Express API',
version: '1.0.0',
description: 'API documentation for my awesome Express app',
},
servers: [
{
url: `http://localhost:${PORT}`,
},
],
swaggerDefinition: {
openapi: '3.0.0',
info: {
title: 'My Express API',
version: '1.0.0',
description: 'API documentation for my awesome Express app',
},
apis: ["./server.ts"], // Pointing to this file for Swagger docs
servers: [
{
url: `http://localhost:${PORT}`,
},
],
},
apis: ["./server_convolution.ts"], // Pointing to the correct, renamed file
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
@ -67,7 +70,6 @@ interface DataMatrix {
columns?: string[];
rows?: string[];
}
interface Condition {
field: string;
operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
@ -85,9 +87,8 @@ interface ApiResponse<T> {
// ========================================
const handleError = (error: unknown): string => {
return error instanceof Error ? error.message : 'Unknown error';
return error instanceof Error ? error.message : 'Unknown error';
};
const validateSeries = (series: DataSeries): void => {
if (!series || !Array.isArray(series.values) || series.values.length === 0) {
throw new Error('Series must contain at least one value');
@ -854,6 +855,159 @@ app.post('/api/series/rolling', (req, res) => {
}
});
/**
* @swagger
* /api/series/auto-arima-find:
* post:
* summary: (EXPERIMENTAL) Automatically find best SARIMA parameters
* description: Performs a grid search to find the best SARIMA parameters based on AIC. NOTE - This is a simplified estimation and may not find the true optimal model. For best results, use the identification tools and the 'manual-forecast' endpoint.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* seasonalPeriod:
* type: integer
* description: The seasonal period of the data (e.g., 7 for weekly).
* example: 7
* responses:
* '200':
* description: The best model found and the search log.
* '400':
* description: Invalid input data.
*/
app.post('/api/series/auto-arima-find', (req, res) => {
try {
const { series, seasonalPeriod } = req.body;
validateSeries(series);
const result = AnalysisPipelines.findBestArimaParameters(series.values, seasonalPeriod);
res.status(200).json({ success: true, data: result });
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage });
}
});
/**
* @swagger
* /api/series/manual-forecast:
* post:
* summary: Generate a forecast with manually specified SARIMA parameters
* description: This is the primary forecasting tool. It allows an expert user (who has analyzed ACF/PACF plots) to apply a specific SARIMA model to a time series and generate a forecast.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* $ref: '#/components/schemas/ARIMAOptions'
* forecastSteps:
* type: integer
* description: The number of future time steps to predict.
* example: 7
* responses:
* '200':
* description: The forecast results.
* '400':
* description: Invalid input data
*/
app.post('/api/series/manual-forecast', (req, res) => {
try {
const { series, options, forecastSteps } = req.body;
validateSeries(series);
const result = TimeSeriesAnalyzer.arimaForecast(series.values, options, forecastSteps);
res.status(200).json({ success: true, data: result });
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage });
}
});
/**
* @swagger
* /api/series/identify-correlations:
* post:
* summary: Calculate ACF and PACF for a time series
* description: Returns the Autocorrelation and Partial Autocorrelation function values, which are essential for identifying SARIMA model parameters.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* maxLag:
* type: integer
* description: The maximum number of lags to calculate.
* example: 40
* responses:
* '200':
* description: The calculated ACF and PACF values.
* '400':
* description: Invalid input data.
*/
app.post('/api/series/identify-correlations', (req, res) => {
try {
const { series, maxLag } = req.body;
validateSeries(series);
const acf = TimeSeriesAnalyzer.calculateACF(series.values, maxLag);
const pacf = TimeSeriesAnalyzer.calculatePACF(series.values, maxLag);
res.status(200).json({ success: true, data: { acf, pacf } });
} catch (error) {
res.status(400).json({ success: false, error: handleError(error) });
}
});
/**
* @swagger
* /api/series/decompose-stl:
* post:
* summary: Decompose a time series into components
* description: Applies Seasonal-Trend-Loess (STL) decomposition to separate the series into trend, seasonal, and residual components.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* period:
* type: integer
* description: The seasonal period of the data (e.g., 7 for weekly).
* example: 7
* responses:
* '200':
* description: The decomposed components of the time series.
* '400':
* description: Invalid input data.
*/
app.post('/api/series/decompose-stl', (req, res) => {
try {
const { series, period } = req.body;
validateSeries(series);
const result = TimeSeriesAnalyzer.stlDecomposition(series.values, period);
res.status(200).json({ success: true, data: result });
} catch (error) {
res.status(400).json({ success: false, error: handleError(error) });
}
});
/**
* @swagger
* /api/ml/kmeans:
@ -1648,6 +1802,30 @@ app.get('/api/kernels/:name', (req, res) => {
* type: number
* default: 0.1
* description: The sensitivity threshold for detecting an edge. Values below this will be set to 0.
* ARIMAOptions:
* type: object
* properties:
* p:
* type: integer
* description: Non-seasonal AutoRegressive (AR) order.
* d:
* type: integer
* description: Non-seasonal Differencing (I) order.
* q:
* type: integer
* description: Non-seasonal Moving Average (MA) order.
* P:
* type: integer
* description: Seasonal AR order.
* D:
* type: integer
* description: Seasonal Differencing order.
* Q:
* type: integer
* description: Seasonal MA order.
* s:
* type: integer
* description: The seasonal period length (e.g., 7 for weekly).
* ApiResponse:
* type: object
* properties:
@ -1780,7 +1958,7 @@ app.get('/api/docs/export/html', (req, res) => {
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse<any>);
res.status(500).json({ success: false, error: 'Internal server error' });
});
app.use('*', (req, res) => {
@ -1792,9 +1970,8 @@ app.use('*', (req, res) => {
// ========================================
app.listen(PORT, () => {
console.log(`Analytics API server running on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/api/health`);
console.log(`API Documentation: http://localhost:${PORT}/api-docs`);
console.log(`Analytics API server running on port ${PORT}`);
console.log(`API Documentation: http://localhost:${PORT}/api-docs`);
});
export default app;