用C ++计算滚动/移动平均值

时间:2012-06-12 04:38:10

标签: c++ boost moving-average

我知道这可以通过提升实现:

Using boost::accumulators, how can I reset a rolling window size, does it keep extra history?

但我真的想避免使用提升。我用谷歌搜索,没有找到任何合适或可读的例子。

基本上我想使用最近的1000个数字作为数据样本来跟踪正在进行的浮点数流的移动平均值。

实现这一目标的最简单方法是什么?


我尝试使用圆形阵列,指数移动平均线和更简单的移动平均线,发现圆形阵列的结果最适合我的需要。

11 个答案:

答案 0 :(得分:78)

如果您的需求很简单,您可以尝试使用指数移动平均线。

http://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average

简单地说,您创建一个累加器变量,并且当您的代码查看每个示例时,代码会使用新值更新累加器。你选择一个介于0和1之间的常量“alpha”,然后计算:

accumulator = (alpha * new_value) + (1.0 - alpha) * accumulator

您只需要找到“alpha”值,其中给定样本的效果仅持续约1000个样本。

嗯,我现在还不确定它是否适合你,现在我把它放在这里。问题是,对于指数移动平均线而言,1000是一个相当长的窗口;我不确定是否有一个alpha会在过去的1000个数字上传播平均值,而在浮点计算中没有下溢。但是如果你想要一个较小的平均值,比如30个数左右,这是一个非常简单快捷的方法。

答案 1 :(得分:19)

你只需要一个包含1000个元素的圆形数组,在这里你可以将元素添加到前一个元素并存储它......它变成了一个增加的总和,你可以随时得到任意两对元素之间的和,并除以通过它们之间的元素数量,得出平均值。

答案 2 :(得分:14)

您可以通过在输入流上应用加权平均值来估算滚动平均值。

template <unsigned N>
double approxRollingAverage (double avg, double input) {
    avg -= avg/N;
    avg += input/N;
    return avg;
}

这样,您不需要维护1000个桶。但是,这是一个近似值,因此它的值与真正的滚动平均值不完全匹配。

编辑:刚注意到@ steveha的帖子。这相当于指数移动平均线,alpha为1 / N(在这种情况下我将N设为1000来模拟1000个桶)。

答案 3 :(得分:13)

  

基本上我想使用最近的1000个数字作为数据样本来跟踪正在进行的浮点数流的移动平均值。

请注意,下面将total_更新为添加/替换的元素,避免代价高昂的 O (N)遍历来计算平均值所需的总和 - 按需。

template <typename T, typename Total, size_t N>
class Moving_Average
{
  public:
    void operator()(T sample)
    {
        if (num_samples_ < N)
        {
            samples_[num_samples_++] = sample;
            total_ += sample;
        }
        else
        {
            T& oldest = samples_[num_samples_++ % N];
            total_ += sample - oldest;
            oldest = sample;
        }
    }

    operator double() const { return total_ / std::min(num_samples_, N); }

  private:
    T samples_[N];
    size_t num_samples_{0};
    Total total_{0};
};

TotalT的参数不同,以支持例如使用long long时总计1000 longint chardoublefloat

<强> 问题

这有点有缺陷,因为num_samples_可以在概念上回绕到0,但很难想象有人有2 ^ 64个样本:如果担心,请使用额外的bool数据成员来记录容器第一次在数组周围循环num_samples_时填充(最好重命名为“pos”无关紧要的东西。)

另一个问题是浮点精度所固有的,可用T = double,N = 2的简单场景来说明:我们从total_ = 0开始,然后注入样本...

  • 1E17,我们执行total_ += 1E17,所以total_ == 1E17,然后注入

  • 1,我们执行total += 1,但仍然total_ == 1E17,因为“1”对于更改数字的64位double表示来说太微不足道了1E17,然后我们注入

  • 2,我们执行total += 2 - 1E17,其中2 - 1E17首先被评估并产生-1E17,因为2失去了不精确/无意义,所以对于我们的总共1E17我们加上-1E17和total_变为0,尽管我们希望total_的当前样本为1和2,我们的移动平均值将计算为0而不是1.5。当我们添加另一个样本时,我们将从total_中减去“最老的”1,尽管它从未被正确地合并到其中;我们的total_和移动平均线可能仍然存在错误。

