add kmeans, moving-average, temporal functions, retail functions

add 
1. "ml/kmeans" (kmeans.ts) 
2. "series/moving-average" (time-helper.ts)
3. "time/week-number", "time/same-day-last-year" (time-helper.ts)
4. 購買率"retail/purchase-rate", リフト値"retail/lift-value", 原価率"retail/cost-ratio" 値入り率"retail/gross-margin" (server.ts)
This commit is contained in:
raymond 2025-09-02 04:32:29 +00:00
parent 9d2b0dc043
commit 93d192a995
3 changed files with 524 additions and 384 deletions

118
kmeans.ts Normal file
View file

@ -0,0 +1,118 @@
// kmeans.ts - K-Means clustering algorithm
export interface Point {
x: number;
y: number;
}
export interface Cluster {
centroid: Point;
points: Point[];
}
export interface KMeansResult {
clusters: Cluster[];
iterations: number;
converged: boolean;
}
export class KMeans {
private readonly k: number;
private readonly maxIterations: number;
private readonly data: Point[];
private clusters: Cluster[] = [];
constructor(data: Point[], k: number, maxIterations: number = 50) {
this.k = k;
this.maxIterations = maxIterations;
this.data = data;
}
private static euclideanDistance(p1: Point, p2: Point): number {
const dx = p2.x - p1.x;
const dy = p2.y - p1.y;
return Math.sqrt(dx * dx + dy * dy);
}
private initializeCentroids(): void {
const centroids: Point[] = [];
const dataCopy = [...this.data];
for (let i = 0; i < this.k && dataCopy.length > 0; i++) {
const randomIndex = Math.floor(Math.random() * dataCopy.length);
const centroid = { ...dataCopy[randomIndex] };
centroids.push(centroid);
dataCopy.splice(randomIndex, 1);
}
this.clusters = centroids.map(c => ({ centroid: c, points: [] }));
}
private assignClusters(pointAssignments: number[]): boolean {
let hasChanged = false;
for (const cluster of this.clusters) {
cluster.points = [];
}
this.data.forEach((point, pointIndex) => {
let minDistance = Infinity;
let closestClusterIndex = -1;
this.clusters.forEach((cluster, clusterIndex) => {
const distance = KMeans.euclideanDistance(point, cluster.centroid);
if (distance < minDistance) {
minDistance = distance;
closestClusterIndex = clusterIndex;
}
});
if (pointAssignments[pointIndex] !== closestClusterIndex) {
hasChanged = true;
pointAssignments[pointIndex] = closestClusterIndex;
}
if (closestClusterIndex !== -1) {
this.clusters[closestClusterIndex].points.push(point);
}
});
return hasChanged;
}
private updateCentroids(): void {
for (const cluster of this.clusters) {
if (cluster.points.length === 0) continue;
const sumX = cluster.points.reduce((sum, p) => sum + p.x, 0);
const sumY = cluster.points.reduce((sum, p) => sum + p.y, 0);
cluster.centroid.x = sumX / cluster.points.length;
cluster.centroid.y = sumY / cluster.points.length;
}
}
public run(): KMeansResult {
this.initializeCentroids();
const pointAssignments = new Array(this.data.length).fill(-1);
let iterations = 0;
let converged = false;
for (let i = 0; i < this.maxIterations; i++) {
iterations = i + 1;
const hasChanged = this.assignClusters(pointAssignments);
this.updateCentroids();
if (!hasChanged) {
converged = true;
break;
}
}
return {
clusters: this.clusters,
iterations,
converged
};
}
}

766
server.ts
View file

