89 lines
3.6 KiB
JavaScript
89 lines
3.6 KiB
JavaScript
class DynamicClusterDeviation {
|
|
constructor() {
|
|
this.clusters = []; // Stores clusters as { center, spread, count }
|
|
}
|
|
|
|
update(value) {
|
|
console.log(`\nProcessing value: ${value}`);
|
|
|
|
// If no clusters exist, create the first one
|
|
if (this.clusters.length === 0) {
|
|
this.clusters.push({ center: value, spread: 0, count: 1 });
|
|
console.log(` → First cluster created at ${value}`);
|
|
return { value, isOutlier: false };
|
|
}
|
|
|
|
// Step 1: Find the closest cluster
|
|
let bestMatch = null;
|
|
let minDistance = Infinity;
|
|
|
|
for (const cluster of this.clusters) {
|
|
const distance = Math.abs(value - cluster.center);
|
|
console.log(` Checking against cluster at ${cluster.center} (spread: ${cluster.spread}, count: ${cluster.count}) → distance: ${distance}`);
|
|
|
|
if (distance < minDistance) {
|
|
bestMatch = cluster;
|
|
minDistance = distance;
|
|
}
|
|
}
|
|
|
|
console.log(` Closest cluster found at ${bestMatch.center} with distance: ${minDistance}`);
|
|
|
|
// Step 2: Compute dynamic threshold
|
|
const dynamicThreshold = 1 + 5 / Math.sqrt(bestMatch.count + 1);
|
|
const allowedDeviation = dynamicThreshold * (bestMatch.spread || 1);
|
|
|
|
console.log(` Dynamic threshold: ${dynamicThreshold.toFixed(2)}, Allowed deviation: ${allowedDeviation.toFixed(2)}`);
|
|
|
|
// Step 3: Check if value fits within the dynamically adjusted cluster spread
|
|
if (minDistance <= allowedDeviation) {
|
|
// Update cluster dynamically
|
|
const newCenter = (bestMatch.center * bestMatch.count + value) / (bestMatch.count + 1);
|
|
const newSpread = Math.max(bestMatch.spread, minDistance);
|
|
bestMatch.center = newCenter;
|
|
bestMatch.spread = newSpread;
|
|
bestMatch.count += 1;
|
|
|
|
console.log(` ✅ Value fits in cluster! Updating cluster:`);
|
|
console.log(` → New center: ${newCenter.toFixed(2)}`);
|
|
console.log(` → New spread: ${newSpread.toFixed(2)}`);
|
|
console.log(` → New count: ${bestMatch.count}`);
|
|
|
|
return { value, isOutlier: false };
|
|
} else {
|
|
// If too far, create a new cluster
|
|
this.clusters.push({ center: value, spread: 0, count: 1 });
|
|
|
|
console.log(` ❌ Outlier detected! New cluster created at ${value}`);
|
|
|
|
return { value, isOutlier: true };
|
|
}
|
|
}
|
|
}
|
|
|
|
// Rolling window simulation with outlier detection
|
|
/*
|
|
const detector = new DynamicClusterDeviation();
|
|
const dataStream = [10, 10.2, 10.5, 9.8, 11, 50, 10.3, 200, 201, 200.1, 205, 202, 250, 260, 270, 280, 290, 300];
|
|
|
|
// Define the number of elements per rolling window chunk.
|
|
const windowSize = 5;
|
|
let rollingWindow = [];
|
|
|
|
dataStream.forEach((value, index) => {
|
|
console.log(`\n=== Processing value ${index + 1} ===`);
|
|
rollingWindow.push(value);
|
|
const result = detector.update(value);
|
|
console.log(`Current rolling window: [${rollingWindow.join(', ')}]`);
|
|
console.log(`Result: value=${result.value} (${result.isOutlier ? 'Outlier' : 'Inlier'})`);
|
|
|
|
// Once the window size is reached, show current cluster states and reset the window for the next chunk.
|
|
if (rollingWindow.length === windowSize) {
|
|
console.log("\n--- Rolling window chunk finished ---");
|
|
console.log("Detector cluster states:", JSON.stringify(detector.clusters, null, 2));
|
|
rollingWindow = [];
|
|
}
|
|
});
|
|
|
|
console.log("\nFinal detector cluster states:", JSON.stringify(detector.clusters, null, 2));
|
|
*/ |