Арифметическое скользящее среднее

Арифметическое скользящее среднее

При обработке экспериментальных данных часто возникает необходимость их усреднения. С этой целью часто используют алгоритм арифметического скользящего среднего.

Скользящее среднее — общее название для семейства функций, значения которых в каждой точке определения равны среднему значению исходной функции за предыдущий период. Скользящие средние обычно используются с данными временных рядов для сглаживания краткосрочных колебаний и выделения основных тенденций или циклов.

Математически скользящее среднее является одним из видов свертки, и поэтому его можно рассматривать как фильтр низких частот, используемых в обработке сигналов.

Простое (арифметическое) скользящее среднее численно равно среднему арифметическому значений исходной функции за установленный период и вычисляется по формуле:

{\displaystyle{\widetilde{f}_k ={\frac {1}{h} \sum_{i=l}^k f_i}}}

где {\displaystyle{f_i}} – исходные значения рассматриваемой функции, {\displaystyle{h=k-l}} – сглаживающий интервал – количество значений исходной функции для расчета скользящего среднего.

На рисунке представлены функции скользящего среднего для исходной последовательности значений с разной величиной сглаживающего интервала.

Скользящее среднее с разным интервалом усреднения

Чем шире сглаживающий интервал, тем более плавным будет график результирующей функции. Однако с другой стороны, увеличение сглаживающего интервала приводит к временному сдвигу усредненной функции относительно исходной.

Реализация на C++

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
#include <iostream>
#include <stdlib.h>
using namespace std;
// Задание начального набора значений
double** getData(int n) 
{
  double** f;
  f = new double* [3];
  f[0] = new double[n];
  f[1] = new double[n];
  f[2] = new double[n];
  for (int i = 0; i < n; i++)
  {
    f[0][i] = (double)i;
    f[1][i] = 8 * (double)i - 3;
    // Добавление случайной составляющей
    //f[1][i] = 8*(double)i - 3 + ((rand()%101)-50)*0.05;
    f[2][i] = 0; // для заполнения усредненными значениями
  }
  return f;
}
// Вычисление результата скользящего среднего
void getMA(double** x, int n, int size) 
{
  // size - количество отсчетов интервала усреднения
  double sumx = 0; // сумма отсчетов на интервале
  double* mas; // массив для хранения size отсчетов
  int index = 0; // индекс элемента массива
  mas = new double[size];
  for (int i = 0; i < n; i++)
    mas[i] = 0;
  for (int i = 0; i < n - size; i++)
  {
    mas[index] = x[1][i];
    index++;
    if (index >= size)
      index = 0; // возврат к началу "окна"
    sumx = 0;
    for (int k = 0; k < size; k++)
      sumx += mas[k];
    x[2][i] = sumx / size;
  }
  return;
}
int main()
{
  double** x;
  int n, h;
  system("chcp 1251");
  system("cls");
  cout << "Введите количество точек: ";
  cin >> n;
  cout << "Введите сглаживающий интервал: ";
  cin >> h;
  x = getData(n);
  getMA(x, n, h);
  cout.width(2);
  cout.fixed;
  cout.precision(1);
  for (int i = 0; i < n; i++)
  {
    cout.width(2);
    cout << x[0][i] << ": ";
    cout.width(4);
    cout.precision(3);
    cout << x[1][i] << " - ";
    cout.width(5);
    cout.precision(3);
    cout << x[2][i] << endl;
  }
  cin.get();
  cin.get();
  return 0;
}

Результат выполнения

Как видим, в результате усреднения имеем на h точек меньше, чем набор исходных значений. Усредненные значения в примере сдвинуты в сторону младших отсчетов, хотя в реальности если номером отсчета выступает время, как правило, сдвиг идет в сторону старших отсчетов.

В примере использовался циклический буфер mas для хранения усредняемых значений. Индекс index движется по циклу, и как только выходит за пределы буфера, возвращается на начальную (нулевую) позицию.

Из предыдущего своего значения простое скользящее среднее может быть получено по следующей рекуррентной формуле:

{\displaystyle{\widetilde{ f}_{k+1} = \widetilde{f}_k — {\frac {f_{k-h}}{h}} + {\frac {f_k}{h}}}}

где {\displaystyle{f_{k-h}}} — значение исходной функции в точке {\displaystyle{k-h}} (в случае временного ряда, самое «раннее» значение исходной функции, используемое для вычисления предыдущей скользящей средней), {\displaystyle{f_k}} — значение исследуемой функции в точке {\displaystyle{k}} (последнее значение).

