Compare commits
No commits in common. "main" and "reconstruct" have entirely different histories.
main
...
reconstruc
17 changed files with 1110 additions and 1268 deletions
|
|
@ -23,7 +23,7 @@ export interface AutoArimaResult {
|
||||||
P: number;
|
P: number;
|
||||||
D: number;
|
D: number;
|
||||||
Q: number;
|
Q: number;
|
||||||
s: number;
|
s: number; // Correctly included
|
||||||
aic: number;
|
aic: number;
|
||||||
};
|
};
|
||||||
searchLog: { p: number; d: number; q: number; P: number; D: number; Q: number; s: number; aic: number }[];
|
searchLog: { p: number; d: number; q: number; P: number; D: number; Q: number; s: number; aic: number }[];
|
||||||
46
api-documentation.html
Normal file
46
api-documentation.html
Normal file
File diff suppressed because one or more lines are too long
35
package.json
35
package.json
|
|
@ -1,35 +0,0 @@
|
||||||
{
|
|
||||||
"name": "analytics-api",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"main": "index.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"description": "",
|
|
||||||
"dependencies": {
|
|
||||||
"cors": "^2.8.5",
|
|
||||||
"date-fns": "^4.1.0",
|
|
||||||
"express": "^4.21.2",
|
|
||||||
"lodash": "^4.17.21",
|
|
||||||
"mathjs": "^14.6.0",
|
|
||||||
"swagger-ui-express": "^5.0.1"
|
|
||||||
},
|
|
||||||
"devDependencies": {
|
|
||||||
"@types/cors": "^2.8.19",
|
|
||||||
"@types/express": "^4.17.23",
|
|
||||||
"@types/jest": "^30.0.0",
|
|
||||||
"@types/lodash": "^4.17.20",
|
|
||||||
"@types/node": "^24.3.0",
|
|
||||||
"@types/swagger-jsdoc": "^6.0.4",
|
|
||||||
"@types/swagger-ui-express": "^4.1.8",
|
|
||||||
"concurrently": "^9.2.1",
|
|
||||||
"jest": "^30.1.3",
|
|
||||||
"swagger-jsdoc": "^6.2.8",
|
|
||||||
"ts-jest": "^29.4.4",
|
|
||||||
"ts-node": "^10.9.2",
|
|
||||||
"typescript": "^5.9.2"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -48,7 +48,7 @@ export function calculateLinearRegression(yValues: number[]): LinearRegressionMo
|
||||||
// Cast the result of math.sum to a Number
|
// Cast the result of math.sum to a Number
|
||||||
const correlationNumerator = Number(math.sum(xValues.map((x, i) => (x - meanX) * (yValues[i] - meanY))));
|
const correlationNumerator = Number(math.sum(xValues.map((x, i) => (x - meanX) * (yValues[i] - meanY))));
|
||||||
|
|
||||||
const correlation = correlationNumerator / ((xValues.length) * stdDevX * stdDevY);
|
const correlation = correlationNumerator / ((xValues.length - 1) * stdDevX * stdDevY);
|
||||||
|
|
||||||
const slope = correlation * (stdDevY / stdDevX);
|
const slope = correlation * (stdDevY / stdDevX);
|
||||||
const intercept = meanY - slope * meanX;
|
const intercept = meanY - slope * meanX;
|
||||||
400
server.ts
400
server.ts
|
|
@ -6,25 +6,30 @@
|
||||||
import express from 'express';
|
import express from 'express';
|
||||||
import swaggerJsdoc from 'swagger-jsdoc';
|
import swaggerJsdoc from 'swagger-jsdoc';
|
||||||
import swaggerUi from 'swagger-ui-express';
|
import swaggerUi from 'swagger-ui-express';
|
||||||
import cors from 'cors';
|
import * as math from 'mathjs';
|
||||||
|
import * as _ from 'lodash';
|
||||||
|
import cors from 'cors'; // <-- 1. IMPORT THE CORS PACKAGE
|
||||||
|
|
||||||
// Assuming these files exist in the same directory
|
// Assuming these files exist in the same directory
|
||||||
// import { KMeans, KMeansOptions } from './kmeans';
|
// import { KMeans, KMeansOptions } from './kmeans';
|
||||||
// import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
|
// import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
|
||||||
// import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction';
|
// import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction';
|
||||||
import { SignalProcessor, SmoothingOptions, EdgeDetectionOptions } from './services/signal_processing_convolution';
|
import { SignalProcessor, SmoothingOptions, EdgeDetectionOptions } from './signal_processing_convolution';
|
||||||
import { TimeSeriesAnalyzer, ARIMAOptions } from './services/timeseries';
|
import { TimeSeriesAnalyzer, ARIMAOptions } from './timeseries';
|
||||||
import { AnalysisPipelines } from './services/analysis_pipelines';
|
import { AnalysisPipelines } from './analysis_pipelines';
|
||||||
import { convolve1D, convolve2D, ConvolutionKernels } from './services/convolution';
|
import { convolve1D, convolve2D, ConvolutionKernels } from './convolution';
|
||||||
import { DataSeries, DataMatrix, Condition, ApiResponse } from './types/index';
|
|
||||||
import { handleError, validateSeries, validateMatrix } from './services/analytics_engine';
|
// Dummy interfaces/classes if the files are not present, to prevent compile errors
|
||||||
import { ForecastResult } from './services/prediction';
|
interface KMeansOptions {}
|
||||||
import { analytics } from './services/analytics_engine';
|
class KMeans { constructor(p: any, n: any, o: any) {}; run = () => ({ clusters: [] }) }
|
||||||
import { purchaseRate, liftValue, costRatio, grossMarginRate, averageSpendPerCustomer, purchaseIndex } from './services/retail_metrics';
|
const getWeekNumber = (d: string) => 1;
|
||||||
import { RollingWindow } from './services/rolling_window';
|
const getSameWeekDayLastYear = (d: string) => new Date().toISOString();
|
||||||
import { pivotTable, PivotOptions } from './services/pivot_table';
|
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) => [];
|
||||||
|
|
||||||
|
|
||||||
// Initialize Express app
|
|
||||||
const app = express();
|
const app = express();
|
||||||
app.use(express.json());
|
app.use(express.json());
|
||||||
app.use(cors()); // <-- 2. ENABLE CORS FOR ALL ROUTES
|
app.use(cors()); // <-- 2. ENABLE CORS FOR ALL ROUTES
|
||||||
|
|
@ -51,6 +56,301 @@ const swaggerSpec = swaggerJsdoc(swaggerOptions);
|
||||||
|
|
||||||
app.use('/api-docs', swaggerUi.serve, swaggerUi.setup(swaggerSpec));
|
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
|
// API ROUTES
|
||||||
// ========================================
|
// ========================================
|
||||||
|
|
@ -479,45 +779,6 @@ app.post('/api/correlation', (req, res) => {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* @swagger
|
|
||||||
* /api/pivot-table:
|
|
||||||
* post:
|
|
||||||
* summary: Generate a pivot table from records
|
|
||||||
* description: Returns a pivot table based on the provided data and options
|
|
||||||
* tags: [Data Transformation]
|
|
||||||
* requestBody:
|
|
||||||
* required: true
|
|
||||||
* content:
|
|
||||||
* application/json:
|
|
||||||
* schema:
|
|
||||||
* type: object
|
|
||||||
* properties:
|
|
||||||
* data:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: object
|
|
||||||
* description: Array of records to pivot
|
|
||||||
* options:
|
|
||||||
* $ref: '#/components/schemas/PivotOptions'
|
|
||||||
* responses:
|
|
||||||
* '200':
|
|
||||||
* description: Pivot table generated successfully
|
|
||||||
* '400':
|
|
||||||
* description: Invalid input data
|
|
||||||
*/
|
|
||||||
app.post('/api/pivot-table', (req, res) => {
|
|
||||||
try {
|
|
||||||
const { data, options } = req.body;
|
|
||||||
// You can pass analytics.mean, analytics.count, etc. as options.aggFunc if needed
|
|
||||||
const result = pivotTable(data, options);
|
|
||||||
res.status(200).json({ success: true, data: result });
|
|
||||||
} catch (error) {
|
|
||||||
const errorMessage = handleError(error);
|
|
||||||
res.status(400).json({ success: false, error: errorMessage });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @swagger
|
* @swagger
|
||||||
* /api/series/moving-average:
|
* /api/series/moving-average:
|
||||||
|
|
@ -889,7 +1150,7 @@ app.post('/api/time/same-day-last-year', (req, res) => {
|
||||||
*/
|
*/
|
||||||
app.post('/api/retail/purchase-rate', (req, res) => {
|
app.post('/api/retail/purchase-rate', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = purchaseRate(req.body.productPurchases, req.body.totalTransactions);
|
const result = analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -931,7 +1192,7 @@ app.post('/api/retail/purchase-rate', (req, res) => {
|
||||||
*/
|
*/
|
||||||
app.post('/api/retail/lift-value', (req, res) => {
|
app.post('/api/retail/lift-value', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate);
|
const result = analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -969,7 +1230,7 @@ app.post('/api/retail/lift-value', (req, res) => {
|
||||||
*/
|
*/
|
||||||
app.post('/api/retail/cost-ratio', (req, res) => {
|
app.post('/api/retail/cost-ratio', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = costRatio(req.body.cost, req.body.salePrice);
|
const result = analytics.costRatio(req.body.cost, req.body.salePrice);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -1007,7 +1268,7 @@ app.post('/api/retail/cost-ratio', (req, res) => {
|
||||||
*/
|
*/
|
||||||
app.post('/api/retail/gross-margin', (req, res) => {
|
app.post('/api/retail/gross-margin', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const result = grossMarginRate(req.body.salePrice, req.body.cost);
|
const result = analytics.grossMarginRate(req.body.salePrice, req.body.cost);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -1046,7 +1307,7 @@ app.post('/api/retail/gross-margin', (req, res) => {
|
||||||
app.post('/api/retail/average-spend', (req, res) => {
|
app.post('/api/retail/average-spend', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { totalRevenue, numberOfCustomers } = req.body;
|
const { totalRevenue, numberOfCustomers } = req.body;
|
||||||
const result = averageSpendPerCustomer(totalRevenue, numberOfCustomers);
|
const result = analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -1085,7 +1346,7 @@ app.post('/api/retail/average-spend', (req, res) => {
|
||||||
app.post('/api/retail/purchase-index', (req, res) => {
|
app.post('/api/retail/purchase-index', (req, res) => {
|
||||||
try {
|
try {
|
||||||
const { totalItemsSold, numberOfCustomers } = req.body;
|
const { totalItemsSold, numberOfCustomers } = req.body;
|
||||||
const result = purchaseIndex(totalItemsSold, numberOfCustomers);
|
const result = analytics.purchaseIndex(totalItemsSold, numberOfCustomers);
|
||||||
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
res.status(200).json({ success: true, data: result } as ApiResponse<number>);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
const errorMessage = handleError(error);
|
const errorMessage = handleError(error);
|
||||||
|
|
@ -1565,29 +1826,6 @@ app.get('/api/kernels/:name', (req, res) => {
|
||||||
* s:
|
* s:
|
||||||
* type: integer
|
* type: integer
|
||||||
* description: The seasonal period length (e.g., 7 for weekly).
|
* description: The seasonal period length (e.g., 7 for weekly).
|
||||||
* PivotOptions:
|
|
||||||
* type: object
|
|
||||||
* required:
|
|
||||||
* - index
|
|
||||||
* - columns
|
|
||||||
* - values
|
|
||||||
* properties:
|
|
||||||
* index:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
* description: Keys to use as row labels
|
|
||||||
* columns:
|
|
||||||
* type: array
|
|
||||||
* items:
|
|
||||||
* type: string
|
|
||||||
* description: Keys to use as column labels
|
|
||||||
* values:
|
|
||||||
* type: string
|
|
||||||
* description: Key to aggregate
|
|
||||||
* aggFunc:
|
|
||||||
* type: string
|
|
||||||
* description: Aggregation function name (e.g., "sum", "mean", "count")
|
|
||||||
* ApiResponse:
|
* ApiResponse:
|
||||||
* type: object
|
* type: object
|
||||||
* properties:
|
* properties:
|
||||||
|
|
|
||||||
|
|
@ -1,208 +0,0 @@
|
||||||
import * as math from 'mathjs';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
import { DataSeries, DataMatrix, Condition, ApiResponse } from '../types/index';
|
|
||||||
import { RollingWindow } from './rolling_window';
|
|
||||||
import { KMeans, KMeansOptions } from './kmeans';
|
|
||||||
import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
|
|
||||||
import { calculateLinearRegression, generateForecast, calculatePredictionIntervals, ForecastResult } from './prediction';
|
|
||||||
|
|
||||||
export const handleError = (error: unknown): string => {
|
|
||||||
return error instanceof Error ? error.message : 'Unknown error';
|
|
||||||
};
|
|
||||||
export 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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export 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');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
distinctCount(series: DataSeries, conditions: Condition[] = []): number {
|
|
||||||
validateSeries(series);
|
|
||||||
const filteredValues = this.applyConditions(series, conditions);
|
|
||||||
const uniqueValues = _.uniq(filteredValues);
|
|
||||||
return uniqueValues.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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// ========================================
|
|
||||||
// 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,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export const analytics = new AnalyticsEngine();
|
|
||||||
|
|
||||||
|
|
@ -1,36 +0,0 @@
|
||||||
import { analytics } from './analytics_engine'; // Import your analytics engine
|
|
||||||
|
|
||||||
export interface PivotOptions {
|
|
||||||
index: string[];
|
|
||||||
columns: string[];
|
|
||||||
values: string;
|
|
||||||
aggFunc?: (items: number[]) => number; // Aggregation function (e.g., analytics.mean)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function pivotTable(
|
|
||||||
data: Record<string, any>[],
|
|
||||||
options: PivotOptions
|
|
||||||
): Record<string, Record<string, number>> {
|
|
||||||
const { index, columns, values, aggFunc = arr => arr.reduce((a, b) => a + b, 0) } = options;
|
|
||||||
const cellMap: Record<string, Record<string, number[]>> = {};
|
|
||||||
|
|
||||||
data.forEach(row => {
|
|
||||||
const rowKey = index.map(k => row[k]).join('|');
|
|
||||||
const colKey = columns.map(k => row[k]).join('|');
|
|
||||||
|
|
||||||
if (!cellMap[rowKey]) cellMap[rowKey] = {};
|
|
||||||
if (!cellMap[rowKey][colKey]) cellMap[rowKey][colKey] = [];
|
|
||||||
cellMap[rowKey][colKey].push(row[values]);
|
|
||||||
});
|
|
||||||
|
|
||||||
// Apply aggregation function to each cell
|
|
||||||
const result: Record<string, Record<string, number>> = {};
|
|
||||||
Object.entries(cellMap).forEach(([rowKey, cols]) => {
|
|
||||||
result[rowKey] = {};
|
|
||||||
Object.entries(cols).forEach(([colKey, valuesArr]) => {
|
|
||||||
result[rowKey][colKey] = aggFunc(valuesArr);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
@ -1,77 +0,0 @@
|
||||||
export function purchaseIndex(totalItemsSold: number, numberOfCustomers: number): number {
|
|
||||||
if (numberOfCustomers === 0) {
|
|
||||||
throw new Error('Number of customers cannot be zero');
|
|
||||||
}
|
|
||||||
return (totalItemsSold / numberOfCustomers) * 1000;
|
|
||||||
}
|
|
||||||
|
|
||||||
export function purchaseRate(productPurchases: number, totalTransactions: number): number;
|
|
||||||
export function purchaseRate(productPurchases: number[], totalTransactions: number[]): number[];
|
|
||||||
export function purchaseRate(productPurchases: number | number[], totalTransactions: number | number[]): number | number[] {
|
|
||||||
if (Array.isArray(productPurchases) && Array.isArray(totalTransactions)) {
|
|
||||||
if (productPurchases.length !== totalTransactions.length) throw new Error('Arrays must have the same length');
|
|
||||||
return productPurchases.map((pp, i) => purchaseRate(pp, totalTransactions[i]));
|
|
||||||
}
|
|
||||||
if (typeof productPurchases === 'number' && typeof totalTransactions === 'number') {
|
|
||||||
if (totalTransactions === 0) throw new Error('Total transactions cannot be zero');
|
|
||||||
return (productPurchases / totalTransactions) * 100;
|
|
||||||
}
|
|
||||||
throw new Error('Input types must match');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number;
|
|
||||||
export function liftValue(jointPurchaseRate: number[], productAPurchaseRate: number[], productBPurchaseRate: number[]): number[];
|
|
||||||
export function liftValue(jointPurchaseRate: number | number[], productAPurchaseRate: number | number[], productBPurchaseRate: number | number[]): number | number[] {
|
|
||||||
if (Array.isArray(jointPurchaseRate) && Array.isArray(productAPurchaseRate) && Array.isArray(productBPurchaseRate)) {
|
|
||||||
if (jointPurchaseRate.length !== productAPurchaseRate.length || jointPurchaseRate.length !== productBPurchaseRate.length) throw new Error('Arrays must have the same length');
|
|
||||||
return jointPurchaseRate.map((jpr, i) => liftValue(jpr, productAPurchaseRate[i], productBPurchaseRate[i]));
|
|
||||||
}
|
|
||||||
if (typeof jointPurchaseRate === 'number' && typeof productAPurchaseRate === 'number' && typeof productBPurchaseRate === 'number') {
|
|
||||||
const expectedJointRate = productAPurchaseRate * productBPurchaseRate;
|
|
||||||
if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero');
|
|
||||||
return jointPurchaseRate / expectedJointRate;
|
|
||||||
}
|
|
||||||
throw new Error('Input types must match');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function costRatio(cost: number, salePrice: number): number;
|
|
||||||
export function costRatio(cost: number[], salePrice: number[]): number[];
|
|
||||||
export function costRatio(cost: number | number[], salePrice: number | number[]): number | number[] {
|
|
||||||
if (Array.isArray(cost) && Array.isArray(salePrice)) {
|
|
||||||
if (cost.length !== salePrice.length) throw new Error('Arrays must have the same length');
|
|
||||||
return cost.map((c, i) => costRatio(c, salePrice[i]));
|
|
||||||
}
|
|
||||||
if (typeof cost === 'number' && typeof salePrice === 'number') {
|
|
||||||
if (salePrice === 0) throw new Error('Sale price cannot be zero');
|
|
||||||
return cost / salePrice;
|
|
||||||
}
|
|
||||||
throw new Error('Input types must match');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function grossMarginRate(salePrice: number, cost: number): number;
|
|
||||||
export function grossMarginRate(salePrice: number[], cost: number[]): number[];
|
|
||||||
export function grossMarginRate(salePrice: number | number[], cost: number | number[]): number | number[] {
|
|
||||||
if (Array.isArray(salePrice) && Array.isArray(cost)) {
|
|
||||||
if (salePrice.length !== cost.length) throw new Error('Arrays must have the same length');
|
|
||||||
return salePrice.map((sp, i) => grossMarginRate(sp, cost[i]));
|
|
||||||
}
|
|
||||||
if (typeof salePrice === 'number' && typeof cost === 'number') {
|
|
||||||
if (salePrice === 0) throw new Error('Sale price cannot be zero');
|
|
||||||
return (salePrice - cost) / salePrice;
|
|
||||||
}
|
|
||||||
throw new Error('Input types must match');
|
|
||||||
}
|
|
||||||
|
|
||||||
export function averageSpendPerCustomer(totalRevenue: number, numberOfCustomers: number): number;
|
|
||||||
export function averageSpendPerCustomer(totalRevenue: number[], numberOfCustomers: number[]): number[];
|
|
||||||
export function averageSpendPerCustomer(totalRevenue: number | number[], numberOfCustomers: number | number[]): number | number[] {
|
|
||||||
if (Array.isArray(totalRevenue) && Array.isArray(numberOfCustomers)) {
|
|
||||||
if (totalRevenue.length !== numberOfCustomers.length) throw new Error('Arrays must have the same length');
|
|
||||||
return totalRevenue.map((tr, i) => averageSpendPerCustomer(tr, numberOfCustomers[i]));
|
|
||||||
}
|
|
||||||
if (typeof totalRevenue === 'number' && typeof numberOfCustomers === 'number') {
|
|
||||||
if (numberOfCustomers === 0) throw new Error('Number of customers cannot be zero');
|
|
||||||
return totalRevenue / numberOfCustomers;
|
|
||||||
}
|
|
||||||
throw new Error('Input types must match');
|
|
||||||
}
|
|
||||||
|
|
@ -1,30 +0,0 @@
|
||||||
import * as math from 'mathjs';
|
|
||||||
import * as _ from 'lodash';
|
|
||||||
|
|
||||||
export 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
@ -1,21 +0,0 @@
|
||||||
import { analytics } from '../services/analytics_engine';
|
|
||||||
|
|
||||||
describe('AnalyticsEngine', () => {
|
|
||||||
test('mean returns correct average', () => {
|
|
||||||
const series = { values: [1, 2, 3, 4, 5] };
|
|
||||||
const result = analytics.mean(series);
|
|
||||||
expect(result).toBe(3);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('max returns correct maximum', () => {
|
|
||||||
const series = { values: [1, 2, 3, 4, 5] };
|
|
||||||
const result = analytics.max(series);
|
|
||||||
expect(result).toBe(5);
|
|
||||||
});
|
|
||||||
|
|
||||||
test('min returns correct minimum', () => {
|
|
||||||
const series = { values: [1, 2, 3, 4, 5] };
|
|
||||||
const result = analytics.min(series);
|
|
||||||
expect(result).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@ -1,3 +1,5 @@
|
||||||
|
// time-helpers.ts - Date and time utility functions
|
||||||
|
|
||||||
import { getISOWeek, getISODay, subYears, setISOWeek, setISODay, isValid } from 'date-fns';
|
import { getISOWeek, getISODay, subYears, setISOWeek, setISODay, isValid } from 'date-fns';
|
||||||
|
|
||||||
export const getWeekNumber = (dateString: string): number => {
|
export const getWeekNumber = (dateString: string): number => {
|
||||||
|
|
@ -1,15 +0,0 @@
|
||||||
{
|
|
||||||
"compilerOptions": {
|
|
||||||
"target": "ES2020",
|
|
||||||
"module": "commonjs",
|
|
||||||
"strict": true,
|
|
||||||
"esModuleInterop": true,
|
|
||||||
"skipLibCheck": true,
|
|
||||||
"forceConsistentCasingInFileNames": true,
|
|
||||||
"resolveJsonModule": true,
|
|
||||||
"outDir": "./dist",
|
|
||||||
"rootDir": "./"
|
|
||||||
},
|
|
||||||
"include": ["**/*.ts"],
|
|
||||||
"exclude": ["node_modules", "dist"]
|
|
||||||
}
|
|
||||||
|
|
@ -1,22 +0,0 @@
|
||||||
export interface DataSeries {
|
|
||||||
values: number[];
|
|
||||||
labels?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface DataMatrix {
|
|
||||||
data: number[][];
|
|
||||||
columns?: string[];
|
|
||||||
rows?: string[];
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Condition {
|
|
||||||
field: string;
|
|
||||||
operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
|
|
||||||
value: number | string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface ApiResponse<T> {
|
|
||||||
success: boolean;
|
|
||||||
data?: T;
|
|
||||||
error?: string;
|
|
||||||
}
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue