52

我知道这是可以通过以下方式实现的:

使用 boost::accumulators,我如何重置滚动窗口大小,它会保留额外的历史记录吗?

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

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

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


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

4

11 回答 11

91

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

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 个左右的数字,这是一种非常简单快捷的方法。

于 2012-06-12T04:44:29.080 回答
20

您只需要一个包含 1000 个元素的循环数组(循环缓冲区),在其中将元素添加到前一个元素并存储它。

它变成了一个递增的总和,你总是可以得到任意两对元素之间的总和,然后除以它们之间的元素数量,得到平均值。

于 2012-06-12T04:50:22.003 回答
19

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

请注意,以下内容将total_as 元素更新为添加/替换,避免了昂贵的O (N) 遍历来按需计算求和所需的平均值。

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

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

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

例子:

// average of last 3 (from 4) samples...
std::cout << Moving_Average<double, double, 3>{}(4)(7)(2)(6) << '\n';
    // "5\n"

// average of last 3 squares...
Moving_Average<double, double, 3> ma;
for (int i = 0; i < 10; ++i)
    std::cout << (i * i) << ':' << ma(i * i) << ' ';
std::cout << '\n';
    // 0:0 1:0.5 4:1.66667 9:4.66667 16:9.66667 25:16.6667 36:25.6667 49:36.6667 64:49.6667 81:64.6667

Total由一个不同的参数T来支持,例如long long在总计 1000long秒时使用 a,int对于chars 或 adouble到总计floats。

问题

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

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

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

  • 1,我们执行total += 1,但仍然,因为“1”太微不足道而无法改变像 1E17 这样大的数字total_ == 1E17的 64 位表示,然后我们注入double

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

您可以添加存储最近最近的代码,total_如果当前total_太小(模板参数可以提供乘法阈值),您可以从数组total_中的所有样本重新计算 (并设置为 new ),但是我会把它留给足够关心的读者。samples_highest_recent_total_total_

于 2012-06-12T05:19:52.887 回答
15

您可以通过对输入流应用加权平均值来近似滚动平均值。

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 个桶)。

于 2012-06-12T05:29:49.903 回答
4

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

#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);
    }

};
于 2013-12-09T20:58:30.787 回答
1

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

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

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

于 2015-12-01T17:36:55.007 回答
1

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

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

平均值 = 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 给出的答案的实际实现。有时更容易理解一个具体的例子。

于 2018-01-25T01:09:10.527 回答
0

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

于 2012-06-12T04:50:58.307 回答
0

增加@Nilesh 的回答(归功于他),您可以:

  • 跟踪总和,不需要每次除然后乘,产生错误
  • 避免使用 % 运算符的 if 条件

这是UNTESTED示例代码来展示这个想法,它也可以被包装到一个类中:

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;
}

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

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

那是对的。100 个元素和 10 个缓冲区的滚动平均值给出了不同的结果:10 个淡入,90 个完美滚动 10 个元素,最后 10 个淡出,对于 100 个输入的数字,总共有 110 个结果!您可以选择显示哪些内容(如果最好直接从旧到最近,或者向后,从最近到旧)。

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

用法是这样的:

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();
于 2020-11-03T08:48:50.473 回答
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% 准确的(对于向量中的 avg / 值)。很少维护正确的顺序,因为它开始向后重写(在 0 处),因此在示例 {5,1,2,3,4} 等中,第 5 个索引将在 0 处。

于 2021-02-06T17:35:56.357 回答
-1

使用列表的 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();
}
于 2014-01-27T00:46:21.230 回答