import Foundation class PredictionEngine { static let shared = PredictionEngine() private let context = CoreDataStack.shared.viewContext // MARK: - Performance: Cached Calendar reference private static let calendar = Calendar.current private init() {} // MARK: - Main Prediction Interface func predict( snapshots: [Snapshot], monthsAhead: Int = 12, algorithm: PredictionAlgorithm? = nil ) -> PredictionResult { guard snapshots.count >= 3 else { return PredictionResult( predictions: [], algorithm: .linear, accuracy: 0, volatility: 0 ) } // Sort snapshots by date let sortedSnapshots = snapshots.sorted { $0.date < $1.date } // Calculate volatility for algorithm selection let volatility = calculateVolatility(snapshots: sortedSnapshots) // Select algorithm if not specified let selectedAlgorithm = algorithm ?? selectBestAlgorithm(volatility: volatility) // Generate predictions let predictions: [Prediction] let accuracy: Double switch selectedAlgorithm { case .linear: predictions = predictLinear(snapshots: sortedSnapshots, monthsAhead: monthsAhead) accuracy = calculateLinearAccuracy(snapshots: sortedSnapshots) case .exponentialSmoothing: predictions = predictExponentialSmoothing(snapshots: sortedSnapshots, monthsAhead: monthsAhead) accuracy = calculateESAccuracy(snapshots: sortedSnapshots) case .movingAverage: predictions = predictMovingAverage(snapshots: sortedSnapshots, monthsAhead: monthsAhead) accuracy = calculateMAAccuracy(snapshots: sortedSnapshots) case .holtTrend: predictions = predictHoltTrend(snapshots: sortedSnapshots, monthsAhead: monthsAhead) accuracy = calculateHoltAccuracy(snapshots: sortedSnapshots) } return PredictionResult( predictions: predictions, algorithm: selectedAlgorithm, accuracy: accuracy, volatility: volatility ) } // MARK: - Algorithm Selection private func selectBestAlgorithm(volatility: Double) -> PredictionAlgorithm { switch volatility { case 0..<8: return .holtTrend case 8..<20: return .exponentialSmoothing default: return .movingAverage } } // MARK: - Linear Regression func predictLinear(snapshots: [Snapshot], monthsAhead: Int = 12) -> [Prediction] { guard snapshots.count >= 3 else { return [] } guard let firstDate = snapshots.first?.date else { return [] } let dataPoints: [(x: Double, y: Double)] = snapshots.map { snapshot in let daysSinceStart = snapshot.date.timeIntervalSince(firstDate) / 86400 return (x: daysSinceStart, y: snapshot.decimalValue.doubleValue) } let (slope, intercept) = calculateLinearRegression(dataPoints: dataPoints) let residualStdDev = calculateResidualStdDev(dataPoints: dataPoints, slope: slope, intercept: intercept) var predictions: [Prediction] = [] let lastDate = snapshots.last!.date for month in 1...monthsAhead { guard let futureDate = Self.calendar.date( byAdding: .month, value: month, to: lastDate ) else { continue } let daysFromStart = futureDate.timeIntervalSince(firstDate) / 86400 let predictedValue = max(0, slope * daysFromStart + intercept) // Widen confidence interval for further predictions let confidenceMultiplier = 1.0 + (Double(month) * 0.02) let intervalWidth = residualStdDev * 1.96 * confidenceMultiplier predictions.append(Prediction( date: futureDate, predictedValue: Decimal(predictedValue), algorithm: .linear, confidenceInterval: Prediction.ConfidenceInterval( lower: Decimal(max(0, predictedValue - intervalWidth)), upper: Decimal(predictedValue + intervalWidth) ) )) } return predictions } private func calculateLinearRegression( dataPoints: [(x: Double, y: Double)] ) -> (slope: Double, intercept: Double) { let n = Double(dataPoints.count) let sumX = dataPoints.reduce(0) { $0 + $1.x } let sumY = dataPoints.reduce(0) { $0 + $1.y } let sumXY = dataPoints.reduce(0) { $0 + ($1.x * $1.y) } let sumX2 = dataPoints.reduce(0) { $0 + ($1.x * $1.x) } let denominator = n * sumX2 - sumX * sumX guard denominator != 0 else { return (0, sumY / n) } let slope = (n * sumXY - sumX * sumY) / denominator let intercept = (sumY - slope * sumX) / n return (slope, intercept) } private func calculateResidualStdDev( dataPoints: [(x: Double, y: Double)], slope: Double, intercept: Double ) -> Double { guard dataPoints.count > 2 else { return 0 } let residuals = dataPoints.map { point in let predicted = slope * point.x + intercept return pow(point.y - predicted, 2) } let meanSquaredError = residuals.reduce(0, +) / Double(dataPoints.count - 2) return sqrt(meanSquaredError) } private func calculateLinearAccuracy(snapshots: [Snapshot]) -> Double { guard snapshots.count >= 5 else { return 0.5 } // Use last 20% of data for validation let splitIndex = Int(Double(snapshots.count) * 0.8) let trainingData = Array(snapshots.prefix(splitIndex)) let validationData = Array(snapshots.suffix(from: splitIndex)) guard let firstDate = trainingData.first?.date else { return 0.5 } let trainPoints = trainingData.map { snapshot in (x: snapshot.date.timeIntervalSince(firstDate) / 86400, y: snapshot.decimalValue.doubleValue) } let (slope, intercept) = calculateLinearRegression(dataPoints: trainPoints) // Calculate R-squared on validation data let validationValues = validationData.map { $0.decimalValue.doubleValue } let meanValidation = validationValues.reduce(0, +) / Double(validationValues.count) var ssRes: Double = 0 var ssTot: Double = 0 for snapshot in validationData { let x = snapshot.date.timeIntervalSince(firstDate) / 86400 let actual = snapshot.decimalValue.doubleValue let predicted = slope * x + intercept ssRes += pow(actual - predicted, 2) ssTot += pow(actual - meanValidation, 2) } guard ssTot != 0 else { return 0.5 } let rSquared = max(0, 1 - (ssRes / ssTot)) return min(1.0, rSquared) } // MARK: - Exponential Smoothing func predictExponentialSmoothing( snapshots: [Snapshot], monthsAhead: Int = 12, alpha: Double = 0.3 ) -> [Prediction] { guard snapshots.count >= 3 else { return [] } let values = snapshots.map { $0.decimalValue.doubleValue } // Calculate smoothed values var smoothed = values[0] for i in 1..= 2 { let recentChange = values.suffix(3).reduce(0) { $0 + $1 } / 3.0 - values.prefix(3).reduce(0) { $0 + $1 } / 3.0 trend = recentChange / Double(values.count) } // Calculate standard deviation for confidence interval let stdDev = calculateStdDev(values: values) var predictions: [Prediction] = [] let lastDate = snapshots.last!.date for month in 1...monthsAhead { guard let futureDate = Self.calendar.date( byAdding: .month, value: month, to: lastDate ) else { continue } let predictedValue = max(0, smoothed + trend * Double(month)) let intervalWidth = stdDev * 1.96 * (1.0 + Double(month) * 0.05) predictions.append(Prediction( date: futureDate, predictedValue: Decimal(predictedValue), algorithm: .exponentialSmoothing, confidenceInterval: Prediction.ConfidenceInterval( lower: Decimal(max(0, predictedValue - intervalWidth)), upper: Decimal(predictedValue + intervalWidth) ) )) } return predictions } private func calculateESAccuracy(snapshots: [Snapshot]) -> Double { guard snapshots.count >= 5 else { return 0.5 } let values = snapshots.map { $0.decimalValue.doubleValue } let splitIndex = Int(Double(values.count) * 0.8) var smoothed = values[0] for i in 1.. [Prediction] { guard snapshots.count >= windowSize else { return [] } let values = snapshots.map { $0.decimalValue.doubleValue } // Calculate moving average of last window let recentValues = Array(values.suffix(windowSize)) let movingAverage = recentValues.reduce(0, +) / Double(windowSize) // Calculate average monthly change var changes: [Double] = [] for i in 1.. Double { guard snapshots.count >= 5 else { return 0.5 } let values = snapshots.map { $0.decimalValue.doubleValue } let windowSize = 3 let splitIndex = Int(Double(values.count) * 0.8) guard splitIndex > windowSize else { return 0.5 } let recentWindow = Array(values[(splitIndex - windowSize).. [Prediction] { guard snapshots.count >= 3 else { return [] } let values = snapshots.map { $0.decimalValue.doubleValue } var level = values[0] var trend = values[1] - values[0] var fitted: [Double] = [] for value in values { let lastLevel = level level = alpha * value + (1 - alpha) * (level + trend) trend = beta * (level - lastLevel) + (1 - beta) * trend fitted.append(level + trend) } let residuals = zip(values, fitted).map { $0 - $1 } let stdDev = calculateStdDev(values: residuals) var predictions: [Prediction] = [] let lastDate = snapshots.last!.date for month in 1...monthsAhead { guard let futureDate = Self.calendar.date( byAdding: .month, value: month, to: lastDate ) else { continue } let predictedValue = max(0, level + Double(month) * trend) let intervalWidth = stdDev * 1.96 * (1.0 + Double(month) * 0.04) predictions.append(Prediction( date: futureDate, predictedValue: Decimal(predictedValue), algorithm: .holtTrend, confidenceInterval: Prediction.ConfidenceInterval( lower: Decimal(max(0, predictedValue - intervalWidth)), upper: Decimal(predictedValue + intervalWidth) ) )) } return predictions } private func calculateHoltAccuracy(snapshots: [Snapshot]) -> Double { guard snapshots.count >= 5 else { return 0.5 } let values = snapshots.map { $0.decimalValue.doubleValue } let splitIndex = Int(Double(values.count) * 0.8) guard splitIndex >= 2 else { return 0.5 } var level = values[0] var trend = values[1] - values[0] for value in values.prefix(splitIndex) { let lastLevel = level level = 0.4 * value + 0.6 * (level + trend) trend = 0.3 * (level - lastLevel) + 0.7 * trend } let validationValues = Array(values.suffix(from: splitIndex)) let meanValidation = validationValues.reduce(0, +) / Double(validationValues.count) var ssRes: Double = 0 var ssTot: Double = 0 for (i, actual) in validationValues.enumerated() { let predicted = level + Double(i + 1) * trend ssRes += pow(actual - predicted, 2) ssTot += pow(actual - meanValidation, 2) } guard ssTot != 0 else { return 0.5 } return max(0, min(1.0, 1 - (ssRes / ssTot))) } // MARK: - Helpers private func calculateVolatility(snapshots: [Snapshot]) -> Double { let values = snapshots.map { $0.decimalValue.doubleValue } guard values.count >= 2 else { return 0 } var returns: [Double] = [] for i in 1.. Double { guard values.count >= 2 else { return 0 } let mean = values.reduce(0, +) / Double(values.count) let squaredDifferences = values.map { pow($0 - mean, 2) } let variance = squaredDifferences.reduce(0, +) / Double(values.count - 1) return sqrt(variance) } }