如何在C#中更快地计算简单移动平均?

24

什么是计算简单移动平均的最快库/算法? 我编写了自己的代码,但在330,000项十进制数据集上运行太慢了。

  • 周期 / 时间(毫秒)
  • 20 / 300;
  • 60 / 1500;
  • 120 / 3500。

这是我的方法的代码:

public decimal MA_Simple(int period, int ii) {
    if (period != 0 && ii > period) {
        //stp.Start();
        decimal summ = 0;
        for (int i = ii; i > ii - period; i--) {
            summ = summ + Data.Close[i];
        }
        summ = summ / period;
        //stp.Stop();
        //if (ii == 1500) System.Windows.Forms.MessageBox.Show((stp.ElapsedTicks * 1000.0) / Stopwatch.Frequency + " ms");
        return summ;
    } else return -1;
}

Data.Close[] 是一个固定大小(1,000,000)的十进制数组。


3
你使用移动平均值的目的是什么?如果你正在对滑动窗口进行平均,那么你可以增量更新平均值,这样可以使速度更快。如果你正在计算随机窗口,你可以将数组预处理为累积和数组,以加速移动平均值的计算。优化取决于你的使用情况。 - nneonneo
对一个大数组进行累加会导致精度丢失,除非使用具有任意精度的数字库。 - Storstamp
decimal 具有 96 位精度,对于这种累积求和计算而言比 doublefloat 要表现出更好的性能。 - nneonneo
16个回答

26
    public class MovingAverage  
    {
        private Queue<Decimal> samples = new Queue<Decimal>();
        private int windowSize = 16;
        private Decimal sampleAccumulator;
        public Decimal Average { get; private set; }

        /// <summary>
        /// Computes a new windowed average each time a new sample arrives
        /// </summary>
        /// <param name="newSample"></param>
        public void ComputeAverage(Decimal newSample)
        {
            sampleAccumulator += newSample;
            samples.Enqueue(newSample);

            if (samples.Count > windowSize)
            {
                sampleAccumulator -= samples.Dequeue();
            }

            Average = sampleAccumulator / samples.Count;
        }
    }

将此转换为 PowerShell 脚本,以便在调用 Web 服务时估计剩余时间。https://gist.github.com/michaellwest/d7712f97bd3fba6109ea2369e50347c6 - Coding101

19

您的主要问题是每次迭代丢失了太多信息。如果您想运行得更快,需要保持与帧长度相同大小的缓冲区。

此代码将为整个数据集运行移动平均:

