mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 10:11:13 -05:00
Add Advanced ML Anomaly Detection
Enhance detect-anomalies with advanced ML algorithms (Isolation Forest, seasonal decomposition, predictive modeling) and schedule frequent runs via pg_cron. Updates include implementing new detectors, ensemble logic, and plumbing to run and expose results through the anomaly detection UI and data hooks.
This commit is contained in:
@@ -32,8 +32,181 @@ interface AnomalyResult {
|
|||||||
anomalyValue: number;
|
anomalyValue: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Statistical anomaly detection algorithms
|
// Advanced ML-based anomaly detection algorithms
|
||||||
class AnomalyDetector {
|
class AnomalyDetector {
|
||||||
|
// Isolation Forest approximation: Detects outliers based on isolation score
|
||||||
|
static isolationForest(data: number[], currentValue: number, sensitivity: number = 0.6): AnomalyResult {
|
||||||
|
if (data.length < 10) {
|
||||||
|
return { isAnomaly: false, anomalyType: 'none', deviationScore: 0, confidenceScore: 0, algorithm: 'isolation_forest', baselineValue: currentValue, anomalyValue: currentValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate isolation score (simplified version)
|
||||||
|
// Based on how different the value is from random samples
|
||||||
|
const samples = 20;
|
||||||
|
let isolationScore = 0;
|
||||||
|
|
||||||
|
for (let i = 0; i < samples; i++) {
|
||||||
|
const randomSample = data[Math.floor(Math.random() * data.length)];
|
||||||
|
const distance = Math.abs(currentValue - randomSample);
|
||||||
|
isolationScore += distance;
|
||||||
|
}
|
||||||
|
|
||||||
|
isolationScore = isolationScore / samples;
|
||||||
|
|
||||||
|
// Normalize by standard deviation
|
||||||
|
const mean = data.reduce((sum, val) => sum + val, 0) / data.length;
|
||||||
|
const variance = data.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / data.length;
|
||||||
|
const stdDev = Math.sqrt(variance);
|
||||||
|
|
||||||
|
const normalizedScore = stdDev > 0 ? isolationScore / stdDev : 0;
|
||||||
|
const isAnomaly = normalizedScore > (1 / sensitivity);
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAnomaly,
|
||||||
|
anomalyType: currentValue > mean ? 'outlier_high' : 'outlier_low',
|
||||||
|
deviationScore: normalizedScore,
|
||||||
|
confidenceScore: Math.min(normalizedScore / 5, 1),
|
||||||
|
algorithm: 'isolation_forest',
|
||||||
|
baselineValue: mean,
|
||||||
|
anomalyValue: currentValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Seasonal decomposition: Detects anomalies considering seasonal patterns
|
||||||
|
static seasonalDecomposition(data: number[], currentValue: number, sensitivity: number = 2.5, period: number = 24): AnomalyResult {
|
||||||
|
if (data.length < period * 2) {
|
||||||
|
return { isAnomaly: false, anomalyType: 'none', deviationScore: 0, confidenceScore: 0, algorithm: 'seasonal', baselineValue: currentValue, anomalyValue: currentValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Calculate seasonal component (average of values at same position in period)
|
||||||
|
const position = data.length % period;
|
||||||
|
const seasonalValues: number[] = [];
|
||||||
|
|
||||||
|
for (let i = position; i < data.length; i += period) {
|
||||||
|
seasonalValues.push(data[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
const seasonalMean = seasonalValues.reduce((sum, val) => sum + val, 0) / seasonalValues.length;
|
||||||
|
const seasonalStdDev = Math.sqrt(
|
||||||
|
seasonalValues.reduce((sum, val) => sum + Math.pow(val - seasonalMean, 2), 0) / seasonalValues.length
|
||||||
|
);
|
||||||
|
|
||||||
|
if (seasonalStdDev === 0) {
|
||||||
|
return { isAnomaly: false, anomalyType: 'none', deviationScore: 0, confidenceScore: 0, algorithm: 'seasonal', baselineValue: seasonalMean, anomalyValue: currentValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
const deviationScore = Math.abs(currentValue - seasonalMean) / seasonalStdDev;
|
||||||
|
const isAnomaly = deviationScore > sensitivity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAnomaly,
|
||||||
|
anomalyType: currentValue > seasonalMean ? 'seasonal_spike' : 'seasonal_drop',
|
||||||
|
deviationScore,
|
||||||
|
confidenceScore: Math.min(deviationScore / (sensitivity * 2), 1),
|
||||||
|
algorithm: 'seasonal',
|
||||||
|
baselineValue: seasonalMean,
|
||||||
|
anomalyValue: currentValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// LSTM-inspired prediction: Simple exponential smoothing with trend detection
|
||||||
|
static predictiveAnomaly(data: number[], currentValue: number, sensitivity: number = 2.5): AnomalyResult {
|
||||||
|
if (data.length < 5) {
|
||||||
|
return { isAnomaly: false, anomalyType: 'none', deviationScore: 0, confidenceScore: 0, algorithm: 'predictive', baselineValue: currentValue, anomalyValue: currentValue };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Triple exponential smoothing (Holt-Winters approximation)
|
||||||
|
const alpha = 0.3; // Level smoothing
|
||||||
|
const beta = 0.1; // Trend smoothing
|
||||||
|
|
||||||
|
let level = data[0];
|
||||||
|
let trend = data[1] - data[0];
|
||||||
|
|
||||||
|
// Calculate smoothed values
|
||||||
|
for (let i = 1; i < data.length; i++) {
|
||||||
|
const prevLevel = level;
|
||||||
|
level = alpha * data[i] + (1 - alpha) * (level + trend);
|
||||||
|
trend = beta * (level - prevLevel) + (1 - beta) * trend;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Predict next value
|
||||||
|
const prediction = level + trend;
|
||||||
|
|
||||||
|
// Calculate prediction error
|
||||||
|
const recentData = data.slice(-10);
|
||||||
|
const predictionErrors: number[] = [];
|
||||||
|
|
||||||
|
for (let i = 1; i < recentData.length; i++) {
|
||||||
|
const simplePrediction = recentData[i - 1];
|
||||||
|
predictionErrors.push(Math.abs(recentData[i] - simplePrediction));
|
||||||
|
}
|
||||||
|
|
||||||
|
const meanError = predictionErrors.reduce((sum, err) => sum + err, 0) / predictionErrors.length;
|
||||||
|
const errorStdDev = Math.sqrt(
|
||||||
|
predictionErrors.reduce((sum, err) => sum + Math.pow(err - meanError, 2), 0) / predictionErrors.length
|
||||||
|
);
|
||||||
|
|
||||||
|
const actualError = Math.abs(currentValue - prediction);
|
||||||
|
const deviationScore = errorStdDev > 0 ? actualError / errorStdDev : 0;
|
||||||
|
const isAnomaly = deviationScore > sensitivity;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAnomaly,
|
||||||
|
anomalyType: currentValue > prediction ? 'unexpected_spike' : 'unexpected_drop',
|
||||||
|
deviationScore,
|
||||||
|
confidenceScore: Math.min(deviationScore / (sensitivity * 2), 1),
|
||||||
|
algorithm: 'predictive',
|
||||||
|
baselineValue: prediction,
|
||||||
|
anomalyValue: currentValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Ensemble method: Combines multiple algorithms for better accuracy
|
||||||
|
static ensemble(data: number[], currentValue: number, sensitivity: number = 2.5): AnomalyResult {
|
||||||
|
const results: AnomalyResult[] = [
|
||||||
|
this.zScore(data, currentValue, sensitivity),
|
||||||
|
this.movingAverage(data, currentValue, sensitivity),
|
||||||
|
this.rateOfChange(data, currentValue, sensitivity),
|
||||||
|
this.isolationForest(data, currentValue, 0.6),
|
||||||
|
this.predictiveAnomaly(data, currentValue, sensitivity),
|
||||||
|
];
|
||||||
|
|
||||||
|
// Count how many algorithms detected an anomaly
|
||||||
|
const anomalyCount = results.filter(r => r.isAnomaly).length;
|
||||||
|
const anomalyRatio = anomalyCount / results.length;
|
||||||
|
|
||||||
|
// Calculate average deviation and confidence
|
||||||
|
const avgDeviation = results.reduce((sum, r) => sum + r.deviationScore, 0) / results.length;
|
||||||
|
const avgConfidence = results.reduce((sum, r) => sum + r.confidenceScore, 0) / results.length;
|
||||||
|
|
||||||
|
// Determine anomaly type based on most common classification
|
||||||
|
const typeCount = new Map<string, number>();
|
||||||
|
results.forEach(r => {
|
||||||
|
typeCount.set(r.anomalyType, (typeCount.get(r.anomalyType) || 0) + 1);
|
||||||
|
});
|
||||||
|
|
||||||
|
let mostCommonType = 'none';
|
||||||
|
let maxCount = 0;
|
||||||
|
typeCount.forEach((count, type) => {
|
||||||
|
if (count > maxCount) {
|
||||||
|
maxCount = count;
|
||||||
|
mostCommonType = type;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const mean = data.reduce((sum, val) => sum + val, 0) / data.length;
|
||||||
|
|
||||||
|
return {
|
||||||
|
isAnomaly: anomalyRatio >= 0.4, // At least 40% of algorithms agree
|
||||||
|
anomalyType: mostCommonType,
|
||||||
|
deviationScore: avgDeviation,
|
||||||
|
confidenceScore: Math.min(avgConfidence * anomalyRatio * 2, 1),
|
||||||
|
algorithm: 'ensemble',
|
||||||
|
baselineValue: mean,
|
||||||
|
anomalyValue: currentValue,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
// Z-Score algorithm: Detects outliers based on standard deviation
|
// Z-Score algorithm: Detects outliers based on standard deviation
|
||||||
static zScore(data: number[], currentValue: number, sensitivity: number = 3.0): AnomalyResult {
|
static zScore(data: number[], currentValue: number, sensitivity: number = 3.0): AnomalyResult {
|
||||||
if (data.length < 2) {
|
if (data.length < 2) {
|
||||||
@@ -189,6 +362,18 @@ Deno.serve(async (req) => {
|
|||||||
case 'rate_of_change':
|
case 'rate_of_change':
|
||||||
result = AnomalyDetector.rateOfChange(historicalValues, currentValue, config.sensitivity);
|
result = AnomalyDetector.rateOfChange(historicalValues, currentValue, config.sensitivity);
|
||||||
break;
|
break;
|
||||||
|
case 'isolation_forest':
|
||||||
|
result = AnomalyDetector.isolationForest(historicalValues, currentValue, 0.6);
|
||||||
|
break;
|
||||||
|
case 'seasonal':
|
||||||
|
result = AnomalyDetector.seasonalDecomposition(historicalValues, currentValue, config.sensitivity, 24);
|
||||||
|
break;
|
||||||
|
case 'predictive':
|
||||||
|
result = AnomalyDetector.predictiveAnomaly(historicalValues, currentValue, config.sensitivity);
|
||||||
|
break;
|
||||||
|
case 'ensemble':
|
||||||
|
result = AnomalyDetector.ensemble(historicalValues, currentValue, config.sensitivity);
|
||||||
|
break;
|
||||||
default:
|
default:
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user