analytics-api/server.ts

1800 lines
No EOL
60 KiB
TypeScript

// server.ts - Simplified main server file
// package.json dependencies needed:
// npm install express mathjs lodash date-fns swagger-jsdoc swagger-ui-express js-yaml
// npm install -D @types/express @types/node @types/lodash typescript ts-node
import express from 'express';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
import * as math from 'mathjs';
import * as _ from 'lodash';
// These imports assume the 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
interface KMeansOptions {}
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 {}
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());
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}`,
},
],
},
apis: ["./server.ts"], // Pointing to this file for Swagger docs
};
const swaggerSpec = swaggerJsdoc(swaggerOptions);
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
// ========================================
// TYPE DEFINITIONS
// ========================================
interface DataSeries {
values: number[];
labels?: string[];
}
interface DataMatrix {
data: number[][];
columns?: string[];
rows?: string[];
}
interface Condition {
field: string;
operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
value: number | string;
}
interface ApiResponse<T> {
success: boolean;
data?: T;
error?: string;
}
// ========================================
// HELPER FUNCTIONS
// ========================================
const handleError = (error: unknown): string => {
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');
}
};
const validateMatrix = (matrix: DataMatrix): void => {
if (!matrix || !Array.isArray(matrix.data) || matrix.data.length === 0) {
throw new Error('Matrix must contain at least one row');
}
};
/**
* A helper class to provide a fluent API for rolling window calculations.
*/
class RollingWindow {
private windows: number[][];
constructor(windows: number[][]) {
this.windows = windows;
}
mean(): number[] {
return this.windows.map(window => Number(math.mean(window)));
}
sum(): number[] {
return this.windows.map(window => _.sum(window));
}
min(): number[] {
return this.windows.map(window => Math.min(...window));
}
max(): number[] {
return this.windows.map(window => Math.max(...window));
}
toArray(): number[][] {
return this.windows;
}
}
// ========================================
// ANALYTICS ENGINE (Simplified)
// ========================================
class AnalyticsEngine {
private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] {
if (conditions.length === 0) return series.values;
return series.values; // TODO: Implement filtering
}
// Basic statistical functions
unique(series: DataSeries): number[] {
validateSeries(series);
return _.uniq(series.values);
}
mean(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Number(math.mean(filteredValues));
}
count(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return filteredValues.length;
}
variance(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Number(math.variance(filteredValues));
}
standardDeviation(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Number(math.std(filteredValues));
}
percentile(series: DataSeries, percent: number, ascending: boolean = true, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
const sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse();
const index = (percent / 100) * (sorted.length - 1);
const lower = Math.floor(index);
const upper = Math.ceil(index);
const weight = index % 1;
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
}
median(series: DataSeries, conditions: Condition[] = []): number {
return this.percentile(series, 50, true, conditions);
}
mode(series: DataSeries, conditions: Condition[] = []): number[] {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
const frequency = _.countBy(filteredValues);
const maxFreq = Math.max(...Object.values(frequency));
return Object.keys(frequency)
.filter(key => frequency[key] === maxFreq)
.map(Number);
}
max(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Math.max(...filteredValues);
}
min(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Math.min(...filteredValues);
}
correlation(series1: DataSeries, series2: DataSeries): number {
validateSeries(series1);
validateSeries(series2);
if (series1.values.length !== series2.values.length) {
throw new Error('Series must have same length for correlation');
}
const x = series1.values;
const y = series2.values;
const n = x.length;
const sumX = _.sum(x);
const sumY = _.sum(y);
const sumXY = _.sum(x.map((xi, i) => xi * y[i]));
const sumX2 = _.sum(x.map(xi => xi * xi));
const sumY2 = _.sum(y.map(yi => yi * yi));
const numerator = n * sumXY - sumX * sumY;
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
return numerator / denominator;
}
// Rolling window functions
rolling(series: DataSeries, windowSize: number): RollingWindow {
validateSeries(series);
if (windowSize <= 0) {
throw new Error('Window size must be a positive number.');
}
if (series.values.length < windowSize) {
return new RollingWindow([]);
}
const windows: number[][] = [];
for (let i = 0; i <= series.values.length - windowSize; i++) {
const window = series.values.slice(i, i + windowSize);
windows.push(window);
}
return new RollingWindow(windows);
}
movingAverage(series: DataSeries, windowSize: number): number[] {
return this.rolling(series, windowSize).mean();
}
// K-means wrapper (uses imported KMeans class)
kmeans(matrix: DataMatrix, nClusters: number, options: KMeansOptions = {}): { clusters: number[][][], centroids: number[][] } {
validateMatrix(matrix);
const points: number[][] = matrix.data;
// Use the new MiniBatchKMeans class
const kmeans = new KMeans(points, nClusters, options);
const result = kmeans.run();
const centroids = result.clusters.map(c => (c as any).centroid);
const clusters = result.clusters.map(c => (c as any).points);
return { clusters, centroids };
}
// Time helper wrapper functions
getWeekNumber(dateString: string): number {
return getWeekNumber(dateString);
}
getSameWeekDayLastYear(dateString: string): string {
return getSameWeekDayLastYear(dateString);
}
// Retail functions
purchaseRate(productPurchases: number, totalTransactions: number): number {
if (totalTransactions === 0) throw new Error('Total transactions cannot be zero');
return (productPurchases / totalTransactions) * 100;
}
liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number {
const expectedJointRate = productAPurchaseRate * productBPurchaseRate;
if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero');
return jointPurchaseRate / expectedJointRate;
}
costRatio(cost: number, salePrice: number): number {
if (salePrice === 0) throw new Error('Sale price cannot be zero');
return cost / salePrice;
}
grossMarginRate(salePrice: number, cost: number): number {
if (salePrice === 0) throw new Error('Sale price cannot be zero');
return (salePrice - cost) / salePrice;
}
averageSpendPerCustomer(totalRevenue: number, numberOfCustomers: number): number {
if (numberOfCustomers === 0) {
throw new Error('Number of customers cannot be zero');
}
return totalRevenue / numberOfCustomers;
}
purchaseIndex(totalItemsSold: number, numberOfCustomers: number): number {
if (numberOfCustomers === 0) {
throw new Error('Number of customers cannot be zero');
}
return (totalItemsSold / numberOfCustomers) * 1000;
}
// ========================================
// Prediction functions
// ========================================
timeSeriesForecast(series: DataSeries, forecastPeriods: number): ForecastResult {
validateSeries(series);
const model = calculateLinearRegression(series.values);
const forecast = generateForecast(model, series.values.length, forecastPeriods);
const predictionIntervals = calculatePredictionIntervals(series.values, model, forecast);
return {
forecast,
predictionIntervals,
modelParameters: {
slope: model.slope,
intercept: model.intercept,
},
};
}
}
// Initialize analytics engine
const analytics = new AnalyticsEngine();
// ========================================
// API ROUTES
// ========================================
/**
* @swagger
* /api/health:
* get:
* summary: Health check endpoint
* description: Returns the health status of the API
* tags: [Health]
* responses:
* '200':
* description: API is healthy
* content:
* application/json:
* schema:
* type: object
* properties:
* status:
* type: string
* example: OK
* timestamp:
* type: string
* format: date-time
*/
app.get('/api/health', (req, res) => {
res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});
/**
* @swagger
* /api/unique:
* post:
* summary: Get unique values from a data series
* description: Returns an array of unique values from the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* responses:
* '200':
* description: Unique values calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/unique', (req, res) => {
try {
const result = analytics.unique(req.body.series);
res.status(200).json({ success: true, data: result } as ApiResponse<number[]>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
}
});
/**
* @swagger
* /api/mean:
* post:
* summary: Calculate mean of a data series
* description: Returns the arithmetic mean of the provided data series, optionally filtered by conditions
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Mean calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/mean', (req, res) => {
try {
const result = analytics.mean(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/count:
* post:
* summary: Count data points in a series
* description: Returns the count of data points in the series, optionally filtered by conditions
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Count calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/count', (req, res) => {
try {
const result = analytics.count(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/variance:
* post:
* summary: Calculate variance of a data series
* description: Returns the variance of the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Variance calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/variance', (req, res) => {
try {
const result = analytics.variance(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/std:
* post:
* summary: Calculate standard deviation of a data series
* description: Returns the standard deviation of the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Standard deviation calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/std', (req, res) => {
try {
const result = analytics.standardDeviation(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/percentile:
* post:
* summary: Calculate percentile of a data series
* description: Returns the specified percentile of the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* percent:
* type: number
* description: Percentile to calculate (0-100)
* example: 95
* ascending:
* type: boolean
* description: Sort order
* default: true
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Percentile calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/percentile', (req, res) => {
try {
const result = analytics.percentile(req.body.series, req.body.percent, req.body.ascending, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/median:
* post:
* summary: Calculate median of a data series
* description: Returns the median (50th percentile) of the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Median calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/median', (req, res) => {
try {
const result = analytics.median(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/mode:
* post:
* summary: Calculate mode of a data series
* description: Returns the mode (most frequent values) of the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Mode calculated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/mode', (req, res) => {
try {
const result = analytics.mode(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number[]>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
}
});
/**
* @swagger
* /api/max:
* post:
* summary: Find maximum value in a data series
* description: Returns the maximum value from the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Maximum value found successfully
* '400':
* description: Invalid input data
*/
app.post('/api/max', (req, res) => {
try {
const result = analytics.max(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/min:
* post:
* summary: Find minimum value in a data series
* description: Returns the minimum value from the provided data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* conditions:
* type: array
* items:
* $ref: '#/components/schemas/Condition'
* responses:
* '200':
* description: Minimum value found successfully
* '400':
* description: Invalid input data
*/
app.post('/api/min', (req, res) => {
try {
const result = analytics.min(req.body.series, req.body.conditions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/correlation:
* post:
* summary: Calculate correlation between two data series
* description: Returns the Pearson correlation coefficient between two data series
* tags: [Statistics]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series1:
* $ref: '#/components/schemas/DataSeries'
* series2:
* $ref: '#/components/schemas/DataSeries'
* responses:
* '200':
* description: Correlation calculated successfully
* '400':
* description: Invalid input data or series have different lengths
*/
app.post('/api/correlation', (req, res) => {
try {
const result = analytics.correlation(req.body.series1, req.body.series2);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/series/moving-average:
* post:
* summary: Calculate moving average of a data series
* description: Returns the moving average of the provided data series with specified window size
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* windowSize:
* type: integer
* description: Size of the moving window
* minimum: 1
* example: 5
* responses:
* '200':
* description: Moving average calculated successfully
* '400':
* description: Invalid input data or window size
*/
app.post('/api/series/moving-average', (req, res) => {
try {
const { series, windowSize } = req.body;
const result = analytics.movingAverage(series, windowSize);
res.status(200).json({ success: true, data: result } as ApiResponse<number[]>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
}
});
/**
* @swagger
* /api/series/rolling:
* post:
* summary: Get rolling windows of a data series
* description: Returns rolling windows of the provided data series with specified window size
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* windowSize:
* type: integer
* description: Size of the rolling window
* minimum: 1
* example: 3
* responses:
* '200':
* description: Rolling windows calculated successfully
* '400':
* description: Invalid input data or window size
*/
app.post('/api/series/rolling', (req, res) => {
try {
const { series, windowSize } = req.body;
const result = analytics.rolling(series, windowSize).toArray();
res.status(200).json({ success: true, data: result } as ApiResponse<number[][]>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[][]>);
}
});
/**
* @swagger
* /api/ml/kmeans:
* post:
* summary: Perform K-means clustering
* description: Performs K-means clustering on the provided data matrix
* tags: [Machine Learning]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* matrix:
* $ref: '#/components/schemas/DataMatrix'
* nClusters:
* type: integer
* description: Number of clusters
* minimum: 1
* example: 3
* options:
* type: object
* description: K-means options
* responses:
* '200':
* description: K-means clustering completed successfully
* '400':
* description: Invalid input data
*/
app.post('/api/ml/kmeans', (req, res) => {
try {
const result = analytics.kmeans(req.body.matrix, req.body.nClusters, req.body.options);
res.status(200).json({ success: true, data: result } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<{ clusters: number[][][], centroids: number[][] }>);
}
});
/**
* @swagger
* /api/time/week-number:
* post:
* summary: Get week number from date
* description: Returns the ISO week number for the provided date string
* tags: [Time]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* date:
* type: string
* format: date
* description: Date string in ISO format
* example: "2024-03-15"
* responses:
* '200':
* description: Week number calculated successfully
* '400':
* description: Invalid date format
*/
app.post('/api/time/week-number', (req, res) => {
try {
const { date } = req.body;
const result = analytics.getWeekNumber(date);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/time/same-day-last-year:
* post:
* summary: Get same day of week from last year
* description: Returns the date string for the same day of the week from the previous year
* tags: [Time]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* date:
* type: string
* format: date
* description: Date string in ISO format
* example: "2024-03-15"
* responses:
* '200':
* description: Same day last year calculated successfully
* '400':
* description: Invalid date format
*/
app.post('/api/time/same-day-last-year', (req, res) => {
try {
const { date } = req.body;
const result = analytics.getSameWeekDayLastYear(date);
res.status(200).json({ success: true, data: result } as ApiResponse<string>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<string>);
}
});
/**
* @swagger
* /api/retail/purchase-rate:
* post:
* summary: Calculate purchase rate
* description: Calculates the purchase rate as a percentage of product purchases over total transactions
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* productPurchases:
* type: number
* description: Number of product purchases
* example: 150
* totalTransactions:
* type: number
* description: Total number of transactions
* example: 1000
* responses:
* '200':
* description: Purchase rate calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/purchase-rate', (req, res) => {
try {
const result = analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/retail/lift-value:
* post:
* summary: Calculate lift value
* description: Calculates the lift value for market basket analysis
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* jointPurchaseRate:
* type: number
* description: Joint purchase rate of both products
* example: 0.05
* productAPurchaseRate:
* type: number
* description: Purchase rate of product A
* example: 0.2
* productBPurchaseRate:
* type: number
* description: Purchase rate of product B
* example: 0.3
* responses:
* '200':
* description: Lift value calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/lift-value', (req, res) => {
try {
const result = analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/retail/cost-ratio:
* post:
* summary: Calculate cost ratio
* description: Calculates the cost ratio (cost divided by sale price)
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* cost:
* type: number
* description: Cost of the product
* example: 50
* salePrice:
* type: number
* description: Sale price of the product
* example: 100
* responses:
* '200':
* description: Cost ratio calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/cost-ratio', (req, res) => {
try {
const result = analytics.costRatio(req.body.cost, req.body.salePrice);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/retail/gross-margin:
* post:
* summary: Calculate gross margin rate
* description: Calculates the gross margin rate as a percentage
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* salePrice:
* type: number
* description: Sale price of the product
* example: 100
* cost:
* type: number
* description: Cost of the product
* example: 60
* responses:
* '200':
* description: Gross margin rate calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/gross-margin', (req, res) => {
try {
const result = analytics.grossMarginRate(req.body.salePrice, req.body.cost);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/retail/average-spend:
* post:
* summary: Calculate average spend per customer
* description: Calculates the average amount spent per customer
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* totalRevenue:
* type: number
* description: Total revenue
* example: 50000
* numberOfCustomers:
* type: number
* description: Number of customers
* example: 500
* responses:
* '200':
* description: Average spend calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/average-spend', (req, res) => {
try {
const { totalRevenue, numberOfCustomers } = req.body;
const result = analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/retail/purchase-index:
* post:
* summary: Calculate purchase index
* description: Calculates the purchase index (items per 1000 customers)
* tags: [Retail]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* totalItemsSold:
* type: number
* description: Total number of items sold
* example: 2500
* numberOfCustomers:
* type: number
* description: Number of customers
* example: 1000
* responses:
* '200':
* description: Purchase index calculated successfully
* '400':
* description: Invalid input data or division by zero
*/
app.post('/api/retail/purchase-index', (req, res) => {
try {
const { totalItemsSold, numberOfCustomers } = req.body;
const result = analytics.purchaseIndex(totalItemsSold, numberOfCustomers);
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
}
});
/**
* @swagger
* /api/predict/forecast:
* post:
* summary: Generate time series forecast
* description: Generates a forecast for time series data using linear regression
* tags: [Prediction]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* forecastPeriods:
* type: integer
* description: Number of periods to forecast
* minimum: 1
* example: 10
* responses:
* '200':
* description: Forecast generated successfully
* '400':
* description: Invalid input data
*/
app.post('/api/predict/forecast', (req, res) => {
try {
const { series, forecastPeriods } = req.body;
const result = analytics.timeSeriesForecast(series, forecastPeriods);
res.status(200).json({ success: true, data: result } as ApiResponse<ForecastResult>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<ForecastResult>);
}
});
// ========================================
// NEW SIGNAL & IMAGE PROCESSING ROUTES
// ========================================
/**
* @swagger
* /api/series/smooth:
* post:
* summary: Smooth a 1D data series
* description: Applies a smoothing filter (Gaussian or Moving Average) to a 1D data series to reduce noise.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* $ref: '#/components/schemas/SmoothingOptions'
* responses:
* '200':
* description: The smoothed data series
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiResponse'
* '400':
* description: Invalid input data
*/
app.post('/api/series/smooth', (req, res) => {
try {
const { series, options } = req.body;
validateSeries(series);
const result = SignalProcessor.smooth(series.values, options);
res.status(200).json({ success: true, data: result } as ApiResponse<number[]>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
}
});
/**
* @swagger
* /api/series/detect-peaks:
* post:
* summary: Detect peaks in a 1D data series
* description: Identifies local maxima (peaks) in a 1D data series. More robust and accurate logic.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* type: object
* properties:
* smoothWindow:
* type: integer
* description: Optional window size for Gaussian smoothing to reduce noise before peak detection.
* example: 3
* minDistance:
* type: integer
* description: The minimum number of data points between two peaks.
* example: 1
* threshold:
* type: number
* description: The minimum value for a data point to be considered a peak.
* example: 0.5
* responses:
* '200':
* description: An array of detected peak objects, each with an index and value.
* '400':
* description: Invalid input data
*/
app.post('/api/series/detect-peaks', (req, res) => {
try {
const { series, options } = req.body;
validateSeries(series);
const result = SignalProcessor.detectPeaksConvolution(series.values, options);
res.status(200).json({ success: true, data: result } as ApiResponse<any>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<any>);
}
});
/**
* @swagger
* /api/series/detect-valleys:
* post:
* summary: Detect valleys in a 1D data series
* description: Identifies local minima (valleys) in a 1D data series. More robust and accurate logic.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* type: object
* properties:
* smoothWindow:
* type: integer
* description: Optional window size for Gaussian smoothing to reduce noise before valley detection.
* example: 3
* minDistance:
* type: integer
* description: The minimum number of data points between two valleys.
* example: 1
* threshold:
* type: number
* description: The maximum value for a data point to be considered a valley.
* example: -0.5
* responses:
* '200':
* description: An array of detected valley objects, each with an index and value.
* '400':
* description: Invalid input data
*/
app.post('/api/series/detect-valleys', (req, res) => {
try {
const { series, options } = req.body;
validateSeries(series);
const result = SignalProcessor.detectValleysConvolution(series.values, options);
res.status(200).json({ success: true, data: result } as ApiResponse<any>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<any>);
}
});
/**
* @swagger
* /api/series/detect-outliers:
* post:
* summary: Detect outliers in a 1D data series
* description: Identifies outliers in a 1D data series using statistically sound methods.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* type: object
* properties:
* method:
* type: string
* enum: [local_deviation, mean_diff]
* default: local_deviation
* windowSize:
* type: integer
* default: 7
* threshold:
* type: number
* description: "The sensitivity threshold. For 'local_deviation', this is the number of standard deviations (Z-score)."
* default: 3.0
* responses:
* '200':
* description: An array of detected outlier objects.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiResponse'
* '400':
* description: Invalid input data
*/
app.post('/api/series/detect-outliers', (req, res) => {
try {
const { series, options } = req.body;
validateSeries(series);
const result = SignalProcessor.detectOutliersConvolution(series.values, options);
res.status(200).json({ success: true, data: result } as ApiResponse<any>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<any>);
}
});
/**
* @swagger
* /api/series/detect-vertices:
* post:
* summary: Detect trend vertices (turning points) in a 1D series
* description: Identifies all significant peaks and valleys in a data series trend using a robust local maxima/minima search.
* tags: [Series Operations]
* requestBody:
* required: true
* content:
* application/json:
* schema:
* type: object
* properties:
* series:
* $ref: '#/components/schemas/DataSeries'
* options:
* type: object
* properties:
* smoothingWindow:
* type: integer
* default: 5
* description: Window size for an initial Gaussian smoothing pass to reduce noise.
* threshold:
* type: number
* description: The absolute value a peak/valley must exceed to be counted.
* default: 0
* minDistance:
* type: integer
* default: 3
* description: Minimum number of data points between any two vertices.
* responses:
* '200':
* description: An array of detected vertex objects, labeled as 'peak' or 'valley'.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiResponse'
* '400':
* description: Invalid input data
*/
app.post('/api/series/detect-vertices', (req, res) => {
try {
const { series, options } = req.body;
validateSeries(series);
const result = SignalProcessor.detectTrendVertices(series.values, options);
res.status(200).json({ success: true, data: result } as ApiResponse<any>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<any>);
}
});
/**
* @swagger
* /api/kernels/{name}:
* get:
* summary: Get a pre-defined convolution kernel
* description: Retrieves a standard 1D or 2D convolution kernel by its name.
* tags: [Kernels]
* parameters:
* - in: path
* name: name
* required: true
* schema:
* type: string
* enum: [sobel-x, sobel-y, laplacian, difference1d, average1d]
* description: The name of the kernel to retrieve.
* - in: query
* name: size
* schema:
* type: integer
* default: 3
* description: The size of the kernel (for kernels like 'average1d').
* responses:
* '200':
* description: The requested kernel as a 1D or 2D array.
* content:
* application/json:
* schema:
* $ref: '#/components/schemas/ApiResponse'
* '400':
* description: Unknown kernel name or invalid options.
*/
app.get('/api/kernels/:name', (req, res) => {
try {
const kernelName = req.params.name;
const size = req.query.size ? parseInt(req.query.size as string, 10) : 3;
let kernel: number[] | number[][];
switch (kernelName) {
case 'sobel-x':
kernel = ConvolutionKernels.sobel('x');
break;
case 'sobel-y':
kernel = ConvolutionKernels.sobel('y');
break;
case 'laplacian':
kernel = ConvolutionKernels.laplacian();
break;
case 'difference1d':
kernel = ConvolutionKernels.difference1D();
break;
case 'average1d':
kernel = ConvolutionKernels.average1D(size);
break;
default:
throw new Error(`Unknown kernel name: ${kernelName}`);
}
res.status(200).json({ success: true, data: kernel } as ApiResponse<any>);
} catch (error) {
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<any>);
}
});
// ========================================
// SWAGGER COMPONENTS
// ========================================
/**
* @swagger
* components:
* schemas:
* DataSeries:
* type: object
* required:
* - values
* properties:
* values:
* type: array
* items:
* type: number
* description: Array of numerical values
* example: [1, 2, 3, 4, 5]
* labels:
* type: array
* items:
* type: string
* description: Optional labels for the values
* example: ["Jan", "Feb", "Mar", "Apr", "May"]
* DataMatrix:
* type: object
* required:
* - data
* properties:
* data:
* type: array
* items:
* type: array
* items:
* type: number
* description: 2D array of numerical values
* example: [[1, 2], [3, 4], [5, 6]]
* columns:
* type: array
* items:
* type: string
* description: Optional column names
* example: ["x", "y"]
* rows:
* type: array
* items:
* type: string
* description: Optional row names
* example: ["row1", "row2", "row3"]
* Condition:
* type: object
* required:
* - field
* - operator
* - value
* properties:
* field:
* type: string
* description: Field name to apply condition on
* example: "value"
* operator:
* type: string
* enum: [">", "<", "=", ">=", "<=", "!="]
* description: Comparison operator
* example: ">"
* value:
* oneOf:
* - type: number
* - type: string
* description: Value to compare against
* example: 10
* SmoothingOptions:
* type: object
* properties:
* method:
* type: string
* enum: [gaussian, moving_average]
* default: gaussian
* description: The smoothing method to use.
* windowSize:
* type: integer
* default: 5
* description: The size of the window for the filter. Must be an odd number for Gaussian.
* sigma:
* type: number
* default: 1.0
* description: The standard deviation for the Gaussian filter.
* EdgeDetectionOptions:
* type: object
* properties:
* method:
* type: string
* enum: [sobel, laplacian]
* default: sobel
* description: The edge detection algorithm to use.
* threshold:
* type: number
* default: 0.1
* description: The sensitivity threshold for detecting an edge. Values below this will be set to 0.
* ApiResponse:
* type: object
* properties:
* success:
* type: boolean
* description: Whether the request was successful
* data:
* description: Response data (varies by endpoint)
* error:
* type: string
* description: Error message if success is false
*/
/**
* @swagger
* /api/docs/export/json:
* get:
* summary: Export API documentation as JSON
* description: Returns the complete OpenAPI specification in JSON format
* tags: [Documentation]
* responses:
* '200':
* description: OpenAPI specification in JSON format
* content:
* application/json:
* schema:
* type: object
*/
app.get('/api/docs/export/json', (req, res) => {
res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.json"');
res.setHeader('Content-Type', 'application/json');
res.json(swaggerSpec);
});
/**
* @swagger
* /api/docs/export/yaml:
* get:
* summary: Export API documentation as YAML
* description: Returns the complete OpenAPI specification in YAML format
* tags: [Documentation]
* responses:
* '200':
* description: OpenAPI specification in YAML format
* content:
* text/yaml:
* schema:
* type: string
*/
app.get('/api/docs/export/yaml', (req, res) => {
const yaml = require('js-yaml');
const yamlString = yaml.dump(swaggerSpec, { indent: 2 });
res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.yaml"');
res.setHeader('Content-Type', 'text/yaml');
res.send(yamlString);
});
/**
* @swagger
* /api/docs/export/html:
* get:
* summary: Export API documentation as HTML
* description: Returns a standalone HTML file with the complete API documentation
* tags: [Documentation]
* responses:
* '200':
* description: Standalone HTML documentation
* content:
* text/html:
* schema:
* type: string
*/
app.get('/api/docs/export/html', (req, res) => {
const htmlTemplate = `
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>API Documentation</title>
<link rel="stylesheet" type="text/css" href="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui.css" />
<style>
html {
box-sizing: border-box;
overflow: -moz-scrollbars-vertical;
overflow-y: scroll;
}
*, *:before, *:after {
box-sizing: inherit;
}
body {
margin:0;
background: #fafafa;
}
</style>
</head>
<body>
<div id="swagger-ui"></div>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-bundle.js"></script>
<script src="https://unpkg.com/swagger-ui-dist@4.15.5/swagger-ui-standalone-preset.js"></script>
<script>
window.onload = function() {
const ui = SwaggerUIBundle({
spec: ${JSON.stringify(swaggerSpec)},
dom_id: '#swagger-ui',
deepLinking: true,
presets: [
SwaggerUIBundle.presets.apis,
SwaggerUIStandalonePreset
],
plugins: [
SwaggerUIBundle.plugins.DownloadUrl
],
layout: "StandaloneLayout"
});
};
</script>
</body>
</html>`;
res.setHeader('Content-Disposition', 'attachment; filename="api-documentation.html"');
res.setHeader('Content-Type', 'text/html');
res.send(htmlTemplate);
});
// ========================================
// ERROR HANDLING
// ========================================
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>);
});
app.use('*', (req, res) => {
res.status(404).json({ success: false, error: 'Endpoint not found' });
});
// ========================================
// SERVER STARTUP
// ========================================
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`);
});
export default app;