mirror of
https://github.com/mihakralj/QuanTAlib.git
synced 2026-08-13 16:18:05 +00:00
Merge dev into main: v0.8.7 Kahan compensated summation
This commit is contained in:
+25
-36
@@ -21,6 +21,8 @@ namespace QuanTAlib;
|
||||
/// - MASE = 1 means same as naive forecast
|
||||
/// - MASE > 1 means worse than naive forecast
|
||||
/// - Robust to zero actual values (unlike MAPE)
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Mase : AbstractBase
|
||||
@@ -32,6 +34,8 @@ public sealed class Mase : AbstractBase
|
||||
private record struct State(
|
||||
double ErrorSum,
|
||||
double ScaleSum,
|
||||
double ErrorComp,
|
||||
double ScaleComp,
|
||||
double LastValidActual,
|
||||
double LastValidPredicted,
|
||||
double PrevActual,
|
||||
@@ -39,8 +43,6 @@ public sealed class Mase : AbstractBase
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public Mase(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -50,8 +52,8 @@ public sealed class Mase : AbstractBase
|
||||
|
||||
_errorBuffer = new RingBuffer(period);
|
||||
_scaleBuffer = new RingBuffer(period);
|
||||
_state = new State(0, 0, 0, 0, double.NaN, 0);
|
||||
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
|
||||
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
|
||||
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
|
||||
Name = $"Mase({period})";
|
||||
WarmupPeriod = period + 1; // Need one extra for scale calculation
|
||||
}
|
||||
@@ -89,34 +91,36 @@ public sealed class Mase : AbstractBase
|
||||
{
|
||||
_p_state = _state;
|
||||
|
||||
// Update error buffer
|
||||
// Update error buffer — Kahan compensated
|
||||
double removedError = _errorBuffer.Count == _errorBuffer.Capacity ? _errorBuffer.Oldest : 0.0;
|
||||
_state.ErrorSum = _state.ErrorSum - removedError + absError;
|
||||
{
|
||||
double delta = absError - removedError;
|
||||
double y = delta - _state.ErrorComp;
|
||||
double t = _state.ErrorSum + y;
|
||||
_state.ErrorComp = (t - _state.ErrorSum) - y;
|
||||
_state.ErrorSum = t;
|
||||
}
|
||||
_errorBuffer.Add(absError);
|
||||
|
||||
// Update scale buffer
|
||||
// Update scale buffer — Kahan compensated
|
||||
double removedScale = _scaleBuffer.Count == _scaleBuffer.Capacity ? _scaleBuffer.Oldest : 0.0;
|
||||
_state.ScaleSum = _state.ScaleSum - removedScale + naiveDiff;
|
||||
{
|
||||
double delta = naiveDiff - removedScale;
|
||||
double y = delta - _state.ScaleComp;
|
||||
double t = _state.ScaleSum + y;
|
||||
_state.ScaleComp = (t - _state.ScaleSum) - y;
|
||||
_state.ScaleSum = t;
|
||||
}
|
||||
_scaleBuffer.Add(naiveDiff);
|
||||
|
||||
_state.PrevActual = actualVal;
|
||||
|
||||
_state.TickCount++;
|
||||
if (_state.TickCount >= ResyncInterval)
|
||||
{
|
||||
// Keep TickCount > period to maintain post-warmup state
|
||||
_state.TickCount = _errorBuffer.Capacity + 1;
|
||||
_state.ErrorSum = _errorBuffer.RecalculateSum();
|
||||
_state.ScaleSum = _scaleBuffer.RecalculateSum();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_errorBuffer.UpdateNewest(absError);
|
||||
_scaleBuffer.UpdateNewest(naiveDiff);
|
||||
|
||||
@@ -174,8 +178,8 @@ public sealed class Mase : AbstractBase
|
||||
{
|
||||
_errorBuffer.Clear();
|
||||
_scaleBuffer.Clear();
|
||||
_state = new State(0, 0, 0, 0, double.NaN, 0);
|
||||
_p_state = new State(0, 0, 0, 0, double.NaN, 0);
|
||||
_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
|
||||
_p_state = new State(0, 0, 0, 0, 0, 0, double.NaN, 0);
|
||||
Last = default;
|
||||
}
|
||||
|
||||
@@ -293,7 +297,6 @@ public sealed class Mase : AbstractBase
|
||||
prevActual = act;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -336,20 +339,6 @@ public sealed class Mase : AbstractBase
|
||||
output[i] = scale > 1e-10 ? mae / scale : mae;
|
||||
|
||||
prevActual = act;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcError = 0, recalcScale = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcError += errorBuffer[k];
|
||||
recalcScale += scaleBuffer[k];
|
||||
}
|
||||
errorSum = recalcError;
|
||||
scaleSum = recalcScale;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,4 +348,4 @@ public sealed class Mase : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -168,7 +168,6 @@ public sealed class QuantileLoss : BiInputIndicatorBase
|
||||
output[i] = lossSum / (i + 1);
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -205,19 +204,6 @@ public sealed class QuantileLoss : BiInputIndicatorBase
|
||||
}
|
||||
|
||||
output[i] = lossSum / period;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSum = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcSum += lossBuffer[k];
|
||||
}
|
||||
|
||||
lossSum = recalcSum;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+30
-36
@@ -19,6 +19,8 @@ namespace QuanTAlib;
|
||||
/// - RAE = 1 means same as mean predictor
|
||||
/// - RAE > 1 means worse than mean predictor
|
||||
/// - Scale-independent ratio
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rae : AbstractBase
|
||||
@@ -32,14 +34,14 @@ public sealed class Rae : AbstractBase
|
||||
double ActualSum,
|
||||
double AbsErrorSum,
|
||||
double AbsBaselineSum,
|
||||
double ActualComp,
|
||||
double AbsErrorComp,
|
||||
double AbsBaselineComp,
|
||||
double LastValidActual,
|
||||
double LastValidPredicted,
|
||||
int TickCount);
|
||||
double LastValidPredicted);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public Rae(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -93,9 +95,15 @@ public sealed class Rae : AbstractBase
|
||||
|
||||
if (isNew)
|
||||
{
|
||||
// Update actual buffer for mean calculation
|
||||
// Update actual buffer for mean calculation — Kahan compensated
|
||||
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
|
||||
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
|
||||
{
|
||||
double delta = actualVal - removedActual;
|
||||
double y = delta - _state.ActualComp;
|
||||
double t = _state.ActualSum + y;
|
||||
_state.ActualComp = (t - _state.ActualSum) - y;
|
||||
_state.ActualSum = t;
|
||||
}
|
||||
_actualBuffer.Add(actualVal);
|
||||
|
||||
// Calculate mean and baseline error
|
||||
@@ -103,24 +111,27 @@ public sealed class Rae : AbstractBase
|
||||
double absError = Math.Abs(actualVal - predictedVal);
|
||||
double absBaseline = Math.Abs(actualVal - mean);
|
||||
|
||||
// Update error buffer
|
||||
// Update error buffer — Kahan compensated
|
||||
double removedError = _absErrorBuffer.Count == _absErrorBuffer.Capacity ? _absErrorBuffer.Oldest : 0.0;
|
||||
_state.AbsErrorSum = _state.AbsErrorSum - removedError + absError;
|
||||
{
|
||||
double delta = absError - removedError;
|
||||
double y = delta - _state.AbsErrorComp;
|
||||
double t = _state.AbsErrorSum + y;
|
||||
_state.AbsErrorComp = (t - _state.AbsErrorSum) - y;
|
||||
_state.AbsErrorSum = t;
|
||||
}
|
||||
_absErrorBuffer.Add(absError);
|
||||
|
||||
// Update baseline buffer
|
||||
// Update baseline buffer — Kahan compensated
|
||||
double removedBaseline = _absBaselineBuffer.Count == _absBaselineBuffer.Capacity ? _absBaselineBuffer.Oldest : 0.0;
|
||||
_state.AbsBaselineSum = _state.AbsBaselineSum - removedBaseline + absBaseline;
|
||||
_absBaselineBuffer.Add(absBaseline);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
_state.AbsBaselineSum = _absBaselineBuffer.RecalculateSum();
|
||||
double delta = absBaseline - removedBaseline;
|
||||
double y = delta - _state.AbsBaselineComp;
|
||||
double t = _state.AbsBaselineSum + y;
|
||||
_state.AbsBaselineComp = (t - _state.AbsBaselineSum) - y;
|
||||
_state.AbsBaselineSum = t;
|
||||
}
|
||||
_absBaselineBuffer.Add(absBaseline);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -294,7 +305,6 @@ public sealed class Rae : AbstractBase
|
||||
output[i] = absBaselineSum > 1e-10 ? absErrorSum / absBaselineSum : 1.0;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -337,22 +347,6 @@ public sealed class Rae : AbstractBase
|
||||
}
|
||||
|
||||
output[i] = absBaselineSum > 1e-10 ? absErrorSum / absBaselineSum : 1.0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcActual = 0, recalcError = 0, recalcBaseline = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcActual += actualBuffer[k];
|
||||
recalcError += absErrorBuffer[k];
|
||||
recalcBaseline += absBaselineBuffer[k];
|
||||
}
|
||||
actualSum = recalcActual;
|
||||
absErrorSum = recalcError;
|
||||
absBaselineSum = recalcBaseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -362,4 +356,4 @@ public sealed class Rae : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+30
-36
@@ -19,6 +19,8 @@ namespace QuanTAlib;
|
||||
/// - RSE = 1 means same as mean predictor
|
||||
/// - RSE > 1 means worse than mean predictor
|
||||
/// - Related to R² by: R² = 1 - RSE
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rse : AbstractBase
|
||||
@@ -32,14 +34,14 @@ public sealed class Rse : AbstractBase
|
||||
double ActualSum,
|
||||
double SqErrorSum,
|
||||
double SqBaselineSum,
|
||||
double ActualComp,
|
||||
double SqErrorComp,
|
||||
double SqBaselineComp,
|
||||
double LastValidActual,
|
||||
double LastValidPredicted,
|
||||
int TickCount);
|
||||
double LastValidPredicted);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public Rse(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -90,9 +92,15 @@ public sealed class Rse : AbstractBase
|
||||
{
|
||||
_p_state = _state;
|
||||
|
||||
// Update actual buffer for mean calculation
|
||||
// Update actual buffer for mean calculation — Kahan compensated
|
||||
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
|
||||
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
|
||||
{
|
||||
double delta = actualVal - removedActual;
|
||||
double y = delta - _state.ActualComp;
|
||||
double t = _state.ActualSum + y;
|
||||
_state.ActualComp = (t - _state.ActualSum) - y;
|
||||
_state.ActualSum = t;
|
||||
}
|
||||
_actualBuffer.Add(actualVal);
|
||||
|
||||
// Calculate mean and baseline error
|
||||
@@ -102,24 +110,27 @@ public sealed class Rse : AbstractBase
|
||||
double sqError = error * error;
|
||||
double sqBaseline = baselineError * baselineError;
|
||||
|
||||
// Update squared error buffer
|
||||
// Update squared error buffer — Kahan compensated
|
||||
double removedError = _sqErrorBuffer.Count == _sqErrorBuffer.Capacity ? _sqErrorBuffer.Oldest : 0.0;
|
||||
_state.SqErrorSum = _state.SqErrorSum - removedError + sqError;
|
||||
{
|
||||
double delta = sqError - removedError;
|
||||
double y = delta - _state.SqErrorComp;
|
||||
double t = _state.SqErrorSum + y;
|
||||
_state.SqErrorComp = (t - _state.SqErrorSum) - y;
|
||||
_state.SqErrorSum = t;
|
||||
}
|
||||
_sqErrorBuffer.Add(sqError);
|
||||
|
||||
// Update squared baseline buffer
|
||||
// Update squared baseline buffer — Kahan compensated
|
||||
double removedBaseline = _sqBaselineBuffer.Count == _sqBaselineBuffer.Capacity ? _sqBaselineBuffer.Oldest : 0.0;
|
||||
_state.SqBaselineSum = _state.SqBaselineSum - removedBaseline + sqBaseline;
|
||||
_sqBaselineBuffer.Add(sqBaseline);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
|
||||
_state.SqBaselineSum = _sqBaselineBuffer.RecalculateSum();
|
||||
double delta = sqBaseline - removedBaseline;
|
||||
double y = delta - _state.SqBaselineComp;
|
||||
double t = _state.SqBaselineSum + y;
|
||||
_state.SqBaselineComp = (t - _state.SqBaselineSum) - y;
|
||||
_state.SqBaselineSum = t;
|
||||
}
|
||||
_sqBaselineBuffer.Add(sqBaseline);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -303,7 +314,6 @@ public sealed class Rse : AbstractBase
|
||||
output[i] = sqBaselineSum > 1e-10 ? sqErrorSum / sqBaselineSum : 1.0;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -348,22 +358,6 @@ public sealed class Rse : AbstractBase
|
||||
}
|
||||
|
||||
output[i] = sqBaselineSum > 1e-10 ? sqErrorSum / sqBaselineSum : 1.0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcActual = 0, recalcError = 0, recalcBaseline = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcActual += actualBuffer[k];
|
||||
recalcError += sqErrorBuffer[k];
|
||||
recalcBaseline += sqBaselineBuffer[k];
|
||||
}
|
||||
actualSum = recalcActual;
|
||||
sqErrorSum = recalcError;
|
||||
sqBaselineSum = recalcBaseline;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,4 +367,4 @@ public sealed class Rse : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,6 +20,8 @@ namespace QuanTAlib;
|
||||
/// - R² = 0 means predictions equal mean predictor
|
||||
/// - R² < 0 means predictions worse than mean predictor
|
||||
/// - Range: (-∞, 1]
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Rsquared : AbstractBase
|
||||
@@ -33,14 +35,14 @@ public sealed class Rsquared : AbstractBase
|
||||
double ActualSum,
|
||||
double SqResidualSum,
|
||||
double SqTotalSum,
|
||||
double ActualComp,
|
||||
double SqResidualComp,
|
||||
double SqTotalComp,
|
||||
double LastValidActual,
|
||||
double LastValidPredicted,
|
||||
int TickCount);
|
||||
double LastValidPredicted);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public Rsquared(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -85,9 +87,15 @@ public sealed class Rsquared : AbstractBase
|
||||
{
|
||||
_p_state = _state;
|
||||
|
||||
// Update actual buffer for mean calculation
|
||||
// Update actual buffer for mean calculation — Kahan compensated
|
||||
double removedActual = _actualBuffer.Count == _actualBuffer.Capacity ? _actualBuffer.Oldest : 0.0;
|
||||
_state.ActualSum = _state.ActualSum - removedActual + actualVal;
|
||||
{
|
||||
double delta = actualVal - removedActual;
|
||||
double y = delta - _state.ActualComp;
|
||||
double t = _state.ActualSum + y;
|
||||
_state.ActualComp = (t - _state.ActualSum) - y;
|
||||
_state.ActualSum = t;
|
||||
}
|
||||
_actualBuffer.Add(actualVal);
|
||||
|
||||
// Calculate mean and errors
|
||||
@@ -97,24 +105,27 @@ public sealed class Rsquared : AbstractBase
|
||||
double sqResidual = residual * residual;
|
||||
double sqTotal = totalDev * totalDev;
|
||||
|
||||
// Update squared residual buffer (RSS)
|
||||
// Update squared residual buffer (RSS) — Kahan compensated
|
||||
double removedResidual = _sqResidualBuffer.Count == _sqResidualBuffer.Capacity ? _sqResidualBuffer.Oldest : 0.0;
|
||||
_state.SqResidualSum = _state.SqResidualSum - removedResidual + sqResidual;
|
||||
{
|
||||
double delta = sqResidual - removedResidual;
|
||||
double y = delta - _state.SqResidualComp;
|
||||
double t = _state.SqResidualSum + y;
|
||||
_state.SqResidualComp = (t - _state.SqResidualSum) - y;
|
||||
_state.SqResidualSum = t;
|
||||
}
|
||||
_sqResidualBuffer.Add(sqResidual);
|
||||
|
||||
// Update squared total buffer (TSS)
|
||||
// Update squared total buffer (TSS) — Kahan compensated
|
||||
double removedTotal = _sqTotalBuffer.Count == _sqTotalBuffer.Capacity ? _sqTotalBuffer.Oldest : 0.0;
|
||||
_state.SqTotalSum = _state.SqTotalSum - removedTotal + sqTotal;
|
||||
_sqTotalBuffer.Add(sqTotal);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_actualBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.ActualSum = _actualBuffer.RecalculateSum();
|
||||
_state.SqResidualSum = _sqResidualBuffer.RecalculateSum();
|
||||
_state.SqTotalSum = _sqTotalBuffer.RecalculateSum();
|
||||
double delta = sqTotal - removedTotal;
|
||||
double y = delta - _state.SqTotalComp;
|
||||
double t = _state.SqTotalSum + y;
|
||||
_state.SqTotalComp = (t - _state.SqTotalSum) - y;
|
||||
_state.SqTotalSum = t;
|
||||
}
|
||||
_sqTotalBuffer.Add(sqTotal);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -297,7 +308,6 @@ public sealed class Rsquared : AbstractBase
|
||||
output[i] = sqTotalSum > 1e-10 ? 1.0 - (sqResidualSum / sqTotalSum) : 1.0;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -342,22 +352,6 @@ public sealed class Rsquared : AbstractBase
|
||||
}
|
||||
|
||||
output[i] = sqTotalSum > 1e-10 ? 1.0 - (sqResidualSum / sqTotalSum) : 1.0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcActual = 0, recalcResidual = 0, recalcTotal = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcActual += actualBuffer[k];
|
||||
recalcResidual += sqResidualBuffer[k];
|
||||
recalcTotal += sqTotalBuffer[k];
|
||||
}
|
||||
actualSum = recalcActual;
|
||||
sqResidualSum = recalcResidual;
|
||||
sqTotalSum = recalcTotal;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -367,4 +361,4 @@ public sealed class Rsquared : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+24
-35
@@ -21,6 +21,8 @@ namespace QuanTAlib;
|
||||
/// - U = 1: Forecast as good as naive (no-change) forecast
|
||||
/// - U > 1: Forecast worse than naive forecast
|
||||
/// - Useful for comparing forecasting methods
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class TheilU : AbstractBase
|
||||
@@ -30,12 +32,10 @@ public sealed class TheilU : AbstractBase
|
||||
private readonly RingBuffer _sqPredBuffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double SqErrorSum, double SqActualSum, double SqPredSum, double LastValidActual, double LastValidPredicted, int TickCount);
|
||||
private record struct State(double SqErrorSum, double SqActualSum, double SqPredSum, double SqErrorComp, double SqActualComp, double SqPredComp, double LastValidActual, double LastValidPredicted);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
|
||||
public TheilU(int period)
|
||||
{
|
||||
if (period <= 0)
|
||||
@@ -109,34 +109,40 @@ public sealed class TheilU : AbstractBase
|
||||
_p_state = _state;
|
||||
|
||||
double removedSqError = _sqErrorBuffer.Count == _sqErrorBuffer.Capacity ? _sqErrorBuffer.Oldest : 0.0;
|
||||
// Use FMA: sum = sum - removed + new = FMA(1.0, new, FMA(-1.0, removed, sum))
|
||||
_state.SqErrorSum = Math.FusedMultiplyAdd(1.0, sqError, Math.FusedMultiplyAdd(-1.0, removedSqError, _state.SqErrorSum));
|
||||
{
|
||||
double delta = sqError - removedSqError;
|
||||
double y = delta - _state.SqErrorComp;
|
||||
double t = _state.SqErrorSum + y;
|
||||
_state.SqErrorComp = (t - _state.SqErrorSum) - y;
|
||||
_state.SqErrorSum = t;
|
||||
}
|
||||
_sqErrorBuffer.Add(sqError);
|
||||
|
||||
double removedSqActual = _sqActualBuffer.Count == _sqActualBuffer.Capacity ? _sqActualBuffer.Oldest : 0.0;
|
||||
_state.SqActualSum = Math.FusedMultiplyAdd(1.0, sqActual, Math.FusedMultiplyAdd(-1.0, removedSqActual, _state.SqActualSum));
|
||||
{
|
||||
double delta = sqActual - removedSqActual;
|
||||
double y = delta - _state.SqActualComp;
|
||||
double t = _state.SqActualSum + y;
|
||||
_state.SqActualComp = (t - _state.SqActualSum) - y;
|
||||
_state.SqActualSum = t;
|
||||
}
|
||||
_sqActualBuffer.Add(sqActual);
|
||||
|
||||
double removedSqPred = _sqPredBuffer.Count == _sqPredBuffer.Capacity ? _sqPredBuffer.Oldest : 0.0;
|
||||
_state.SqPredSum = Math.FusedMultiplyAdd(1.0, sqPred, Math.FusedMultiplyAdd(-1.0, removedSqPred, _state.SqPredSum));
|
||||
_sqPredBuffer.Add(sqPred);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_sqErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.SqErrorSum = _sqErrorBuffer.RecalculateSum();
|
||||
_state.SqActualSum = _sqActualBuffer.RecalculateSum();
|
||||
_state.SqPredSum = _sqPredBuffer.RecalculateSum();
|
||||
double delta = sqPred - removedSqPred;
|
||||
double y = delta - _state.SqPredComp;
|
||||
double t = _state.SqPredSum + y;
|
||||
_state.SqPredComp = (t - _state.SqPredSum) - y;
|
||||
_state.SqPredSum = t;
|
||||
}
|
||||
_sqPredBuffer.Add(sqPred);
|
||||
}
|
||||
else
|
||||
{
|
||||
_state = _p_state;
|
||||
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_sqErrorBuffer.UpdateNewest(sqError);
|
||||
_sqActualBuffer.UpdateNewest(sqActual);
|
||||
_sqPredBuffer.UpdateNewest(sqPred);
|
||||
@@ -288,7 +294,6 @@ public sealed class TheilU : AbstractBase
|
||||
output[i] = denom > 1e-10 ? Math.Sqrt(sqErrorSum) / denom : 0.0;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -334,22 +339,6 @@ public sealed class TheilU : AbstractBase
|
||||
|
||||
double denom = Math.Sqrt(sqActualSum + sqPredSum);
|
||||
output[i] = denom > 1e-10 ? Math.Sqrt(sqErrorSum) / denom : 0.0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcSqError = 0, recalcSqActual = 0, recalcSqPred = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcSqError += sqErrorBuffer[k];
|
||||
recalcSqActual += sqActualBuffer[k];
|
||||
recalcSqPred += sqPredBuffer[k];
|
||||
}
|
||||
sqErrorSum = recalcSqError;
|
||||
sqActualSum = recalcSqActual;
|
||||
sqPredSum = recalcSqPred;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,4 +348,4 @@ public sealed class TheilU : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,7 +29,6 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
|
||||
{
|
||||
private readonly double _cSquaredOver6;
|
||||
private const double DefaultC = 4.685; // 95% efficiency for normal distribution
|
||||
private const int BatchResyncInterval = 1000; // Local constant for static Batch method
|
||||
|
||||
public TukeyBiweight(int period, double c = DefaultC)
|
||||
: base(period, $"TukeyBiweight({period},{c:F3})")
|
||||
@@ -122,7 +121,7 @@ public sealed class TukeyBiweight : BiInputIndicatorBase
|
||||
ErrorHelpers.ComputeTukeyBiweightErrors(actual, predicted, errors, c);
|
||||
|
||||
// Step 2: Apply rolling mean
|
||||
ErrorHelpers.ApplyRollingMean(errors, output, period, BatchResyncInterval);
|
||||
ErrorHelpers.ApplyRollingMean(errors, output, period);
|
||||
}
|
||||
finally
|
||||
{
|
||||
|
||||
+17
-29
@@ -20,6 +20,8 @@ namespace QuanTAlib;
|
||||
/// - Weights larger actual values more heavily
|
||||
/// - More stable than MAPE for intermittent data
|
||||
/// - Industry standard for demand forecasting
|
||||
///
|
||||
/// Uses Kahan compensated summation to prevent floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Wmape : AbstractBase
|
||||
@@ -28,11 +30,10 @@ public sealed class Wmape : AbstractBase
|
||||
private readonly RingBuffer _absActualBuffer;
|
||||
|
||||
[StructLayout(LayoutKind.Auto)]
|
||||
private record struct State(double AbsErrorSum, double AbsActualSum, double LastValidActual, double LastValidPredicted, int TickCount);
|
||||
private record struct State(double AbsErrorSum, double AbsActualSum, double AbsErrorComp, double AbsActualComp, double LastValidActual, double LastValidPredicted);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
private const int StackAllocThreshold = 256;
|
||||
|
||||
public Wmape(int period)
|
||||
@@ -90,26 +91,28 @@ public sealed class Wmape : AbstractBase
|
||||
if (isNew)
|
||||
{
|
||||
double removedError = _absErrorBuffer.Count == _absErrorBuffer.Capacity ? _absErrorBuffer.Oldest : 0.0;
|
||||
_state.AbsErrorSum = _state.AbsErrorSum - removedError + absError;
|
||||
{
|
||||
double delta = absError - removedError;
|
||||
double y = delta - _state.AbsErrorComp;
|
||||
double t = _state.AbsErrorSum + y;
|
||||
_state.AbsErrorComp = (t - _state.AbsErrorSum) - y;
|
||||
_state.AbsErrorSum = t;
|
||||
}
|
||||
_absErrorBuffer.Add(absError);
|
||||
|
||||
double removedActual = _absActualBuffer.Count == _absActualBuffer.Capacity ? _absActualBuffer.Oldest : 0.0;
|
||||
_state.AbsActualSum = _state.AbsActualSum - removedActual + absActual;
|
||||
_absActualBuffer.Add(absActual);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_absErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.AbsErrorSum = _absErrorBuffer.RecalculateSum();
|
||||
_state.AbsActualSum = _absActualBuffer.RecalculateSum();
|
||||
double delta = absActual - removedActual;
|
||||
double y = delta - _state.AbsActualComp;
|
||||
double t = _state.AbsActualSum + y;
|
||||
_state.AbsActualComp = (t - _state.AbsActualSum) - y;
|
||||
_state.AbsActualSum = t;
|
||||
}
|
||||
_absActualBuffer.Add(absActual);
|
||||
}
|
||||
else
|
||||
{
|
||||
// Bar correction: update buffer and recalculate sums
|
||||
// Note: _p_state was saved BEFORE the Add, but buffer still has the added value
|
||||
// So we update newest and recalculate to ensure consistency
|
||||
_absErrorBuffer.UpdateNewest(absError);
|
||||
_absActualBuffer.UpdateNewest(absActual);
|
||||
|
||||
@@ -277,7 +280,6 @@ public sealed class Wmape : AbstractBase
|
||||
output[i] = absActualSum > 1e-10 ? (absErrorSum / absActualSum) * 100.0 : 0.0;
|
||||
}
|
||||
|
||||
int tickCount = 0;
|
||||
for (; i < len; i++)
|
||||
{
|
||||
double act = actual[i];
|
||||
@@ -316,20 +318,6 @@ public sealed class Wmape : AbstractBase
|
||||
}
|
||||
|
||||
output[i] = absActualSum > 1e-10 ? (absErrorSum / absActualSum) * 100.0 : 0.0;
|
||||
|
||||
tickCount++;
|
||||
if (tickCount >= ResyncInterval)
|
||||
{
|
||||
tickCount = 0;
|
||||
double recalcError = 0, recalcActual = 0;
|
||||
for (int k = 0; k < period; k++)
|
||||
{
|
||||
recalcError += absErrorBuffer[k];
|
||||
recalcActual += absActualBuffer[k];
|
||||
}
|
||||
absErrorSum = recalcError;
|
||||
absActualSum = recalcActual;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
@@ -352,4 +340,4 @@ public sealed class Wmape : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+20
-14
@@ -14,7 +14,7 @@ namespace QuanTAlib;
|
||||
/// Formula:
|
||||
/// WRMSE = √(Σ(w_i * (actual_i - predicted_i)²) / Σ(w_i))
|
||||
///
|
||||
/// Uses dual RingBuffers for O(1) streaming updates with running sums.
|
||||
/// Uses dual RingBuffers for O(1) streaming updates with Kahan compensated running sums.
|
||||
///
|
||||
/// Key properties:
|
||||
/// - Always non-negative (WRMSE ≥ 0)
|
||||
@@ -22,6 +22,8 @@ namespace QuanTAlib;
|
||||
/// - Weights allow emphasizing important observations
|
||||
/// - Reduces to RMSE when all weights are equal
|
||||
/// - WRMSE = 0 indicates perfect prediction
|
||||
///
|
||||
/// Kahan compensated summation prevents floating-point drift without periodic resync.
|
||||
/// </remarks>
|
||||
[SkipLocalsInit]
|
||||
public sealed class Wrmse : AbstractBase
|
||||
@@ -33,14 +35,14 @@ public sealed class Wrmse : AbstractBase
|
||||
private record struct State(
|
||||
double WeightedErrorSum,
|
||||
double WeightSum,
|
||||
double WeightedErrorComp,
|
||||
double WeightComp,
|
||||
double LastValidActual,
|
||||
double LastValidPredicted,
|
||||
double LastValidWeight,
|
||||
int TickCount);
|
||||
double LastValidWeight);
|
||||
private State _state;
|
||||
private State _p_state;
|
||||
|
||||
private const int ResyncInterval = 1000;
|
||||
private const double DefaultWeight = 1.0;
|
||||
|
||||
/// <summary>
|
||||
@@ -124,21 +126,25 @@ public sealed class Wrmse : AbstractBase
|
||||
|
||||
double removedWeightedError = _weightedErrorBuffer.Count == _weightedErrorBuffer.Capacity
|
||||
? _weightedErrorBuffer.Oldest : 0.0;
|
||||
_state.WeightedErrorSum = _state.WeightedErrorSum - removedWeightedError + weightedError;
|
||||
{
|
||||
double delta = weightedError - removedWeightedError;
|
||||
double y = delta - _state.WeightedErrorComp;
|
||||
double t = _state.WeightedErrorSum + y;
|
||||
_state.WeightedErrorComp = (t - _state.WeightedErrorSum) - y;
|
||||
_state.WeightedErrorSum = t;
|
||||
}
|
||||
_weightedErrorBuffer.Add(weightedError);
|
||||
|
||||
double removedWeight = _weightBuffer.Count == _weightBuffer.Capacity
|
||||
? _weightBuffer.Oldest : 0.0;
|
||||
_state.WeightSum = _state.WeightSum - removedWeight + weight;
|
||||
_weightBuffer.Add(weight);
|
||||
|
||||
_state.TickCount++;
|
||||
if (_weightedErrorBuffer.IsFull && _state.TickCount >= ResyncInterval)
|
||||
{
|
||||
_state.TickCount = 0;
|
||||
_state.WeightedErrorSum = _weightedErrorBuffer.RecalculateSum();
|
||||
_state.WeightSum = _weightBuffer.RecalculateSum();
|
||||
double delta = weight - removedWeight;
|
||||
double y = delta - _state.WeightComp;
|
||||
double t = _state.WeightSum + y;
|
||||
_state.WeightComp = (t - _state.WeightSum) - y;
|
||||
_state.WeightSum = t;
|
||||
}
|
||||
_weightBuffer.Add(weight);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -332,4 +338,4 @@ public sealed class Wrmse : AbstractBase
|
||||
TSeries results = Batch(actual, predicted, period);
|
||||
return (results, indicator);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user