При реализации рекуррентной формулы удобно использовать массив для хранения значений сглаживающего интервала, а также переменную, содержащую сумму этих значений. Однако такая реализация доступна только в случае если значения функции хранятся в целочисленном виде.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
#include <iostream>
#include <stdlib.h>
using namespace std;
const double pi = 3.1415926535897932384626433832795;
// Задание начального набора значений
void getData(int n, int *y) 
{
  for (int i = 0; i < n; i++)
    y[i] = 1000 * sin(i * 2 * pi / 100);
}
// Вычисление результата скользящего среднего
void getMA(int* y, int *z, int n, int size) 
{
  // size - количество отсчетов интервала усреднения
  int sumx = 0; // сумма отсчетов на интервале (рекуррентная формула)
  int *mas; // массив для хранения size отсчетов
  int index = 0; // индекс элемента массива
  mas = new int[size];
  for (int i = 0; i < size; i++) mas[i] = 0;
  for (int i = 0; i < n - size; i++)   
  {
    sumx -= mas[index];
    mas[index] = y[i];
    sumx += mas[index];
    index++;
    if (index >= size)
      index = 0; // возврат к началу "окна"
    z[i] = sumx / size;
  }
  return;
}
int main()
{
  int n, h;
  int* y, * z;
  system("chcp 1251");
  system("cls");
  cout << "Введите количество точек: ";
  cin >> n;
  cout << "Введите сглаживающий интервал: ";
  cin >> h;
  y = new int[n];
  z = new int[n];
  for (int i = 0; i < n; i++)
  {
    y[i] = 0;
    z[i] = 0;
  }
  getData(n, y);
  getMA(y, z, n, h);
  cout.width(2);
  cout.fixed;
  cout.precision(1);
  for (int i = 0; i < n; i++)
  {
    cout.width(2);
    cout << i << ": ";
    cout.width(4);
    cout.precision(4);
    cout << y[i] << " - ";
    cout.width(4);
    cout.precision(4);
    cout << z[i] << endl;
  }
  system("pause");
  return 0;
}

Результат выполнения

9 комментариев к “Арифметическое скользящее среднее”

  1. Роман

    Доброго времени Елена.
    Я правильно понимаю что у данного расчёта значимый результат в значении,
    уже получается на первой итерации, независимо от окна интервала?
    То есть нет начального лага, который равен длине интервала?

    Возможно ли данный расчёт сделать адаптивным?
    Буду благодарен за пример адаптивной скользящей средней.

    1. Елена Вставская

      Нет, задержка первого корректного результата равна размеру окна.

      1. Роман

        А не знаете, существует ли такой расчёт? Который бы устранял эту начальную задержку. Возможно двухсторонний какой то расчёт.

        1. Елена Вставская

          Можно вначале усреднять на имеющееся количество точек, до достижения окна

          1. Спасибо попробую. Уточните пожалуйста, усреднять на количество точек окна, или на количество точек всей выборки?

          2. Елена Вставская

            Конечно на количество точек окна. Если Вам скользящее среднее нужно. А если просто среднее значение, то сложить все выборки и разделить на количество.

  2. Артур

    Честно говоря я не совсем смог разобраться в коде. У меня есть своя наработка в вычислении скользящего среднего на МК теперь уже на STM32 благодаря Вашей помощи )) Если интересно покритикуйте.

    файл stm32fxx_it.c

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    uint16_t tmp_arr[256] = {0}; // массив где храним мгновенные значения
    uint16_t tmp_rez = 0; // для хранения текущего результата
    uint32_t sum = 0; // здесь хранится сумма всех элементов массива
    uint32_t number = 0; // а сюда запишем результат вычисления среднего скользящего
    void TIM1_UP_IRQHandler(void// обработчик прерывания таймера (интервал через который будем делать замер запуская АЦП)
    {
    HAL_ADC_Start_IT(&hadc1);
    HAL_TIM_IRQHandler(&htim1);
    }

    void ADC1_2_IRQHandler(void// обработчик прерывания по окончанию преобразования
    {
    static uint8_t cnt = 0; // статическая переменная которая будет переполняться, специально делаю так чтобы не использовать условия if и т.д. , но размер окна получается в 256 значений
    sum -= tmp_arr[cnt]; // отнимаем от общ. сум. знач. ячейки массива которую в дальнейшем перезапишем новым знач (т.е. самое старое значение)
    tmp_res = HAL_ADC_GetValue(&hadc1); // по оконч. преобраз, присваиваем результ.временной перем.
    sum += tmp_res; // прибавляем результат в общую сумму
    tmp_arr[cnt] = tmp_res; // перезаписываем значение ячейки с которой работали
    ++cnt; // увеличиваем счетчик
    number = sum >> 8; // делим на 256, т.е. количество элементов массива
    HAL_ADC_IRQHandler(&hadc1);
    }

    собственно все, из недостатков, то что размер окна 256 если делать больше или меньше стандартных размеров типов языка, то придется вводить условие if. А так все шустро летает.

    1. Елена Вставская

      Ну, думаю, что решение неплохое. Если Вам кажется, что окно из 256 отсчетов слишком велико, а условие ставить не хочется, то можно пойти другим путём.
      Например, для окна 16

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      uint16_t tmp_arr[16] = {0};

      static uint8_t cnt = 0;
      sum -= tmp_arr[cnt];
      tmp_res = HAL_ADC_GetValue(&hadc1);
      sum += tmp_res;
      tmp_arr[cnt] = tmp_res;
      ++cnt;
      cnt = cnt % 16; // находим остаток от деления на 16 **
      number = sum >> 4;

      Строку ** можно еще записать с помощью гашения старших битов с помощью логической операции:

      1
      cnt = cnt & 0x0F;

Оставьте комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Прокрутить вверх