mirror of
https://github.com/pacnpal/thrilltrack-explorer.git
synced 2025-12-20 02:51:12 -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;
|
||||
}
|
||||
|
||||
// Statistical anomaly detection algorithms
|
||||
// Advanced ML-based anomaly detection algorithms
|
||||
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
|
||||
static zScore(data: number[], currentValue: number, sensitivity: number = 3.0): AnomalyResult {
|
||||
if (data.length < 2) {
|
||||
@@ -189,6 +362,18 @@ Deno.serve(async (req) => {
|
||||
case 'rate_of_change':
|
||||
result = AnomalyDetector.rateOfChange(historicalValues, currentValue, config.sensitivity);
|
||||
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:
|
||||
continue;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user