您可以添加存储最近最高total_的代码,如果当前total_太小(模板参数可以提供乘法阈值),则重新计算{{1}来自total_数组中的所有样本(并设置samples_到新highest_recent_total_),但我会将其留给充分关心的读者。

答案 4 :(得分:3)

计算滚动平均值和滚动标准偏差的简单类:

#define _stdev(cnt, sum, ssq) sqrt((((double)(cnt))*ssq-pow((double)(sum),2)) / ((double)(cnt)*((double)(cnt)-1)))

class moving_average {
private:
    boost::circular_buffer<int> *q;
    double sum;
    double ssq;
public:
    moving_average(int n)  {
        sum=0;
        ssq=0;
        q = new boost::circular_buffer<int>(n);
    }
    ~moving_average() {
        delete q;
    }
    void push(double v) {
        if (q->size() == q->capacity()) {
            double t=q->front();
            sum-=t;
            ssq-=t*t;
            q->pop_front();
        }
        q->push_back(v);
        sum+=v;
        ssq+=v*v;
    }
    double size() {
        return q->size();
    }
    double mean() {
        return sum/size();
    }
    double stdev() {
        return _stdev(size(), sum, ssq);
    }

};

答案 5 :(得分:1)

您可以实施ring buffer。创建一个包含1000个元素的数组,以及一些用于存储开始和结束索引以及总大小的字段。然后将最后1000个元素存储在环形缓冲区中,并根据需要重新计算平均值。

答案 6 :(得分:0)

10个项目的简单移动平均线,使用列表:

#include <list>

std::list<float> listDeltaMA;

float getDeltaMovingAverage(float delta)
{
    listDeltaMA.push_back(delta);
    if (listDeltaMA.size() > 10) listDeltaMA.pop_front();
    float sum = 0;
    for (std::list<float>::iterator p = listDeltaMA.begin(); p != listDeltaMA.end(); ++p)
        sum += (float)*p;
    return sum / listDeltaMA.size();
}

答案 7 :(得分:0)

一种方法是循环存储缓冲区数组中的值。并以这种方式计算平均值。

int j = (int) (counter % size);
buffer[j] = mostrecentvalue;
avg = (avg * size - buffer[j - 1 == -1 ? size - 1 : j - 1] + buffer[j]) / size;

counter++;

// buffer[j - 1 == -1 ? size - 1 : j - 1] is the oldest value stored

整个事情在一个循环中运行,其中最近的值是动态的。

答案 8 :(得分:0)

我经常在硬实时系统中使用它,这些系统具有相当疯狂的更新速率(50kilosamples / sec)因此我通常预先计算标量。

计算N个样本的移动平均值: 标量1 = 1 / N; scalar2 = 1 - 标量1; //或(1 - 1 / N) 然后:

Average = currentSample * scalar1 + Average * scalar2;

示例:平均10个元素的滑动

double scalar1 = 1.0/10.0;  // 0.1
double scalar2 = 1.0 - scalar1; // 0.9
bool first_sample = true;
double average=0.0;
while(someCondition)
{
   double newSample = getSample();
   if(first_sample)
   {
    // everybody forgets the initial condition *sigh*
      average = newSample;
      first_sample = false;
   }
   else
   {
      average = (sample*scalar1) + (average*scalar2);
   }
 }

注意:这只是上面steveha给出的答案的实际实现。 有时,更容易理解一个具体的例子。

答案 9 :(得分:0)

增加@Nilesh的答案(功劳归他所有),您可以:

  • 保持总和的轨迹,无需每次都相除然后相乘,从而产生误差
  • 避免使用%运算符的条件

这是未测试的示例代码,用于说明该想法,也可以将其包装到一个类中:

const unsigned int size=10; // ten elements buffer

unsigned int counterPosition=0;
unsigned int counterNum=0;

int buffer[size];
long sum=0;

void reset() {
    for(int i=0;i<size;i++) {
        buffer[i]=0;
    }
}

float addValue(int value) {
    unsigned  int oldPos = ((counterPosition + 1) % size);

    buffer[counterPosition] = value;
    sum = (sum - buffer[oldPos] + value); 

    counterPosition=(counterPosition+1) % size;
    if(counterNum<size) counterNum++;

    return ((float)sum)/(float)counterNum;
}

float removeValue() {
    unsigned  int oldPos =((counterPosition + 1) % size);

    buffer[counterPosition] = 0;
    sum = (sum - buffer[oldPos]); 

    if(counterNum>1) { // leave one last item at the end, forever
        counterPosition=(counterPosition+1) % size;
        counterNum--; // here the two counters are different
    }
    return ((float)sum)/(float)counterNum;
}

应注意,如果将缓冲区重置为全零,则该方法在接收中的第一个值时效果很好,因为-buffer [oldPos]为零且计数器增加。第一个输出是收到的第一个数字。第二个输出仅是前两个的平均值,依此类推,直到它们到达size项时,它们的值才逐渐消失。

还值得考虑的是,如果您在输入数组的结尾处停止,则与其他任何滚动平均值方法一样,该方法也是不对称的,因为在结尾处不会发生相同的衰减(它可能会在数据结束后通过正确的计算发生)。

是正确的。 100个元素的平均滚动和10个缓冲区的滚动会得到不同的结果:10个淡入,90个完美滚动10个元素,最后10个淡出,给出 100个数字总计110个结果!您可以决定显示哪些内容(如果最好的话,还是从旧到新,或者从新到旧,向后退)。

要在结束后正确淡出,可以继续逐个加零,直到达到size元素为止,每次减少1项(仍然跟踪旧值的正确位置) )。

