using System.Runtime.CompilerServices; namespace QuanTAlib; /// /// MPE: Mean Percentage Error /// /// /// MPE measures the average percentage error between actual and predicted values, /// preserving the sign to detect directional bias. Unlike MAPE, it can reveal /// systematic over- or under-prediction. /// /// Formula: /// MPE = (100/n) * Σ((actual - predicted) / actual) /// /// Key properties: /// - Scale-independent (expressed as percentage) /// - Preserves sign: positive = under-prediction, negative = over-prediction /// - Cannot be calculated when actual = 0 /// - Useful for detecting systematic bias in predictions /// [SkipLocalsInit] public sealed class Mpe : BiInputIndicatorBase { private const double Epsilon = 1e-10; /// /// Creates MPE with specified period. /// /// Number of values to average (must be > 0) public Mpe(int period) : base(period, $"Mpe({period})") { } /// [MethodImpl(MethodImplOptions.AggressiveInlining)] protected override double ComputeError(double actual, double predicted) { // MPE: 100 * (actual - predicted) / actual (preserves sign) // When actual is near zero, use signed epsilon to preserve the original sign double divisor; if (Math.Abs(actual) < Epsilon) { int sign = Math.Sign(actual); divisor = sign != 0 ? sign * Epsilon : Epsilon; } else { divisor = actual; } return 100.0 * (actual - predicted) / divisor; } /// /// Calculates MPE for entire series. /// public static TSeries Batch(TSeries actual, TSeries predicted, int period) => CalculateImpl(actual, predicted, period, Batch); /// /// Batch calculation using signed percentage error computation with rolling mean. /// [MethodImpl(MethodImplOptions.AggressiveInlining)] public static void Batch(ReadOnlySpan actual, ReadOnlySpan predicted, Span output, int period) { ValidateBatchInputs(actual, predicted, output, period); int len = actual.Length; if (len == 0) { return; } const int StackAllocThreshold = 256; Span errors = len <= StackAllocThreshold ? stackalloc double[len] : new double[len]; ComputeSignedPercentageErrors(actual, predicted, errors); ErrorHelpers.ApplyRollingMean(errors, output, period); } public static (TSeries Results, Mpe Indicator) Calculate(TSeries actual, TSeries predicted, int period) { var indicator = new Mpe(period); TSeries results = Batch(actual, predicted, period); return (results, indicator); } [MethodImpl(MethodImplOptions.AggressiveInlining)] private static void ComputeSignedPercentageErrors(ReadOnlySpan actual, ReadOnlySpan predicted, Span output) { int len = actual.Length; double lastValidActual = 1.0, lastValidPredicted = 0; for (int i = 0; i < len; i++) { if (double.IsFinite(actual[i]) && Math.Abs(actual[i]) >= Epsilon) { lastValidActual = actual[i]; break; } } for (int i = 0; i < len; i++) { if (double.IsFinite(predicted[i])) { lastValidPredicted = predicted[i]; break; } } for (int i = 0; i < len; i++) { double act = actual[i]; double pred = predicted[i]; if (double.IsFinite(act) && Math.Abs(act) >= Epsilon) { lastValidActual = act; } else { act = lastValidActual; } if (double.IsFinite(pred)) { lastValidPredicted = pred; } else { pred = lastValidPredicted; } // Use signed epsilon to preserve the original sign when actual is near zero double divisor; if (Math.Abs(act) < Epsilon) { int sign = Math.Sign(act); divisor = sign != 0 ? sign * Epsilon : Epsilon; } else { divisor = act; } output[i] = 100.0 * (act - pred) / divisor; } } }