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:
gpt-engineer-app[bot]
2025-11-11 02:28:19 +00:00
parent 915a9fe2df
commit 12a6bfdfab

View File

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