modify kmeans, add retailing functions

modify the kmeans to mini-batch k-means "ml/kmeans"
add PI値 "retail/purchase-index", 平均客単価 "retail/average-spend"
This commit is contained in:
raymond 2025-09-02 07:04:53 +00:00
parent 93d192a995
commit faa546d474
2 changed files with 124 additions and 71 deletions

View file

@ -6,7 +6,7 @@
import express from 'express';
import * as math from 'mathjs';
import * as _ from 'lodash';
import { KMeans, Point } from './kmeans';
import { KMeans, KMeansOptions } from './kmeans';
import { getWeekNumber, getSameWeekDayLastYear } from './time-helper';
const app = express();
@ -223,18 +223,19 @@ class AnalyticsEngine {
}
// 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 };
}
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.centroid);
const clusters = result.clusters.map(c => c.points);
return { clusters, centroids };
}
// Time helper wrapper functions
getWeekNumber(dateString: string): number {
@ -266,6 +267,20 @@ class AnalyticsEngine {
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;
}
}
// Initialize analytics engine
@ -325,7 +340,9 @@ createRoute(app, 'post', '/api/series/rolling', (req) => {
});
// Machine learning routes
createRoute(app, 'post', '/api/ml/kmeans', (req) => analytics.kmeans(req.body.matrix, req.body.nClusters));
createRoute(app, 'post', '/api/ml/kmeans', (req) => {
return analytics.kmeans(req.body.matrix, req.body.nClusters, req.body.options);
});
// Time helper routes
createRoute(app, 'post', '/api/time/week-number', (req) => {
@ -344,6 +361,16 @@ createRoute(app, 'post', '/api/retail/lift-value', (req) => analytics.liftValue(
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));
createRoute(app, 'post', '/api/retail/average-spend', (req) => {
const { totalRevenue, numberOfCustomers } = req.body;
return analytics.averageSpendPerCustomer(totalRevenue, numberOfCustomers);
});
createRoute(app, 'post', '/api/retail/purchase-index', (req) => {
const { totalItemsSold, numberOfCustomers } = req.body;
return analytics.purchaseIndex(totalItemsSold, numberOfCustomers);
});
// ========================================
// ERROR HANDLING
// ========================================