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:
parent
9d2b0dc043
commit
93d192a995
3 changed files with 524 additions and 384 deletions
118
kmeans.ts
Normal file
118
kmeans.ts
Normal 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
|
||||
};
|
||||
}
|
||||
}
|
||||
470
server.ts
470
server.ts
|
|
@ -1,20 +1,32 @@
|
|||
// server.ts - Simplified main server file
|
||||
// package.json dependencies needed:
|
||||
// npm install express mathjs lodash
|
||||
// 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 * as _ from 'lodash';
|
||||
import { KMeans, Point } from './kmeans';
|
||||
import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
|
||||
|
||||
const app = express();
|
||||
app.use(express.json());
|
||||
|
||||
// Types for our data structures
|
||||
// ========================================
|
||||
// TYPE DEFINITIONS
|
||||
// ========================================
|
||||
|
||||
interface DataSeries {
|
||||
values: number[];
|
||||
labels?: string[];
|
||||
}
|
||||
|
||||
interface DataMatrix {
|
||||
data: number[][];
|
||||
columns?: string[];
|
||||
rows?: string[];
|
||||
}
|
||||
|
||||
interface Condition {
|
||||
field: string;
|
||||
operator: '>' | '<' | '=' | '>=' | '<=' | '!=';
|
||||
|
|
@ -27,72 +39,106 @@ interface ApiResponse<T> {
|
|||
error?: string;
|
||||
}
|
||||
|
||||
// Helper function for error handling
|
||||
// ========================================
|
||||
// HELPER FUNCTIONS
|
||||
// ========================================
|
||||
|
||||
const handleError = (error: unknown): string => {
|
||||
return error instanceof Error ? error.message : 'Unknown error';
|
||||
};
|
||||
|
||||
// Core statistical functions
|
||||
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 {
|
||||
|
||||
// Apply conditions to filter data
|
||||
private applyConditions(series: DataSeries, conditions: Condition[] = []): number[] {
|
||||
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
|
||||
return series.values;
|
||||
return series.values; // TODO: Implement filtering
|
||||
}
|
||||
|
||||
// Remove duplicates from series
|
||||
// Basic statistical functions
|
||||
unique(series: DataSeries): number[] {
|
||||
validateSeries(series);
|
||||
return _.uniq(series.values);
|
||||
}
|
||||
|
||||
// Calculate mean with optional conditions
|
||||
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 values with optional conditions
|
||||
count(series: DataSeries, conditions: Condition[] = []): number {
|
||||
const filteredValues = this.applyConditions(series, conditions);
|
||||
return filteredValues.length;
|
||||
validateSeries(series);
|
||||
return this.applyConditions(series, conditions).length;
|
||||
}
|
||||
|
||||
// Calculate variance
|
||||
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));
|
||||
}
|
||||
|
||||
// Calculate standard deviation
|
||||
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));
|
||||
}
|
||||
|
||||
// Calculate percentile/quantile
|
||||
percentile(
|
||||
series: DataSeries,
|
||||
percent: number,
|
||||
ascending: boolean = true,
|
||||
conditions: Condition[] = []
|
||||
): number {
|
||||
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 sorted = ascending ? _.sortBy(filteredValues) : _.sortBy(filteredValues).reverse();
|
||||
const index = (percent / 100) * (sorted.length - 1);
|
||||
const lower = Math.floor(index);
|
||||
const upper = Math.ceil(index);
|
||||
|
|
@ -101,13 +147,12 @@ class AnalyticsEngine {
|
|||
return sorted[lower] * (1 - weight) + sorted[upper] * weight;
|
||||
}
|
||||
|
||||
// Calculate median (50th percentile)
|
||||
median(series: DataSeries, conditions: Condition[] = []): number {
|
||||
return this.percentile(series, 50, true, conditions);
|
||||
}
|
||||
|
||||
// Calculate mode (most frequent value)
|
||||
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));
|
||||
|
|
@ -117,52 +162,24 @@ class AnalyticsEngine {
|
|||
.map(Number);
|
||||
}
|
||||
|
||||
// Rank values and get top N
|
||||
topN(
|
||||
series: DataSeries,
|
||||
n: number,
|
||||
ascending: boolean = false,
|
||||
conditions: Condition[] = []
|
||||
): number[] {
|
||||
const filteredValues = this.applyConditions(series, conditions);
|
||||
const sorted = ascending ?
|
||||
_.sortBy(filteredValues) :
|
||||
_.sortBy(filteredValues).reverse();
|
||||
|
||||
return sorted.slice(0, n);
|
||||
}
|
||||
|
||||
// Get maximum value
|
||||
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);
|
||||
}
|
||||
|
||||
// Get minimum value
|
||||
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);
|
||||
}
|
||||
|
||||
// Calculate percent change
|
||||
percentChange(series: DataSeries, step: number = 1): number[] {
|
||||
const values = series.values;
|
||||
const changes: number[] = [];
|
||||
|
||||
for (let i = step; i < values.length; i++) {
|
||||
const change = ((values[i] - values[i - step]) / values[i - step]) * 100;
|
||||
changes.push(change);
|
||||
}
|
||||
|
||||
return changes;
|
||||
}
|
||||
|
||||
// Basic correlation between two series
|
||||
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');
|
||||
}
|
||||
|
|
@ -182,204 +199,185 @@ class AnalyticsEngine {
|
|||
|
||||
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): { clusters: number[][][], centroids: number[][] } {
|
||||
validateMatrix(matrix);
|
||||
if (matrix.data[0].length !== 2) {
|
||||
throw new Error('K-means implementation currently only supports 2D data.');
|
||||
}
|
||||
const points = matrix.data.map(row => ({ x: row[0], y: row[1] }));
|
||||
const kmeans = new KMeans(points, nClusters);
|
||||
const result = kmeans.run();
|
||||
const centroids = result.clusters.map(c => [c.centroid.x, c.centroid.y]);
|
||||
const clusters = result.clusters.map(c => c.points.map(p => [p.x, p.y]));
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Initialize analytics engine
|
||||
const analytics = new AnalyticsEngine();
|
||||
|
||||
// API Routes
|
||||
// ========================================
|
||||
// ROUTE HELPER FUNCTION
|
||||
// ========================================
|
||||
|
||||
const createRoute = <T>(
|
||||
app: express.Application,
|
||||
method: 'get' | 'post' | 'put' | 'delete',
|
||||
path: string,
|
||||
handler: (req: express.Request) => T
|
||||
) => {
|
||||
app[method](path, (req, res) => {
|
||||
try {
|
||||
const result = handler(req);
|
||||
res.status(200).json({ success: true, data: result } as ApiResponse<T>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<T>);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// ========================================
|
||||
// API ROUTES
|
||||
// ========================================
|
||||
|
||||
app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
res.status(200).json({ status: 'OK', timestamp: new Date().toISOString() });
|
||||
});
|
||||
|
||||
// Unique values endpoint
|
||||
app.post('/api/unique', (req, res) => {
|
||||
try {
|
||||
const { series }: { series: DataSeries } = req.body;
|
||||
const result = analytics.unique(series);
|
||||
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||
}
|
||||
// Statistical function routes
|
||||
createRoute(app, 'post', '/api/unique', (req) => analytics.unique(req.body.series));
|
||||
createRoute(app, 'post', '/api/mean', (req) => analytics.mean(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/count', (req) => analytics.count(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/variance', (req) => analytics.variance(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/std', (req) => analytics.standardDeviation(req.body.series, req.body.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));
|
||||
createRoute(app, 'post', '/api/mode', (req) => analytics.mode(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/max', (req) => analytics.max(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/min', (req) => analytics.min(req.body.series, req.body.conditions));
|
||||
createRoute(app, 'post', '/api/correlation', (req) => analytics.correlation(req.body.series1, req.body.series2));
|
||||
|
||||
// Time series routes
|
||||
createRoute(app, 'post', '/api/series/moving-average', (req) => {
|
||||
const { series, windowSize } = req.body;
|
||||
return analytics.movingAverage(series, windowSize);
|
||||
});
|
||||
|
||||
// Mean calculation endpoint
|
||||
app.post('/api/mean', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.mean(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
createRoute(app, 'post', '/api/series/rolling', (req) => {
|
||||
const { series, windowSize } = req.body;
|
||||
return analytics.rolling(series, windowSize).toArray();
|
||||
});
|
||||
|
||||
// Count endpoint
|
||||
app.post('/api/count', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.count(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
// Machine learning routes
|
||||
createRoute(app, 'post', '/api/ml/kmeans', (req) => analytics.kmeans(req.body.matrix, req.body.nClusters));
|
||||
|
||||
// Time helper routes
|
||||
createRoute(app, 'post', '/api/time/week-number', (req) => {
|
||||
const { date } = req.body;
|
||||
return analytics.getWeekNumber(date);
|
||||
});
|
||||
|
||||
// Variance endpoint
|
||||
app.post('/api/variance', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.variance(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
createRoute(app, 'post', '/api/time/same-day-last-year', (req) => {
|
||||
const { date } = req.body;
|
||||
return analytics.getSameWeekDayLastYear(date);
|
||||
});
|
||||
|
||||
// Standard deviation endpoint
|
||||
app.post('/api/std', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.standardDeviation(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
// Retail analytics routes
|
||||
createRoute(app, 'post', '/api/retail/purchase-rate', (req) => analytics.purchaseRate(req.body.productPurchases, req.body.totalTransactions));
|
||||
createRoute(app, 'post', '/api/retail/lift-value', (req) => analytics.liftValue(req.body.jointPurchaseRate, req.body.productAPurchaseRate, req.body.productBPurchaseRate));
|
||||
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));
|
||||
|
||||
// Percentile endpoint
|
||||
app.post('/api/percentile', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
series,
|
||||
percent,
|
||||
ascending = true,
|
||||
conditions = []
|
||||
}: {
|
||||
series: DataSeries;
|
||||
percent: number;
|
||||
ascending?: boolean;
|
||||
conditions?: Condition[]
|
||||
} = req.body;
|
||||
// ========================================
|
||||
// ERROR HANDLING
|
||||
// ========================================
|
||||
|
||||
const result = analytics.percentile(series, percent, ascending, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
|
||||
// Median endpoint
|
||||
app.post('/api/median', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.median(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
|
||||
// Mode endpoint
|
||||
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) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||
}
|
||||
});
|
||||
|
||||
// Top N endpoint
|
||||
app.post('/api/topn', (req, res) => {
|
||||
try {
|
||||
const {
|
||||
series,
|
||||
n,
|
||||
ascending = false,
|
||||
conditions = []
|
||||
}: {
|
||||
series: DataSeries;
|
||||
n: number;
|
||||
ascending?: boolean;
|
||||
conditions?: Condition[]
|
||||
} = req.body;
|
||||
|
||||
const result = analytics.topN(series, n, ascending, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||
}
|
||||
});
|
||||
|
||||
// Max/Min endpoints
|
||||
app.post('/api/max', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.max(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
|
||||
app.post('/api/min', (req, res) => {
|
||||
try {
|
||||
const { series, conditions = [] }: { series: DataSeries; conditions?: Condition[] } = req.body;
|
||||
const result = analytics.min(series, conditions);
|
||||
res.json({ success: true, data: result } as ApiResponse<number>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
|
||||
// Percent change endpoint
|
||||
app.post('/api/percent-change', (req, res) => {
|
||||
try {
|
||||
const { series, step = 1 }: { series: DataSeries; step?: number } = req.body;
|
||||
const result = analytics.percentChange(series, step);
|
||||
res.json({ success: true, data: result } as ApiResponse<number[]>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number[]>);
|
||||
}
|
||||
});
|
||||
|
||||
// Correlation endpoint
|
||||
app.post('/api/correlation', (req, res) => {
|
||||
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>);
|
||||
} catch (error) {
|
||||
const errorMessage = handleError(error);
|
||||
res.status(400).json({ success: false, error: errorMessage } as ApiResponse<number>);
|
||||
}
|
||||
});
|
||||
|
||||
// Error handling middleware
|
||||
app.use((err: Error, req: express.Request, res: express.Response, next: express.NextFunction) => {
|
||||
console.error(err.stack);
|
||||
res.status(500).json({ success: false, error: 'Internal server error' } as ApiResponse<any>);
|
||||
});
|
||||
|
||||
// Start server
|
||||
// app.use('*', (req, res) => {
|
||||
// res.status(404).json({ success: false, error: 'Endpoint not found' } as ApiResponse<any>);
|
||||
// });
|
||||
|
||||
app.use('*', (req, res) => {
|
||||
res.status(404).json({ success: false, error: 'Endpoint not found' });
|
||||
});
|
||||
|
||||
// ========================================
|
||||
// SERVER STARTUP
|
||||
// ========================================
|
||||
|
||||
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`);
|
||||
console.log('\n=== Available Endpoints ===');
|
||||
console.log('GET /api/health');
|
||||
console.log('POST /api/mean');
|
||||
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');
|
||||
console.log('POST /api/time/same-day-last-year');
|
||||
console.log('POST /api/series/moving-average');
|
||||
console.log('... and more');
|
||||
});
|
||||
|
||||
export default app;
|
||||
24
time-helper.ts
Normal file
24
time-helper.ts
Normal 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
|
||||
};
|
||||
Loading…
Add table
Add a link
Reference in a new issue