(这不是真正的C#,但您应该能理解思路)

decimal buffer[] = new decimal[period];
decimal output[] = new decimal[data.Length];
current_index = 0;
for (int i=0; i<data.Length; i++)
    {
        buffer[current_index] = data[i]/period;
        decimal ma = 0.0;
        for (int j=0;j<period;j++)
            {
                ma += buffer[j];
            }
        output[i] = ma;
        current_index = (current_index + 1) % period;
    }
return output;
请注意,保留一个运行的累加和而不是保留整个缓冲区并计算每次迭代的值可能很诱人,但在数据长度非常长时,这样做将无法正常工作,因为累积总和将变得非常大,添加小的附加值将导致舍入误差。

2
decimal 具有 96 位尾数,但关键是浮点基数为 10 而不是 2。因此,如果您所做的只是操作具有有限小数位数的值(对于大多数金融计算来说,10 个小数位就足够了),则 decimal 没有误差。 - nneonneo
2
嗯,我承认我不知道C#的decimal是浮点数。好知道… - RBarryYoung
1
小改进:十进制数组应该定义为 decimal[] buffer 而不是 decimal buffer[] - satbot
@nneonneo 四舍五入误差,溢出错误等等…,即使十进制数据类型是固定点并带有10个小数位,但仍会因连续除法操作次数而产生累积误差。原因是实际的除法运算往往会产生超过10位小数的结果,这种误差会逐渐累积。实际的除法运算即使使用10位小数也无法得到精确结果。 - tcwicks
请注意,这将使具有“索引<周期”的值向0移动。 - David Sherret
显示剩余6条评论

10
这些天,Math DotNet库有一个名为RunningStatistics的类可以为您执行此操作。如果您只想在最后的“X”项中执行此操作,请改用MovingStatistics

这两个都将实时计算移动平均值、方差和标准偏差,仅一次通过而不存储额外的数据副本。


5
如果数据是静态的,你可以预处理数组,使移动平均查询变得非常快:
decimal[] GetCSum(decimal[] data) {
    decimal csum[] = new decimal[data.Length];
    decimal cursum = 0;
    for(int i=0; i<data.Length; i++) {
        cursum += data[i];
        csum[i] = cursum;
    }
    return csum;
}

现在移动平均值的计算变得简单快捷:
decimal CSumMovingAverage(decimal[] csum, int period, int ii) {
    if(period == 0 || ii <= period)
        return -1;
    return csum[ii] - csum[ii - period];
}

3

您无需维护一个运行队列。只需选择最新的条目并删除旧的条目即可。请注意,这仅使用一个循环和除总和外没有额外存储。

  // n is the window for your Simple Moving Average
  public List<double> GetMovingAverages(List<Price> prices, int n)
  {
    var movingAverages = new double[prices.Count];
    var runningTotal = 0.0d;       

    for (int i = 0; i < prices.Count; ++i)
    {
      runningTotal += prices[i].Value;
      if( i - n >= 0) {
        var lost = prices[i - n].Value;
        runningTotal -= lost;
        movingAverages[i] = runningTotal / n;
      }
    }
    return movingAverages.ToList();
  }

您可以将本地的双重数组删除。不要将值存储在数组中,而是在循环中调用yield return runningTotal / n;。您需要将返回类型更改为IEnumerable<double> - zumalifeguard

2

2
我发现提供的答案有些占用内存且速度较慢,您要求快速。 添加2个字段,一个用于保持运行总数,另一个用于记录值更改的次数,因为平均值是一组值的总和/计数。我添加了一个Add方法,但您也可以在方法中使用变量...
public class Sample
{
    private decimal sum = 0;
    private uint count = 0;

    public void Add(decimal value)
    {
        sum += value;
        count++;
    }

    public decimal AverageMove => count > 0 ? sum / count : 0;
}

使其线程安全:

最初的回答:

public class ThreadSafeSample
{
private decimal sum = 0;
private uint count = 0;

private static object locker = new object();
public void Add(decimal value)
{
    lock (locker)
    {
        sum += value;
        count++;
    }
}

public decimal AverageMove => count > 0 ? sum / count : 0;

}


3
请注意,这个答案只是一个简单的平均计算。移动平均会有不同的表现。 - EventHorizon

1
如何处理Queue
using System.Collections.Generic;
using System.Linq;

public class MovingAverage
{
    private readonly Queue<decimal> _queue;
    private readonly int _period;

    public MovingAverage(int period)
    {
        _period = period;
        _queue = new Queue<decimal>(period);
    }

    public decimal Compute(decimal x)
    {
        if (_queue.Count >= _period)
        {
            _queue.Dequeue();
        }

        _queue.Enqueue(x);

        return _queue.Average();
    }
}

使用方法:

MovingAverage ma = new MovingAverage(3);

foreach(var val in new decimal[] { 1,2,3,4,5,6,7,8,9 })
{
   Console.WriteLine(ma.Compute(val));
}

1

以下是我尝试的方法。但是请注意,我完全是个业余爱好者,所以这可能是完全错误的。

List<decimal> MovingAverage(int period, decimal[] Data)
{
     decimal[] interval = new decimal[period];
     List<decimal> MAs = new List<decimal>();

     for (int i=0, i < Data.length, i++)
     {
          interval[i % period] = Data[i];
          if (i > period - 1)
          {
               MAs.Add(interval.Average());
          }
     }
     return MAs;
}

应该返回一个十进制数列表,其中包含您数据的移动平均值。

1

这是我在我的应用程序中使用的 MA。

double[] MovingAverage(int period, double[] source)
{
    var ma = new double[source.Length];

    double sum = 0;
    for (int bar = 0; bar < period; bar++)
        sum += source[bar];

    ma[period - 1] = sum/period;

    for (int bar = period; bar < source.Length; bar++)
        ma[bar] = ma[bar - 1] + source[bar]/period
                              - source[bar - period]/period;

    return ma;
}

一旦您对整个数据系列进行了计算,就可以立即获取特定的值。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接