analytics-api/kmeans.ts
raymond 93d192a995 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)
2025-09-02 04:32:29 +00:00

118 lines
No EOL
3.5 KiB
TypeScript

// 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
};
}
}