用法是这样的:

int avg=0;
reset();

avg=addValue(2); // Rpeat for 100 times
avg=addValue(3); // Use avg value

...

avg=addValue(-4);
avg=addValue(12); // last numer, 100th input 

// If you want to fade out repeat 10 times after the end of data:

avg=removeValue(); // Rpeat for last 10 times after data has finished
avg=removeValue(); // Use avg value
...
avg=removeValue();
avg=removeValue();

答案 10 :(得分:0)

我使用了双端队列...似乎对我有用。此示例有一个向量,但您可以跳过该方面并将它们添加到双端队列中。

#include <deque>

template <typename T>
double mov_avg(vector<T> vec, int len){
  deque<T> dq = {};
  for(auto i = 0;i < vec.size();i++){
    if(i < len){
      dq.push_back(vec[i]);
    }
    else {
      dq.pop_front();
      dq.push_back(vec[i]);
    }
  }
  double cs = 0;
  for(auto i : dq){
    cs += i;
  }
  return cs / len;
}



//Skip the vector portion, track the input number (or size of deque), and the value.


  double len = 10;
  double val; //Accept as input
  double instance; //Increment each time input accepted.

  deque<double> dq;
  if(instance < len){
      dq.push_back(val);
  }
  else {
      dq.pop_front();
      dq.push_back(val);
    }
  }
  double cs = 0;
  for(auto i : dq){
    cs += i;
  }
  double rolling_avg = cs / len;

//为了进一步简化——向其添加值,然后简单地平均双端队列。

 int MAX_DQ = 3;
 void add_to_dq(deque<double> &dq, double value){
    if(dq.size() < MAX_DQ){
      dq.push_back(value);
    }else {
      dq.pop_front();
      dq.push_back(value);
    }
  }
 

我偶尔使用的另一种技巧是使用 mod 覆盖向量中的值。

  vector<int> test_mod = {0,0,0,0,0};
  int write = 0;
  int LEN = 5;
  
  int instance = 0; //Filler for N -- of Nth Number added.
  int value = 0; //Filler for new number.

  write = instance % LEN;
  test_mod[write] = value;
  //Will write to 0, 1, 2, 3, 4, 0, 1, 2, 3, ...
  //Then average it for MA.

  //To test it...
  int write_idx = 0;
  int len = 5;
  int new_value;
  for(auto i=0;i<100;i++){
      cin >> new_value;
      write_idx = i % len;
      test_mod[write_idx] = new_value;

最后一个(hack)没有桶、缓冲区、循环,什么都没有。只是一个被覆盖的向量。它是 100% 准确的(对于向量中的平均值/值)。很少维护正确的顺序,因为它开始向后重写(在 0 处),因此在示例 {5,1,2,3,4} 等中,第 5 个索引将位于 0。