diff --git a/api-documentation.html b/api-documentation.html
new file mode 100644
index 0000000..6da21b0
--- /dev/null
+++ b/api-documentation.html
@@ -0,0 +1,46 @@
+
+
+
+
+
+
+ API Documentation
+
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/services/convolution.ts b/convolution.ts
similarity index 100%
rename from services/convolution.ts
rename to convolution.ts
diff --git a/services/kmeans.ts b/kmeans.ts
similarity index 97%
rename from services/kmeans.ts
rename to kmeans.ts
index a0ae502..12b85e2 100644
--- a/services/kmeans.ts
+++ b/kmeans.ts
@@ -1,144 +1,144 @@
-export type Point = number[];
-
-export interface Cluster {
- centroid: Point;
- points: Point[];
-}
-
-export interface KMeansOptions {
- batchSize?: number;
- maxIterations?: number;
- tolerance?: number;
-}
-
-export interface KMeansResult {
- clusters: Cluster[];
- iterations: number;
- converged: boolean;
-}
-
-export class KMeans {
- private readonly k: number;
- private readonly batchSize: number;
- private readonly maxIterations: number;
- private readonly tolerance: number;
- private readonly data: Point[];
- private centroids: Point[] = [];
-
- constructor(data: Point[], k: number, options: KMeansOptions = {}) {
- this.data = data;
- this.k = k;
- this.batchSize = options.batchSize ?? 32;
- this.maxIterations = options.maxIterations ?? 100;
- this.tolerance = options.tolerance ?? 0.0001;
- }
-
- private static euclideanDistance(p1: Point, p2: Point): number {
- return Math.sqrt(p1.reduce((sum, val, i) => sum + (val - p2[i]) ** 2, 0));
- }
-
- private initializeCentroids(): void {
- const dataCopy = [...this.data];
- for (let i = 0; i < this.k; i++) {
- const randomIndex = Math.floor(Math.random() * dataCopy.length);
- this.centroids.push([...dataCopy[randomIndex]]);
- dataCopy.splice(randomIndex, 1);
- }
- }
-
- /**
- * Creates a random sample of the data.
- */
- private createMiniBatch(): Point[] {
- const miniBatch: Point[] = [];
- const dataCopy = [...this.data];
- for (let i = 0; i < this.batchSize && dataCopy.length > 0; i++) {
- const randomIndex = Math.floor(Math.random() * dataCopy.length);
- miniBatch.push(dataCopy[randomIndex]);
- dataCopy.splice(randomIndex, 1);
- }
- return miniBatch;
- }
-
- /**
- * Assigns all points in the full dataset to the final centroids.
- */
- private assignFinalClusters(): Cluster[] {
- const clusters: Cluster[] = this.centroids.map(c => ({ centroid: c, points: [] }));
-
- for (const point of this.data) {
- let minDistance = Infinity;
- let closestClusterIndex = -1;
- for (let i = 0; i < this.centroids.length; i++) {
- const distance = KMeans.euclideanDistance(point, this.centroids[i]);
- if (distance < minDistance) {
- minDistance = distance;
- closestClusterIndex = i;
- }
- }
- if (closestClusterIndex !== -1) {
- clusters[closestClusterIndex].points.push(point);
- }
- }
- return clusters;
- }
-
- public run(): KMeansResult {
- this.initializeCentroids();
-
- const clusterPointCounts = new Array(this.k).fill(0);
- let converged = false;
- let iterations = 0;
-
- for (let i = 0; i < this.maxIterations; i++) {
- iterations = i + 1;
- const miniBatch = this.createMiniBatch();
- const previousCentroids = this.centroids.map(c => [...c]);
-
- // Assign points in the batch and update centroids gradually
- for (const point of miniBatch) {
- let minDistance = Infinity;
- let closestClusterIndex = -1;
-
- for (let j = 0; j < this.k; j++) {
- const distance = KMeans.euclideanDistance(point, this.centroids[j]);
- if (distance < minDistance) {
- minDistance = distance;
- closestClusterIndex = j;
- }
- }
-
- if (closestClusterIndex !== -1) {
- clusterPointCounts[closestClusterIndex]++;
- const learningRate = 1 / clusterPointCounts[closestClusterIndex];
- const centroidToUpdate = this.centroids[closestClusterIndex];
-
- // Move the centroid slightly towards the new point
- for (let dim = 0; dim < centroidToUpdate.length; dim++) {
- centroidToUpdate[dim] = (1 - learningRate) * centroidToUpdate[dim] + learningRate * point[dim];
- }
- }
- }
-
- // Check for convergence
- let totalMovement = 0;
- for(let j = 0; j < this.k; j++) {
- totalMovement += KMeans.euclideanDistance(previousCentroids[j], this.centroids[j]);
- }
-
- if (totalMovement < this.tolerance) {
- converged = true;
- break;
- }
- }
-
- // After training, assign all points to the final centroids
- const finalClusters = this.assignFinalClusters();
-
- return {
- clusters: finalClusters,
- iterations,
- converged
- };
- }
+export type Point = number[];
+
+export interface Cluster {
+ centroid: Point;
+ points: Point[];
+}
+
+export interface KMeansOptions {
+ batchSize?: number;
+ maxIterations?: number;
+ tolerance?: number;
+}
+
+export interface KMeansResult {
+ clusters: Cluster[];
+ iterations: number;
+ converged: boolean;
+}
+
+export class KMeans {
+ private readonly k: number;
+ private readonly batchSize: number;
+ private readonly maxIterations: number;
+ private readonly tolerance: number;
+ private readonly data: Point[];
+ private centroids: Point[] = [];
+
+ constructor(data: Point[], k: number, options: KMeansOptions = {}) {
+ this.data = data;
+ this.k = k;
+ this.batchSize = options.batchSize ?? 32;
+ this.maxIterations = options.maxIterations ?? 100;
+ this.tolerance = options.tolerance ?? 0.0001;
+ }
+
+ private static euclideanDistance(p1: Point, p2: Point): number {
+ return Math.sqrt(p1.reduce((sum, val, i) => sum + (val - p2[i]) ** 2, 0));
+ }
+
+ private initializeCentroids(): void {
+ const dataCopy = [...this.data];
+ for (let i = 0; i < this.k; i++) {
+ const randomIndex = Math.floor(Math.random() * dataCopy.length);
+ this.centroids.push([...dataCopy[randomIndex]]);
+ dataCopy.splice(randomIndex, 1);
+ }
+ }
+
+ /**
+ * Creates a random sample of the data.
+ */
+ private createMiniBatch(): Point[] {
+ const miniBatch: Point[] = [];
+ const dataCopy = [...this.data];
+ for (let i = 0; i < this.batchSize && dataCopy.length > 0; i++) {
+ const randomIndex = Math.floor(Math.random() * dataCopy.length);
+ miniBatch.push(dataCopy[randomIndex]);
+ dataCopy.splice(randomIndex, 1);
+ }
+ return miniBatch;
+ }
+
+ /**
+ * Assigns all points in the full dataset to the final centroids.
+ */
+ private assignFinalClusters(): Cluster[] {
+ const clusters: Cluster[] = this.centroids.map(c => ({ centroid: c, points: [] }));
+
+ for (const point of this.data) {
+ let minDistance = Infinity;
+ let closestClusterIndex = -1;
+ for (let i = 0; i < this.centroids.length; i++) {
+ const distance = KMeans.euclideanDistance(point, this.centroids[i]);
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestClusterIndex = i;
+ }
+ }
+ if (closestClusterIndex !== -1) {
+ clusters[closestClusterIndex].points.push(point);
+ }
+ }
+ return clusters;
+ }
+
+ public run(): KMeansResult {
+ this.initializeCentroids();
+
+ const clusterPointCounts = new Array(this.k).fill(0);
+ let converged = false;
+ let iterations = 0;
+
+ for (let i = 0; i < this.maxIterations; i++) {
+ iterations = i + 1;
+ const miniBatch = this.createMiniBatch();
+ const previousCentroids = this.centroids.map(c => [...c]);
+
+ // Assign points in the batch and update centroids gradually
+ for (const point of miniBatch) {
+ let minDistance = Infinity;
+ let closestClusterIndex = -1;
+
+ for (let j = 0; j < this.k; j++) {
+ const distance = KMeans.euclideanDistance(point, this.centroids[j]);
+ if (distance < minDistance) {
+ minDistance = distance;
+ closestClusterIndex = j;
+ }
+ }
+
+ if (closestClusterIndex !== -1) {
+ clusterPointCounts[closestClusterIndex]++;
+ const learningRate = 1 / clusterPointCounts[closestClusterIndex];
+ const centroidToUpdate = this.centroids[closestClusterIndex];
+
+ // Move the centroid slightly towards the new point
+ for (let dim = 0; dim < centroidToUpdate.length; dim++) {
+ centroidToUpdate[dim] = (1 - learningRate) * centroidToUpdate[dim] + learningRate * point[dim];
+ }
+ }
+ }
+
+ // Check for convergence
+ let totalMovement = 0;
+ for(let j = 0; j < this.k; j++) {
+ totalMovement += KMeans.euclideanDistance(previousCentroids[j], this.centroids[j]);
+ }
+
+ if (totalMovement < this.tolerance) {
+ converged = true;
+ break;
+ }
+ }
+
+ // After training, assign all points to the final centroids
+ const finalClusters = this.assignFinalClusters();
+
+ return {
+ clusters: finalClusters,
+ iterations,
+ converged
+ };
+ }
}
\ No newline at end of file
diff --git a/package.json b/package.json
deleted file mode 100644
index 09be592..0000000
--- a/package.json
+++ /dev/null
@@ -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"
- }
-}
diff --git a/services/prediction.ts b/prediction.ts
similarity index 94%
rename from services/prediction.ts
rename to prediction.ts
index 799c5db..eb46525 100644
--- a/services/prediction.ts
+++ b/prediction.ts
@@ -1,101 +1,101 @@
-import * as math from 'mathjs';
-
-// The structure for the returned regression model
-export interface LinearRegressionModel {
- slope: number;
- intercept: number;
- predict: (x: number) => number;
-}
-
-// The structure for the full forecast output
-export interface ForecastResult {
- forecast: number[];
- predictionIntervals: {
- upperBound: number[];
- lowerBound: number[];
- };
- modelParameters: {
- slope: number;
- intercept: number;
- };
-}
-
-/**
- * Calculates the linear regression model from a time series.
- * @param yValues The historical data points (e.g., sales per month).
- * @returns {LinearRegressionModel} An object containing the model's parameters and a predict function.
- */
-export function calculateLinearRegression(yValues: number[]): LinearRegressionModel {
- if (yValues.length < 2) {
- throw new Error('At least two data points are required for linear regression.');
- }
-
- const xValues = Array.from({ length: yValues.length }, (_, i) => i);
-
- const meanX = Number(math.mean(xValues));
- const meanY = Number(math.mean(yValues));
- const stdDevX = Number(math.std(xValues, 'uncorrected'));
- const stdDevY = Number(math.std(yValues, 'uncorrected'));
-
- // Ensure stdDevX is not zero to avoid division by zero
- if (stdDevX === 0) {
- // This happens if all xValues are the same, which is impossible in this time series context,
- // but it's good practice to handle. A vertical line has an infinite slope.
- // For simplicity, we can return a model with zero slope.
- return { slope: 0, intercept: meanY, predict: (x: number) => meanY };
- }
-
- // Cast the result of math.sum to a Number
- const correlationNumerator = Number(math.sum(xValues.map((x, i) => (x - meanX) * (yValues[i] - meanY))));
-
- const correlation = correlationNumerator / ((xValues.length) * stdDevX * stdDevY);
-
- const slope = correlation * (stdDevY / stdDevX);
- const intercept = meanY - slope * meanX;
-
- const predict = (x: number): number => slope * x + intercept;
-
- return { slope, intercept, predict };
-}
-
-/**
- * Generates a forecast for a specified number of future periods.
- * @param model The calculated linear regression model.
- * @param historicalDataLength The number of historical data points.
- * @param forecastPeriods The number of future periods to predict.
- * @returns {number[]} An array of forecasted values.
- */
-export function generateForecast(model: LinearRegressionModel, historicalDataLength: number, forecastPeriods: number): number[] {
- const forecast: number[] = [];
- const startPeriod = historicalDataLength;
-
- for (let i = 0; i < forecastPeriods; i++) {
- const futureX = startPeriod + i;
- forecast.push(model.predict(futureX));
- }
- return forecast;
-}
-
-/**
- * Calculates prediction intervals to show the range of uncertainty.
- * @param yValues The original historical data.
- * @param model The calculated linear regression model.
- * @param forecast The array of forecasted values.
- * @returns An object with upperBound and lowerBound arrays.
- */
-export function calculatePredictionIntervals(yValues: number[], model: LinearRegressionModel, forecast: number[]) {
- const n = yValues.length;
- const residualsSquaredSum = yValues.reduce((sum, y, i) => {
- const predictedY = model.predict(i);
- return sum + (y - predictedY) ** 2;
- }, 0);
- const stdError = Math.sqrt(residualsSquaredSum / (n - 2));
-
- const zScore = 1.96; // For a 95% confidence level
- const marginOfError = zScore * stdError;
-
- const upperBound = forecast.map(val => val + marginOfError);
- const lowerBound = forecast.map(val => val - marginOfError);
-
- return { upperBound, lowerBound };
+import * as math from 'mathjs';
+
+// The structure for the returned regression model
+export interface LinearRegressionModel {
+ slope: number;
+ intercept: number;
+ predict: (x: number) => number;
+}
+
+// The structure for the full forecast output
+export interface ForecastResult {
+ forecast: number[];
+ predictionIntervals: {
+ upperBound: number[];
+ lowerBound: number[];
+ };
+ modelParameters: {
+ slope: number;
+ intercept: number;
+ };
+}
+
+/**
+ * Calculates the linear regression model from a time series.
+ * @param yValues The historical data points (e.g., sales per month).
+ * @returns {LinearRegressionModel} An object containing the model's parameters and a predict function.
+ */
+export function calculateLinearRegression(yValues: number[]): LinearRegressionModel {
+ if (yValues.length < 2) {
+ throw new Error('At least two data points are required for linear regression.');
+ }
+
+ const xValues = Array.from({ length: yValues.length }, (_, i) => i);
+
+ const meanX = Number(math.mean(xValues));
+ const meanY = Number(math.mean(yValues));
+ const stdDevX = Number(math.std(xValues, 'uncorrected'));
+ const stdDevY = Number(math.std(yValues, 'uncorrected'));
+
+ // Ensure stdDevX is not zero to avoid division by zero
+ if (stdDevX === 0) {
+ // This happens if all xValues are the same, which is impossible in this time series context,
+ // but it's good practice to handle. A vertical line has an infinite slope.
+ // For simplicity, we can return a model with zero slope.
+ return { slope: 0, intercept: meanY, predict: (x: number) => meanY };
+ }
+
+ // Cast the result of math.sum to a Number
+ const correlationNumerator = Number(math.sum(xValues.map((x, i) => (x - meanX) * (yValues[i] - meanY))));
+
+ const correlation = correlationNumerator / ((xValues.length - 1) * stdDevX * stdDevY);
+
+ const slope = correlation * (stdDevY / stdDevX);
+ const intercept = meanY - slope * meanX;
+
+ const predict = (x: number): number => slope * x + intercept;
+
+ return { slope, intercept, predict };
+}
+
+/**
+ * Generates a forecast for a specified number of future periods.
+ * @param model The calculated linear regression model.
+ * @param historicalDataLength The number of historical data points.
+ * @param forecastPeriods The number of future periods to predict.
+ * @returns {number[]} An array of forecasted values.
+ */
+export function generateForecast(model: LinearRegressionModel, historicalDataLength: number, forecastPeriods: number): number[] {
+ const forecast: number[] = [];
+ const startPeriod = historicalDataLength;
+
+ for (let i = 0; i < forecastPeriods; i++) {
+ const futureX = startPeriod + i;
+ forecast.push(model.predict(futureX));
+ }
+ return forecast;
+}
+
+/**
+ * Calculates prediction intervals to show the range of uncertainty.
+ * @param yValues The original historical data.
+ * @param model The calculated linear regression model.
+ * @param forecast The array of forecasted values.
+ * @returns An object with upperBound and lowerBound arrays.
+ */
+export function calculatePredictionIntervals(yValues: number[], model: LinearRegressionModel, forecast: number[]) {
+ const n = yValues.length;
+ const residualsSquaredSum = yValues.reduce((sum, y, i) => {
+ const predictedY = model.predict(i);
+ return sum + (y - predictedY) ** 2;
+ }, 0);
+ const stdError = Math.sqrt(residualsSquaredSum / (n - 2));
+
+ const zScore = 1.96; // For a 95% confidence level
+ const marginOfError = zScore * stdError;
+
+ const upperBound = forecast.map(val => val + marginOfError);
+ const lowerBound = forecast.map(val => val - marginOfError);
+
+ return { upperBound, lowerBound };
}
\ No newline at end of file
diff --git a/server.ts b/server.ts
index 1c071f0..4991752 100644
--- a/server.ts
+++ b/server.ts
@@ -6,51 +6,350 @@
import express from 'express';
import swaggerJsdoc from 'swagger-jsdoc';
import swaggerUi from 'swagger-ui-express';
-import cors from 'cors';
+import * as math from 'mathjs';
+import * as _ from 'lodash';
-// Assuming these files exist in the same directory
+// 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 './services/signal_processing_convolution';
-import { TimeSeriesAnalyzer, ARIMAOptions } from './services/timeseries';
-import { AnalysisPipelines } from './services/analysis_pipelines';
-import { convolve1D, convolve2D, ConvolutionKernels } from './services/convolution';
-import { DataSeries, DataMatrix, Condition, ApiResponse } from './types/index';
-import { handleError, validateSeries, validateMatrix } from './services/analytics_engine';
-import { ForecastResult } from './services/prediction';
-import { analytics } from './services/analytics_engine';
-import { purchaseRate, liftValue, costRatio, grossMarginRate, averageSpendPerCustomer, purchaseIndex } from './services/retail_metrics';
-import { RollingWindow } from './services/rolling_window';
-import { pivotTable, PivotOptions } from './services/pivot_table';
+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) => [];
-// Initialize Express app
const app = express();
app.use(express.json());
-app.use(cors()); // <-- 2. ENABLE CORS FOR ALL ROUTES
const PORT = process.env.PORT || 3000;
const swaggerOptions = {
- swaggerDefinition: {
- openapi: '3.0.0',
- info: {
- title: 'My Express API',
- version: '1.0.0',
- description: 'API documentation for my awesome Express app',
+ 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}`,
+ },
+ ],
},
- servers: [
- {
- url: `http://localhost:${PORT}`,
- },
- ],
- },
- apis: ["./server.ts"], // Pointing to the correct, renamed file
+ 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 {
+ 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
// ========================================
@@ -479,45 +778,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
* /api/series/moving-average:
@@ -594,159 +854,6 @@ app.post('/api/series/rolling', (req, res) => {
}
});
-/**
- * @swagger
- * /api/series/auto-arima-find:
- * post:
- * summary: (EXPERIMENTAL) Automatically find best SARIMA parameters
- * description: Performs a grid search to find the best SARIMA parameters based on AIC. NOTE - This is a simplified estimation and may not find the true optimal model. For best results, use the identification tools and the 'manual-forecast' endpoint.
- * tags: [Series Operations]
- * requestBody:
- * required: true
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * series:
- * $ref: '#/components/schemas/DataSeries'
- * seasonalPeriod:
- * type: integer
- * description: The seasonal period of the data (e.g., 7 for weekly).
- * example: 7
- * responses:
- * '200':
- * description: The best model found and the search log.
- * '400':
- * description: Invalid input data.
- */
-app.post('/api/series/auto-arima-find', (req, res) => {
- try {
- const { series, seasonalPeriod } = req.body;
- validateSeries(series);
- const result = AnalysisPipelines.findBestArimaParameters(series.values, seasonalPeriod);
- res.status(200).json({ success: true, data: result });
- } catch (error) {
- const errorMessage = handleError(error);
- res.status(400).json({ success: false, error: errorMessage });
- }
-});
-
-/**
- * @swagger
- * /api/series/manual-forecast:
- * post:
- * summary: Generate a forecast with manually specified SARIMA parameters
- * description: This is the primary forecasting tool. It allows an expert user (who has analyzed ACF/PACF plots) to apply a specific SARIMA model to a time series and generate a forecast.
- * tags: [Series Operations]
- * requestBody:
- * required: true
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * series:
- * $ref: '#/components/schemas/DataSeries'
- * options:
- * $ref: '#/components/schemas/ARIMAOptions'
- * forecastSteps:
- * type: integer
- * description: The number of future time steps to predict.
- * example: 7
- * responses:
- * '200':
- * description: The forecast results.
- * '400':
- * description: Invalid input data
- */
-app.post('/api/series/manual-forecast', (req, res) => {
- try {
- const { series, options, forecastSteps } = req.body;
- validateSeries(series);
- const result = TimeSeriesAnalyzer.arimaForecast(series.values, options, forecastSteps);
- res.status(200).json({ success: true, data: result });
- } catch (error) {
- const errorMessage = handleError(error);
- res.status(400).json({ success: false, error: errorMessage });
- }
-});
-
-/**
- * @swagger
- * /api/series/identify-correlations:
- * post:
- * summary: Calculate ACF and PACF for a time series
- * description: Returns the Autocorrelation and Partial Autocorrelation function values, which are essential for identifying SARIMA model parameters.
- * tags: [Series Operations]
- * requestBody:
- * required: true
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * series:
- * $ref: '#/components/schemas/DataSeries'
- * maxLag:
- * type: integer
- * description: The maximum number of lags to calculate.
- * example: 40
- * responses:
- * '200':
- * description: The calculated ACF and PACF values.
- * '400':
- * description: Invalid input data.
- */
-app.post('/api/series/identify-correlations', (req, res) => {
- try {
- const { series, maxLag } = req.body;
- validateSeries(series);
- const acf = TimeSeriesAnalyzer.calculateACF(series.values, maxLag);
- const pacf = TimeSeriesAnalyzer.calculatePACF(series.values, maxLag);
- res.status(200).json({ success: true, data: { acf, pacf } });
- } catch (error) {
- res.status(400).json({ success: false, error: handleError(error) });
- }
-});
-
-/**
- * @swagger
- * /api/series/decompose-stl:
- * post:
- * summary: Decompose a time series into components
- * description: Applies Seasonal-Trend-Loess (STL) decomposition to separate the series into trend, seasonal, and residual components.
- * tags: [Series Operations]
- * requestBody:
- * required: true
- * content:
- * application/json:
- * schema:
- * type: object
- * properties:
- * series:
- * $ref: '#/components/schemas/DataSeries'
- * period:
- * type: integer
- * description: The seasonal period of the data (e.g., 7 for weekly).
- * example: 7
- * responses:
- * '200':
- * description: The decomposed components of the time series.
- * '400':
- * description: Invalid input data.
- */
-app.post('/api/series/decompose-stl', (req, res) => {
- try {
- const { series, period } = req.body;
- validateSeries(series);
- const result = TimeSeriesAnalyzer.stlDecomposition(series.values, period);
- res.status(200).json({ success: true, data: result });
- } catch (error) {
- res.status(400).json({ success: false, error: handleError(error) });
- }
-});
-
/**
* @swagger
* /api/ml/kmeans:
@@ -889,7 +996,7 @@ app.post('/api/time/same-day-last-year', (req, res) => {
*/
app.post('/api/retail/purchase-rate', (req, res) => {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -931,7 +1038,7 @@ app.post('/api/retail/purchase-rate', (req, res) => {
*/
app.post('/api/retail/lift-value', (req, res) => {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -969,7 +1076,7 @@ app.post('/api/retail/lift-value', (req, res) => {
*/
app.post('/api/retail/cost-ratio', (req, res) => {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -1007,7 +1114,7 @@ app.post('/api/retail/cost-ratio', (req, res) => {
*/
app.post('/api/retail/gross-margin', (req, res) => {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -1046,7 +1153,7 @@ app.post('/api/retail/gross-margin', (req, res) => {
app.post('/api/retail/average-spend', (req, res) => {
try {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -1085,7 +1192,7 @@ app.post('/api/retail/average-spend', (req, res) => {
app.post('/api/retail/purchase-index', (req, res) => {
try {
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);
} catch (error) {
const errorMessage = handleError(error);
@@ -1541,53 +1648,6 @@ app.get('/api/kernels/:name', (req, res) => {
* type: number
* default: 0.1
* description: The sensitivity threshold for detecting an edge. Values below this will be set to 0.
- * ARIMAOptions:
- * type: object
- * properties:
- * p:
- * type: integer
- * description: Non-seasonal AutoRegressive (AR) order.
- * d:
- * type: integer
- * description: Non-seasonal Differencing (I) order.
- * q:
- * type: integer
- * description: Non-seasonal Moving Average (MA) order.
- * P:
- * type: integer
- * description: Seasonal AR order.
- * D:
- * type: integer
- * description: Seasonal Differencing order.
- * Q:
- * type: integer
- * description: Seasonal MA order.
- * s:
- * type: integer
- * description: The seasonal period length (e.g., 7 for weekly).
- * 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:
* type: object
* properties:
@@ -1720,7 +1780,7 @@ app.get('/api/docs/export/html', (req, res) => {
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(err.stack);
- res.status(500).json({ success: false, error: 'Internal server error' });
+ res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse);
});
app.use('*', (req, res) => {
@@ -1732,8 +1792,9 @@ app.use('*', (req, res) => {
// ========================================
app.listen(PORT, () => {
- console.log(`Analytics API server running on port ${PORT}`);
- console.log(`API Documentation: http://localhost:${PORT}/api-docs`);
+ 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;
\ No newline at end of file
diff --git a/services/analysis_pipelines.ts b/services/analysis_pipelines.ts
deleted file mode 100644
index 54b277f..0000000
--- a/services/analysis_pipelines.ts
+++ /dev/null
@@ -1,133 +0,0 @@
-// analysis_pipelines.ts - High-level workflows for common analysis tasks.
-
-import { SignalProcessor } from './signal_processing_convolution';
-import { TimeSeriesAnalyzer, STLDecomposition } from './timeseries';
-
-/**
- * The comprehensive result of a denoise and detrend operation.
- */
-export interface DenoiseAndDetrendResult {
- original: number[];
- smoothed: number[];
- decomposition: STLDecomposition;
-}
-
-/**
- * The result of an automatic SARIMA parameter search.
- */
-export interface AutoArimaResult {
- bestModel: {
- p: number;
- d: number;
- q: number;
- P: number;
- D: number;
- Q: number;
- s: number;
- aic: number;
- };
- searchLog: { p: number; d: number; q: number; P: number; D: number; Q: number; s: number; aic: number }[];
-}
-
-
-/**
- * A class containing high-level analysis pipelines that combine
- * functions from various processing libraries.
- */
-export class AnalysisPipelines {
-
- /**
- * A full pipeline to take a raw signal, smooth it to remove noise,
- * and then decompose it into trend, seasonal, and residual components.
- * @param series The original time series data.
- * @param period The seasonal period for STL decomposition.
- * @param smoothWindow The window size for the initial smoothing (denoising) pass.
- * @returns An object containing the original, smoothed, and decomposed series.
- */
- static denoiseAndDetrend(series: number[], period: number, smoothWindow: number = 5): DenoiseAndDetrendResult {
- // Ensure window is odd for symmetry
- if (smoothWindow > 1 && smoothWindow % 2 === 0) {
- smoothWindow++;
- }
- const smoothed = SignalProcessor.smooth(series, {
- method: 'gaussian',
- windowSize: smoothWindow
- });
-
- const decomposition = TimeSeriesAnalyzer.stlDecomposition(smoothed, period);
-
- return {
- original: series,
- smoothed: smoothed,
- decomposition: decomposition,
- };
- }
-
- /**
- * [FINAL CORRECTED VERSION] Performs a full grid search to find the optimal SARIMA parameters.
- * This version now correctly includes 's' in the final result object.
- * @param series The original time series data.
- * @param seasonalPeriod The seasonal period of the data (e.g., 7 for weekly, 12 for monthly).
- * @returns An object containing the best model parameters and a log of the search.
- */
- static findBestArimaParameters(
- series: number[],
- seasonalPeriod: number,
- maxD: number = 1,
- maxP: number = 2,
- maxQ: number = 2,
- maxSeasonalD: number = 1,
- maxSeasonalP: number = 2,
- maxSeasonalQ: number = 2
- ): AutoArimaResult {
-
- const searchLog: any[] = [];
- let bestModel: any = { aic: Infinity };
-
- const calculateAIC = (residuals: number[], numParams: number): number => {
- const n = residuals.length;
- if (n === 0) return Infinity;
- const sse = residuals.reduce((sum, r) => sum + r * r, 0);
- if (sse < 1e-9) return -Infinity; // Perfect fit
- const logLikelihood = -n / 2 * (Math.log(2 * Math.PI) + Math.log(sse / n)) - n / 2;
- return 2 * numParams - 2 * logLikelihood;
- };
-
- // Grid search over all parameter combinations
- for (let d = 0; d <= maxD; d++) {
- for (let p = 0; p <= maxP; p++) {
- for (let q = 0; q <= maxQ; q++) {
- for (let D = 0; D <= maxSeasonalD; D++) {
- for (let P = 0; P <= maxSeasonalP; P++) {
- for (let Q = 0; Q <= maxSeasonalQ; Q++) {
- // Skip trivial models where nothing is done
- if (p === 0 && d === 0 && q === 0 && P === 0 && D === 0 && Q === 0) continue;
-
- const options = { p, d, q, P, D, Q, s: seasonalPeriod };
- try {
- const { residuals } = TimeSeriesAnalyzer.arimaForecast(series, options, 0);
- const numParams = p + q + P + Q;
- const aic = calculateAIC(residuals, numParams);
-
- // Construct the full model info object, ensuring 's' is included
- const modelInfo = { p, d, q, P, D, Q, s: seasonalPeriod, aic };
- searchLog.push(modelInfo);
-
- if (modelInfo.aic < bestModel.aic) {
- bestModel = modelInfo;
- }
- } catch (error) {
- // Skip invalid parameter combinations that cause errors
- }
- } } } } } }
-
- if (bestModel.aic === Infinity) {
- throw new Error("Could not find a suitable SARIMA model. The data may be too short or complex.");
- }
-
- // Sort the log by AIC for easier reading
- searchLog.sort((a, b) => a.aic - b.aic);
-
- return { bestModel, searchLog };
- }
-}
diff --git a/services/analytics_engine.ts b/services/analytics_engine.ts
deleted file mode 100644
index 88979d3..0000000
--- a/services/analytics_engine.ts
+++ /dev/null
@@ -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();
-
diff --git a/services/pivot_table.ts b/services/pivot_table.ts
deleted file mode 100644
index 515c5e0..0000000
--- a/services/pivot_table.ts
+++ /dev/null
@@ -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[],
- options: PivotOptions
-): Record> {
- const { index, columns, values, aggFunc = arr => arr.reduce((a, b) => a + b, 0) } = options;
- const cellMap: Record> = {};
-
- 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> = {};
- Object.entries(cellMap).forEach(([rowKey, cols]) => {
- result[rowKey] = {};
- Object.entries(cols).forEach(([colKey, valuesArr]) => {
- result[rowKey][colKey] = aggFunc(valuesArr);
- });
- });
-
- return result;
-}
\ No newline at end of file
diff --git a/services/retail_metrics.ts b/services/retail_metrics.ts
deleted file mode 100644
index 08b0b78..0000000
--- a/services/retail_metrics.ts
+++ /dev/null
@@ -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');
- }
\ No newline at end of file
diff --git a/services/rolling_window.ts b/services/rolling_window.ts
deleted file mode 100644
index 2e11e1e..0000000
--- a/services/rolling_window.ts
+++ /dev/null
@@ -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;
- }
-}
\ No newline at end of file
diff --git a/services/timeseries.ts b/services/timeseries.ts
deleted file mode 100644
index 3ceac50..0000000
--- a/services/timeseries.ts
+++ /dev/null
@@ -1,346 +0,0 @@
-// timeseries.ts - A library for time series analysis, focusing on ARIMA.
-
-// ========================================
-// TYPE DEFINITIONS
-// ========================================
-
-/**
- * Defines the parameters for an ARIMA model.
- * (p, d, q) are the non-seasonal components.
- * (P, D, Q, s) are the optional seasonal components for SARIMA.
- */
-export interface ARIMAOptions {
- p: number; // AutoRegressive (AR) order
- d: number; // Differencing (I) order
- q: number; // Moving Average (MA) order
- P?: number; // Seasonal AR order
- D?: number; // Seasonal Differencing order
- Q?: number; // Seasonal MA order
- s?: number; // Seasonal period length
-}
-
-/**
- * The result object from an ARIMA forecast.
- */
-export interface ARIMAForecastResult {
- forecast: number[]; // The predicted future values
- residuals: number[]; // The errors of the model fit on the original data
- model: ARIMAOptions; // The model parameters used
-}
-
-/**
- * The result object from an STL decomposition.
- */
-export interface STLDecomposition {
- seasonal: number[]; // The seasonal component of the series
- trend: number[]; // The trend component of the series
- residual: number[]; // The remainder/residual component
- original: number[]; // The original series, for comparison
-}
-
-
-/**
- * A class for performing time series analysis, including identification and forecasting.
- */
-export class TimeSeriesAnalyzer {
-
- // ========================================
- // 1. IDENTIFICATION METHODS
- // ========================================
-
- /**
- * Calculates the difference of a time series.
- * This is the 'I' (Integrated) part of ARIMA, used to make a series stationary.
- * @param series The input data series.
- * @param lag The lag to difference by (usually 1).
- * @returns A new, differenced time series.
- */
- static difference(series: number[], lag: number = 1): number[] {
- if (lag < 1 || !Number.isInteger(lag)) {
- throw new Error('Lag must be a positive integer.');
- }
- if (series.length <= lag) {
- return [];
- }
-
- const differenced: number[] = [];
- for (let i = lag; i < series.length; i++) {
- differenced.push(series[i] - series[i - lag]);
- }
- return differenced;
- }
-
- /**
- * Helper function to calculate the autocovariance of a series at a given lag.
- */
- private static autocovariance(series: number[], lag: number): number {
- const n = series.length;
- if (lag >= n) return 0;
- const mean = series.reduce((a, b) => a + b) / n;
- let sum = 0;
- for (let i = lag; i < n; i++) {
- sum += (series[i] - mean) * (series[i - lag] - mean);
- }
- return sum / n;
- }
-
- /**
- * Calculates the Autocorrelation Function (ACF) for a time series.
- * ACF helps in determining the 'q' parameter for an ARIMA model.
- * @param series The input data series.
- * @param maxLag The maximum number of lags to calculate.
- * @returns An array of correlation values from lag 1 to maxLag.
- */
- static calculateACF(series: number[], maxLag: number): number[] {
- if (series.length < 2) return [];
-
- const variance = this.autocovariance(series, 0);
- if (variance === 0) {
- return new Array(maxLag).fill(1);
- }
-
- const acf: number[] = [];
- for (let lag = 1; lag <= maxLag; lag++) {
- acf.push(this.autocovariance(series, lag) / variance);
- }
- return acf;
- }
-
- /**
- * Calculates the Partial Autocorrelation Function (PACF) for a time series.
- * This now uses the Durbin-Levinson algorithm for an accurate calculation.
- * PACF helps in determining the 'p' parameter for an ARIMA model.
- * @param series The input data series.
- * @param maxLag The maximum number of lags to calculate.
- * @returns An array of partial correlation values from lag 1 to maxLag.
- */
- static calculatePACF(series: number[], maxLag: number): number[] {
- const acf = this.calculateACF(series, maxLag);
- const pacf: number[] = [];
-
- if (acf.length === 0) return [];
-
- pacf.push(acf[0]); // PACF at lag 1 is the same as ACF at lag 1
-
- for (let k = 2; k <= maxLag; k++) {
- let numerator = acf[k - 1];
- let denominator = 1;
-
- const phi = new Array(k + 1).fill(0).map(() => new Array(k + 1).fill(0));
-
- for(let i=1; i<=k; i++) {
- phi[i][i] = acf[i-1];
- }
-
- for (let j = 1; j < k; j++) {
- const factor = pacf[j - 1];
- numerator -= factor * acf[k - j - 1];
- denominator -= factor * acf[j - 1];
- }
-
- if (Math.abs(denominator) < 1e-9) { // Avoid division by zero
- pacf.push(0);
- continue;
- }
-
- const pacf_k = numerator / denominator;
- pacf.push(pacf_k);
- }
-
- return pacf;
- }
-
- /**
- * Decomposes a time series using the robust Classical Additive method.
- * This version correctly isolates trend, seasonal, and residual components.
- * @param series The input data series.
- * @param period The seasonal period (e.g., 7 for daily data with a weekly cycle).
- * @returns An object containing the seasonal, trend, and residual series.
- */
- static stlDecomposition(series: number[], period: number): STLDecomposition {
- if (series.length < 2 * period) {
- throw new Error("Series must be at least twice the length of the seasonal period.");
- }
-
- // Helper for a centered moving average
- const movingAverage = (data: number[], window: number) => {
- const result = [];
- const halfWindow = Math.floor(window / 2);
- for (let i = 0; i < data.length; i++) {
- const start = Math.max(0, i - halfWindow);
- const end = Math.min(data.length, i + halfWindow + 1);
- let sum = 0;
- for (let j = start; j < end; j++) {
- sum += data[j];
- }
- result.push(sum / (end - start));
- }
- return result;
- };
-
- // Step 1: Calculate the trend using a centered moving average.
- // If period is even, we use a 2x-MA to center it correctly.
- let trend: number[];
- if (period % 2 === 0) {
- const intermediate = movingAverage(series, period);
- trend = movingAverage(intermediate, 2);
- } else {
- trend = movingAverage(series, period);
- }
-
- // Step 2: Detrend the series
- const detrended = series.map((val, i) => val - trend[i]);
-
- // Step 3: Calculate the seasonal component by averaging the detrended values for each period
- const seasonalAverages = new Array(period).fill(0);
- const seasonalCounts = new Array(period).fill(0);
- for (let i = 0; i < series.length; i++) {
- if (!isNaN(detrended[i])) {
- const seasonIndex = i % period;
- seasonalAverages[seasonIndex] += detrended[i];
- seasonalCounts[seasonIndex]++;
- }
- }
-
- for (let i = 0; i < period; i++) {
- seasonalAverages[i] /= seasonalCounts[i];
- }
-
- // Center the seasonal component to have a mean of zero
- const seasonalMean = seasonalAverages.reduce((a, b) => a + b, 0) / period;
- const centeredSeasonalAverages = seasonalAverages.map(avg => avg - seasonalMean);
-
- const seasonal = new Array(series.length).fill(0);
- for (let i = 0; i < series.length; i++) {
- seasonal[i] = centeredSeasonalAverages[i % period];
- }
-
- // Step 4: Calculate the residual component
- const residual = detrended.map((val, i) => val - seasonal[i]);
-
- return {
- original: series,
- seasonal,
- trend,
- residual,
- };
- }
-
-
- // ========================================
- // 2. FORECASTING METHODS
- // ========================================
-
- /**
- * [UPGRADED] Generates a forecast using a simplified SARIMA model.
- * This implementation now handles both non-seasonal (p,d,q) and seasonal (P,D,Q,s) components.
- * @param series The input time series data.
- * @param options The SARIMA parameters.
- * @param forecastSteps The number of future steps to predict.
- * @returns An object containing the forecast and model residuals.
- */
- static arimaForecast(series: number[], options: ARIMAOptions, forecastSteps: number): ARIMAForecastResult {
- const { p, d, q, P = 0, D = 0, Q = 0, s = 0 } = options;
-
- if (series.length < p + d + (P + D) * s + q + Q * s) {
- throw new Error("Data series is too short for the specified SARIMA order.");
- }
-
- const originalSeries = [...series];
- let differencedSeries = [...series];
- const diffLog: { lag: number, values: number[] }[] = [];
-
- // Step 1: Apply seasonal differencing 'D' times
- for (let i = 0; i < D; i++) {
- diffLog.push({ lag: s, values: differencedSeries.slice(-s) });
- differencedSeries = this.difference(differencedSeries, s);
- }
-
- // Step 2: Apply non-seasonal differencing 'd' times
- for (let i = 0; i < d; i++) {
- diffLog.push({ lag: 1, values: differencedSeries.slice(-1) });
- differencedSeries = this.difference(differencedSeries, 1);
- }
-
- const n = differencedSeries.length;
- // Simplified coefficients
- const arCoeffs = p > 0 ? new Array(p).fill(1 / p) : [];
- const maCoeffs = q > 0 ? new Array(q).fill(1 / q) : [];
- const sarCoeffs = P > 0 ? new Array(P).fill(1 / P) : [];
- const smaCoeffs = Q > 0 ? new Array(Q).fill(1 / Q) : [];
-
- const residuals: number[] = new Array(n).fill(0);
- const fitted: number[] = new Array(n).fill(0);
-
- // Step 3: Fit the model
- const startIdx = Math.max(p, q, P * s, Q * s);
- for (let t = startIdx; t < n; t++) {
- // Non-seasonal AR
- let arVal = 0;
- for (let i = 0; i < p; i++) arVal += arCoeffs[i] * differencedSeries[t - 1 - i];
-
- // Non-seasonal MA
- let maVal = 0;
- for (let i = 0; i < q; i++) maVal += maCoeffs[i] * residuals[t - 1 - i];
-
- // Seasonal AR
- let sarVal = 0;
- for (let i = 0; i < P; i++) sarVal += sarCoeffs[i] * differencedSeries[t - s * (i + 1)];
-
- // Seasonal MA
- let smaVal = 0;
- for (let i = 0; i < Q; i++) smaVal += smaCoeffs[i] * residuals[t - s * (i + 1)];
-
- fitted[t] = arVal + maVal + sarVal + smaVal;
- residuals[t] = differencedSeries[t] - fitted[t];
- }
-
- // Step 4: Generate the forecast
- const forecastDifferenced: number[] = [];
- const extendedSeries = [...differencedSeries];
- const extendedResiduals = [...residuals];
-
- for (let f = 0; f < forecastSteps; f++) {
- const t = n + f;
- let nextForecast = 0;
-
- // AR
- for (let i = 0; i < p; i++) nextForecast += arCoeffs[i] * extendedSeries[t - 1 - i];
- // MA (future residuals are 0)
- for (let i = 0; i < q; i++) nextForecast += maCoeffs[i] * extendedResiduals[t - 1 - i];
- // SAR
- for (let i = 0; i < P; i++) nextForecast += sarCoeffs[i] * extendedSeries[t - s * (i + 1)];
- // SMA
- for (let i = 0; i < Q; i++) nextForecast += smaCoeffs[i] * extendedResiduals[t - s * (i + 1)];
-
- forecastDifferenced.push(nextForecast);
- extendedSeries.push(nextForecast);
- extendedResiduals.push(0);
- }
-
- // Step 5: Invert the differencing
- let forecast = [...forecastDifferenced];
- for (let i = diffLog.length - 1; i >= 0; i--) {
- const { lag, values } = diffLog[i];
- const inverted = [];
- const fullHistory = [...originalSeries, ...forecast]; // Need a temporary full history for inversion
-
- // A simpler inversion method for forecasting
- let history = [...series];
- for (const forecastVal of forecast) {
- const lastSeasonalVal = history[history.length - lag];
- const invertedVal = forecastVal + lastSeasonalVal;
- inverted.push(invertedVal);
- history.push(invertedVal);
- }
- forecast = inverted;
- }
-
- return {
- forecast,
- residuals,
- model: options,
- };
- }
-}
-
diff --git a/services/signal_processing_convolution.ts b/signal_processing_convolution.ts
similarity index 100%
rename from services/signal_processing_convolution.ts
rename to signal_processing_convolution.ts
diff --git a/tests/analyticsEngine.test.ts b/tests/analyticsEngine.test.ts
deleted file mode 100644
index b8391f9..0000000
--- a/tests/analyticsEngine.test.ts
+++ /dev/null
@@ -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);
- });
-});
\ No newline at end of file
diff --git a/services/time-helper.ts b/time-helper.ts
similarity index 91%
rename from services/time-helper.ts
rename to time-helper.ts
index 06faa9f..b7acecc 100644
--- a/services/time-helper.ts
+++ b/time-helper.ts
@@ -1,22 +1,24 @@
-import { getISOWeek, getISODay, subYears, setISOWeek, setISODay, isValid } from 'date-fns';
-
-export const getWeekNumber = (dateString: string): number => {
- const date = new Date(dateString);
- if (!isValid(date)) {
- throw new Error('Invalid date string provided.');
- }
- return getISOWeek(date);
-};
-
-export const getSameWeekDayLastYear = (dateString: string): string => {
- const baseDate = new Date(dateString);
- if (!isValid(baseDate)) {
- throw new Error('Invalid date string provided.');
- }
- const originalWeek = getISOWeek(baseDate);
- const originalDayOfWeek = getISODay(baseDate);
- const lastYearDate = subYears(baseDate, 1);
- const dateWithWeekSet = setISOWeek(lastYearDate, originalWeek);
- const finalDate = setISODay(dateWithWeekSet, originalDayOfWeek);
- return finalDate.toISOString().split('T')[0]; // Return as YYYY-MM-DD
+// time-helpers.ts - Date and time utility functions
+
+import { getISOWeek, getISODay, subYears, setISOWeek, setISODay, isValid } from 'date-fns';
+
+export const getWeekNumber = (dateString: string): number => {
+ const date = new Date(dateString);
+ if (!isValid(date)) {
+ throw new Error('Invalid date string provided.');
+ }
+ return getISOWeek(date);
+};
+
+export const getSameWeekDayLastYear = (dateString: string): string => {
+ const baseDate = new Date(dateString);
+ if (!isValid(baseDate)) {
+ throw new Error('Invalid date string provided.');
+ }
+ const originalWeek = getISOWeek(baseDate);
+ const originalDayOfWeek = getISODay(baseDate);
+ const lastYearDate = subYears(baseDate, 1);
+ const dateWithWeekSet = setISOWeek(lastYearDate, originalWeek);
+ const finalDate = setISODay(dateWithWeekSet, originalDayOfWeek);
+ return finalDate.toISOString().split('T')[0]; // Return as YYYY-MM-DD
};
\ No newline at end of file
diff --git a/tsconfig.json b/tsconfig.json
deleted file mode 100644
index 0d6c2f4..0000000
--- a/tsconfig.json
+++ /dev/null
@@ -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"]
-}
\ No newline at end of file
diff --git a/types/index.ts b/types/index.ts
deleted file mode 100644
index 8cf56d2..0000000
--- a/types/index.ts
+++ /dev/null
@@ -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 {
- success: boolean;
- data?: T;
- error?: string;
-}
\ No newline at end of file