@ -1,385 +1,383 @@
// package.json dependencies needed: // server.ts - Simplified main server file
// npm install express mathjs lodash // package.json dependencies needed:
// npm install -D @types/express @types/node @types/lodash typescript ts-node // npm install express mathjs lodash date-fns
// npm install -D @types/express @types/node @types/lodash typescript ts-node
import express from 'express';
import * as math from 'mathjs'; import express from 'express';
import * as _ from 'lodash'; import * as math from 'mathjs';
import * as _ from 'lodash';
const app = express(); import { KMeans, Point } from './kmeans';
app.use(express.json()); import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
// Types for our data structures const app = express();
interface DataSeries { app.use(express.json());
values: number[];
labels?: string[]; // ========================================
} // TYPE DEFINITIONS
// ========================================
interface Condition {
field: string; interface DataSeries {
operator: '>' | '<' | '=' | '>=' | '<=' | '!='; values: number[];
value: number | string; labels?: string[];
} }
interface ApiResponse<T> { interface DataMatrix {
success: boolean; data: number[][];
data?: T; columns?: string[];
error?: string; rows?: string[];
} }
// Helper function for error handling interface Condition {
const handleError = (error: unknown): string => { field: string;
return error instanceof Error ? error.message : 'Unknown error'; operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
}; value: number | string;
}
// Core statistical functions
class AnalyticsEngine { interface ApiResponse<T> {
success: boolean;
// Apply conditions to filter data data?: T;
private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] { error?: string;
if (conditions.length === 0) return series.values; }
// For now, just return all values - you'd implement condition logic here // ========================================
// This would involve checking conditions against associated metadata // HELPER FUNCTIONS
return series.values; // ========================================
}
const handleError = (error: unknown): string => {
// Remove duplicates from series return error instanceof Error ? error.message : 'Unknown error';
unique(series: DataSeries): number[] { };
return _.uniq(series.values);
} const validateSeries = (series: DataSeries): void => {
if (!series || !Array.isArray(series.values) || series.values.length === 0) {
// Calculate mean with optional conditions throw new Error('Series must contain at least one value');
mean(series: DataSeries, conditions: Condition[] = []): number { }
const filteredValues = this.applyConditions(series, conditions); };
if (filteredValues.length === 0) throw new Error('No data points match conditions');
const validateMatrix = (matrix: DataMatrix): void => {
return Number(math.mean(filteredValues)); if (!matrix || !Array.isArray(matrix.data) || matrix.data.length === 0) {
} throw new Error('Matrix must contain at least one row');
}
// Count values with optional conditions };
count(series: DataSeries, conditions: Condition[] = []): number {
const filteredValues = this.applyConditions(series, conditions); /**
return filteredValues.length; * A helper class to provide a fluent API for rolling window calculations.
} */
class RollingWindow {
// Calculate variance private windows: number[][];
variance(series: DataSeries, conditions: Condition[] = []): number {
const filteredValues = this.applyConditions(series, conditions); constructor(windows: number[][]) {
if (filteredValues.length === 0) throw new Error('No data points match conditions'); this.windows = windows;
}
return Number(math.variance(filteredValues));
} mean(): number[] {
return this.windows.map(window => Number(math.mean(window)));
// Calculate standard deviation }
standardDeviation(series: DataSeries, conditions: Condition[] = []): number {
const filteredValues = this.applyConditions(series, conditions); sum(): number[] {
if (filteredValues.length === 0) throw new Error('No data points match conditions'); return this.windows.map(window => _.sum(window));
}
return Number(math.std(filteredValues));
} min(): number[] {
return this.windows.map(window => Math.min(...window));
// Calculate percentile/quantile }
percentile(
series: DataSeries, max(): number[] {
percent: number, return this.windows.map(window => Math.max(...window));
ascending: boolean = true, }
conditions: Condition[] = []
): number { toArray(): number[][] {
const filteredValues = this.applyConditions(series, conditions); return this.windows;
if (filteredValues.length === 0) throw new Error('No data points match conditions'); }
}
const sorted = ascending ?
_.sortBy(filteredValues) : // ========================================
_.sortBy(filteredValues).reverse(); // ANALYTICS ENGINE (Simplified)
// ========================================
const index = (percent / 100) * (sorted.length - 1);
const lower = Math.floor(index); class AnalyticsEngine {
const upper = Math.ceil(index);
const weight = index % 1; private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] {
if (conditions.length === 0) return series.values;
return sorted[lower] * (1 - weight) + sorted[upper] * weight; return series.values; // TODO: Implement filtering
} }
// Calculate median (50th percentile) // Basic statistical functions
median(series: DataSeries, conditions: Condition[] = []): number { unique(series: DataSeries): number[] {
return this.percentile(series, 50, true, conditions); validateSeries(series);
} return _.uniq(series.values);
}
// Calculate mode (most frequent value)
mode(series: DataSeries, conditions: Condition[] = []): number[] { mean(series: DataSeries, conditions: Condition[] = []): number {
const filteredValues = this.applyConditions(series, conditions); validateSeries(series);
const frequency = _.countBy(filteredValues); const filteredValues = this.applyConditions(series, conditions);
const maxFreq = Math.max(...Object.values(frequency)); if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Number(math.mean(filteredValues));
return Object.keys(frequency) }
.filter(key => frequency[key] === maxFreq)
.map(Number); count(series: DataSeries, conditions: Condition[] = []): number {
} validateSeries(series);
return this.applyConditions(series, conditions).length;
// Rank values and get top N }
topN(
series: DataSeries, variance(series: DataSeries, conditions: Condition[] = []): number {
n: number, validateSeries(series);
ascending: boolean = false, const filteredValues = this.applyConditions(series, conditions);
conditions: Condition[] = [] if (filteredValues.length === 0) throw new Error('No data points match conditions');
): number[] { return Number(math.variance(filteredValues));
const filteredValues = this.applyConditions(series, conditions); }
const sorted = ascending ?
_.sortBy(filteredValues) : standardDeviation(series: DataSeries, conditions: Condition[] = []): number {
_.sortBy(filteredValues).reverse(); validateSeries(series);
const filteredValues = this.applyConditions(series, conditions);
return sorted.slice(0, n); if (filteredValues.length === 0) throw new Error('No data points match conditions');
} return Number(math.std(filteredValues));
}
// Get maximum value
max(series: DataSeries, conditions: Condition[] = []): number { percentile(series: DataSeries, percent: number, ascending: boolean = true, conditions: Condition[] = []): number {
const filteredValues = this.applyConditions(series, conditions); validateSeries(series);
if (filteredValues.length === 0) throw new Error('No data points match conditions'); const filteredValues = this.applyConditions(series, conditions);
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Math.max(...filteredValues);
} const sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse();
const index = (percent / 100) * (sorted.length - 1);
// Get minimum value const lower = Math.floor(index);
min(series: DataSeries, conditions: Condition[] = []): number { const upper = Math.ceil(index);
const filteredValues = this.applyConditions(series, conditions); const weight = index % 1;
if (filteredValues.length === 0) throw new Error('No data points match conditions');
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
return Math.min(...filteredValues); }
}
median(series: DataSeries, conditions: Condition[] = []): number {
// Calculate percent change return this.percentile(series, 50, true, conditions);
percentChange(series: DataSeries, step: number = 1): number[] { }
const values = series.values;
const changes: number[] = []; mode(series: DataSeries, conditions: Condition[] = []): number[] {
validateSeries(series);
for (let i = step; i < values.length; i++) { const filteredValues = this.applyConditions(series, conditions);
const change = ((values[i] - values[i - step]) / values[i - step]) * 100; const frequency = _.countBy(filteredValues);
changes.push(change); const maxFreq = Math.max(...Object.values(frequency));
}
return Object.keys(frequency)
return changes; .filter(key => frequency[key] === maxFreq)
} .map(Number);
}
// Basic correlation between two series
correlation(series1: DataSeries, series2: DataSeries): number { max(series: DataSeries, conditions: Condition[] = []): number {
if (series1.values.length !== series2.values.length) { validateSeries(series);
throw new Error('Series must have same length for correlation'); const filteredValues = this.applyConditions(series, conditions);
} if (filteredValues.length === 0) throw new Error('No data points match conditions');
return Math.max(...filteredValues);
const x = series1.values; }
const y = series2.values;
const n = x.length; min(series: DataSeries, conditions: Condition[] = []): number {
validateSeries(series);
const sumX = _.sum(x); const filteredValues = this.applyConditions(series, conditions);
const sumY = _.sum(y); if (filteredValues.length === 0) throw new Error('No data points match conditions');
const sumXY = _.sum(x.map((xi, i) => xi * y[i])); return Math.min(...filteredValues);
const sumX2 = _.sum(x.map(xi => xi * xi)); }
const sumY2 = _.sum(y.map(yi => yi * yi));
correlation(series1: DataSeries, series2: DataSeries): number {
const numerator = n * sumXY - sumX * sumY; validateSeries(series1);
const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY)); validateSeries(series2);
return numerator / denominator; if (series1.values.length !== series2.values.length) {
} throw new Error('Series must have same length for correlation');
} }
// Initialize analytics engine const x = series1.values;
const analytics = new AnalyticsEngine(); const y = series2.values;
const n = x.length;
// API Routes
app.get('/api/health', (req, res) => { const sumX = _.sum(x);
res.json({ status: 'OK', timestamp: new Date().toISOString() }); const sumY = _.sum(y);
}); const sumXY = _.sum(x.map((xi, i) => xi * y[i]));
const sumX2 = _.sum(x.map(xi => xi * xi));
// Unique values endpoint const sumY2 = _.sum(y.map(yi => yi * yi));
app.post('/api/unique', (req, res) => {
try { const numerator = n * sumXY - sumX * sumY;
const { series }: { series: DataSeries } = req.body; const denominator = Math.sqrt((n * sumX2 - sumX * sumX) * (n * sumY2 - sumY * sumY));
const result = analytics.unique(series);
res.json({ success: true, data: result } as ApiResponse<number[]>); return numerator / denominator;
} catch (error) { }
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>); // Rolling window functions
} rolling(series: DataSeries, windowSize: number): RollingWindow {
}); validateSeries(series);
if (windowSize <= 0) {
// Mean calculation endpoint throw new Error('Window size must be a positive number.');
app.post('/api/mean', (req, res) => { }
try { if (series.values.length < windowSize) {
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; return new RollingWindow([]);
const result = analytics.mean(series, conditions); }
res.json({ success: true, data: result } as ApiResponse<number>);
} catch (error) { const windows: number[][] = [];
const errorMessage = handleError(error); for (let i = 0; i <= series.values.length - windowSize; i++) {
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); const window = series.values.slice(i, i + windowSize);
} windows.push(window);
}); }
return new RollingWindow(windows);
// Count endpoint }
app.post('/api/count', (req, res) => {
try { movingAverage(series: DataSeries, windowSize: number): number[] {
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; return this.rolling(series, windowSize).mean();
const result = analytics.count(series, conditions); }
res.json({ success: true, data: result } as ApiResponse<number>);
} catch (error) { // K-means wrapper (uses imported KMeans class)
const errorMessage = handleError(error); kmeans(matrix: DataMatrix, nClusters: number): { clusters: number[][][], centroids: number[][] } {
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); validateMatrix(matrix);
} if (matrix.data[0].length !== 2) {
}); throw new Error('K-means implementation currently only supports 2D data.');
}
// Variance endpoint const points = matrix.data.map(row => ({ x: row[0], y: row[1] }));
app.post('/api/variance', (req, res) => { const kmeans = new KMeans(points, nClusters);
try { const result = kmeans.run();
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; const centroids = result.clusters.map(c => [c.centroid.x, c.centroid.y]);
const result = analytics.variance(series, conditions); const clusters = result.clusters.map(c => c.points.map(p => [p.x, p.y]));
res.json({ success: true, data: result } as ApiResponse<number>); return { clusters, centroids };
} catch (error) { }
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); // Time helper wrapper functions
} getWeekNumber(dateString: string): number {
}); return getWeekNumber(dateString);
}
// Standard deviation endpoint
app.post('/api/std', (req, res) => { getSameWeekDayLastYear(dateString: string): string {
try { return getSameWeekDayLastYear(dateString);
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; }
const result = analytics.standardDeviation(series, conditions);
res.json({ success: true, data: result } as ApiResponse<number>); // Retail functions
} catch (error) { purchaseRate(productPurchases: number, totalTransactions: number): number {
const errorMessage = handleError(error); if (totalTransactions === 0) throw new Error('Total transactions cannot be zero');
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); return (productPurchases / totalTransactions) * 100;
} }
});
liftValue(jointPurchaseRate: number, productAPurchaseRate: number, productBPurchaseRate: number): number {
// Percentile endpoint const expectedJointRate = productAPurchaseRate * productBPurchaseRate;
app.post('/api/percentile', (req, res) => { if (expectedJointRate === 0) throw new Error('Expected joint rate cannot be zero');
try { return jointPurchaseRate / expectedJointRate;
const { }
series,
percent, costRatio(cost: number, salePrice: number): number {
ascending = true, if (salePrice === 0) throw new Error('Sale price cannot be zero');
conditions = [] return cost / salePrice;
}: { }
series: DataSeries;
percent: number; grossMarginRate(salePrice: number, cost: number): number {
ascending?: boolean; if (salePrice === 0) throw new Error('Sale price cannot be zero');
conditions?: Condition[] return (salePrice - cost) / salePrice;
} = req.body; }
}
const result = analytics.percentile(series, percent, ascending, conditions);
res.json({ success: true, data: result } as ApiResponse<number>); // Initialize analytics engine
} catch (error) { const analytics = new AnalyticsEngine();
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); // ========================================
} // ROUTE HELPER FUNCTION
}); // ========================================
// Median endpoint const createRoute = <T>(
app.post('/api/median', (req, res) => { app: express.Application,
try { method: 'get' | 'post' | 'put' | 'delete',
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; path: string,
const result = analytics.median(series, conditions); handler: (req: express.Request) => T
res.json({ success: true, data: result } as ApiResponse<number>); ) => {
} catch (error) { app[method](path, (req, res) => {
const errorMessage = handleError(error); try {
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); const result = handler(req);
} res.status(200).json({ success: true, data: result } as ApiResponse<T>);
}); } catch (error) {
const errorMessage = handleError(error);
// Mode endpoint res.status(400).json({ success: false, error: errorMessage } as ApiResponse<T>);
app.post('/api/mode', (req, res) => { }
try { });
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; };
const result = analytics.mode(series, conditions);
res.json({ success: true, data: result } as ApiResponse<number[]>); // ========================================
} catch (error) { // API ROUTES
const errorMessage = handleError(error); // ========================================
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
} app.get('/api/health', (req, res) => {
}); res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
});
// Top N endpoint
app.post('/api/topn', (req, res) => { // Statistical function routes
try { createRoute(app, 'post', '/api/unique', (req) => analytics.unique(req.body.series));
const { createRoute(app, 'post', '/api/mean', (req) => analytics.mean(req.body.series, req.body.conditions));
series, createRoute(app, 'post', '/api/count', (req) => analytics.count(req.body.series, req.body.conditions));
n, createRoute(app, 'post', '/api/variance', (req) => analytics.variance(req.body.series, req.body.conditions));
ascending = false, createRoute(app, 'post', '/api/std', (req) => analytics.standardDeviation(req.body.series, req.body.conditions));
conditions = [] createRoute(app, 'post', '/api/percentile', (req) => analytics.percentile(req.body.series, req.body.percent, req.body.ascending, req.body.conditions));
}: { createRoute(app, 'post', '/api/median', (req) => analytics.median(req.body.series, req.body.conditions));
series: DataSeries; createRoute(app, 'post', '/api/mode', (req) => analytics.mode(req.body.series, req.body.conditions));
n: number; createRoute(app, 'post', '/api/max', (req) => analytics.max(req.body.series, req.body.conditions));
ascending?: boolean; createRoute(app, 'post', '/api/min', (req) => analytics.min(req.body.series, req.body.conditions));
conditions?: Condition[] createRoute(app, 'post', '/api/correlation', (req) => analytics.correlation(req.body.series1, req.body.series2));
} = req.body;
// Time series routes
const result = analytics.topN(series, n, ascending, conditions); createRoute(app, 'post', '/api/series/moving-average', (req) => {
res.json({ success: true, data: result } as ApiResponse<number[]>); const { series, windowSize } = req.body;
} catch (error) { return analytics.movingAverage(series, windowSize);
const errorMessage = handleError(error); });
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
} createRoute(app, 'post', '/api/series/rolling', (req) => {
}); const { series, windowSize } = req.body;
return analytics.rolling(series, windowSize).toArray();
// Max/Min endpoints });
app.post('/api/max', (req, res) => {
try { // Machine learning routes
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; createRoute(app, 'post', '/api/ml/kmeans', (req) => analytics.kmeans(req.body.matrix, req.body.nClusters));
const result = analytics.max(series, conditions);
res.json({ success: true, data: result } as ApiResponse<number>); // Time helper routes
} catch (error) { createRoute(app, 'post', '/api/time/week-number', (req) => {
const errorMessage = handleError(error); const { date } = req.body;
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); return analytics.getWeekNumber(date);
} });
});
createRoute(app, 'post', '/api/time/same-day-last-year', (req) => {
app.post('/api/min', (req, res) => { const { date } = req.body;
try { return analytics.getSameWeekDayLastYear(date);
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body; });
const result = analytics.min(series, conditions);
res.json({ success: true, data: result } as ApiResponse<number>); // Retail analytics routes
} catch (error) { createRoute(app, 'post', '/api/retail/purchase-rate', (req) => analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions));
const errorMessage = handleError(error); createRoute(app, 'post', '/api/retail/lift-value', (req) => analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate));
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); createRoute(app, 'post', '/api/retail/cost-ratio', (req) => analytics.costRatio(req.body.cost, req.body.salePrice));
} createRoute(app, 'post', '/api/retail/gross-margin', (req) => analytics.grossMarginRate(req.body.salePrice, req.body.cost));
});
// ========================================
// Percent change endpoint // ERROR HANDLING
app.post('/api/percent-change', (req, res) => { // ========================================
try {
const { series, step = 1 }: { series: DataSeries; step?: number } = req.body; app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
const result = analytics.percentChange(series, step); console.error(err.stack);
res.json({ success: true, data: result } as ApiResponse<number[]>); res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse<any>);
} catch (error) { });
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>); // app.use('*', (req, res) => {
} // res.status(404).json({ success: false, error: 'Endpoint not found' } as ApiResponse<any>);
}); // });
// Correlation endpoint app.use('*', (req, res) => {
app.post('/api/correlation', (req, res) => { res.status(404).json({ success: false, error: 'Endpoint not found' });
try { });
const { series1, series2 }: { series1: DataSeries; series2: DataSeries } = req.body;
const result = analytics.correlation(series1, series2); // ========================================
res.json({ success: true, data: result } as ApiResponse<number>); // SERVER STARTUP
} catch (error) { // ========================================
const errorMessage = handleError(error);
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>); const PORT = process.env.PORT || 3000;
} app.listen(PORT, () => {
}); console.log(`Analytics API server running on port ${PORT}`);
console.log(`Health check: http://localhost:${PORT}/api/health`);
// Error handling middleware console.log('\n=== Available Endpoints ===');
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => { console.log('GET /api/health');
console.error(err.stack); console.log('POST /api/mean');
res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse<any>); console.log('POST /api/variance');
}); console.log('POST /api/ml/kmeans <-- uses external kmeans.ts');
console.log('POST /api/time/week-number <-- uses external time-helper.ts');
// Start server console.log('POST /api/time/same-day-last-year');
const PORT = process.env.PORT || 3000; console.log('POST /api/series/moving-average');
app.listen(PORT, () => { console.log('... and more');
console.log(`Analytics API server running on port ${PORT}`); });
console.log(`Health check: http://localhost:${PORT}/api/health`);
});
export default app; export default app;

24
time-helper.ts Normal file
View file

@ -0,0 +1,24 @@
// 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
};