ГлавнаяАкадемияДатчики и входы: нормализация сигналов → Сглаживание данных с помощью алгоритма простого скользящего среднего (SMA)

Сглаживание данных с помощью алгоритма простого скользящего среднего (SMA)

Урок 4 · Датчики и входы: нормализация сигналов · 30 мин · theory

Введение: Зачем нужно усреднение после гистерезиса?

В предыдущих уроках мы подробно изучили две ключевые проблемы, возникающие при работе с аналоговыми датчиками: флаттеринг (flapping) или "дребезг" на пороговых значениях и наличие постоянного шума в сигнале. Для борьбы с флаттерингом мы внедрили алгоритм гистерезиса, который создает "мертвую зону" между порогами включения и выключения, предотвращая частые переключения исполнительных устройств.

Однако гистерезис, решая одну задачу, не решает другую. Он не избавляет сам сигнал от шума. Показания датчика по-прежнему могут "прыгать" в небольшом диапазоне, даже если физическая величина (например, температура в комнате) остается стабильной. Эти скачки, хоть и отфильтрованные гистерезисом от прямого влияния на реле, все равно искажают реальную картину, мешают качественному мониторингу и могут негативно влиять на более сложные алгоритмы управления.

> 🔗 Связанный материал: Мы подробно рассмотрели природу и проблемы аналогового шума, включая электромагнитные и радиочастотные помехи (ЭМИ/РЧИ), в предыдущем уроке. См. `COURSE-04-M05-L04`.

Здесь в игру вступает сглаживание данных (data smoothing) — процесс фильтрации сигнала для удаления случайного шума и выявления основного тренда. Важно четко разграничить задачи этих двух алгоритмов:

Представьте, что вы находитесь в комнате с термостатом. Если в комнату на секунду ворвался поток холодного воздуха от открывшейся двери, датчик температуры может кратковременно показать падение на 1-2 градуса. Без сглаживания система отопления может немедленно среагировать и включиться. Однако человеческий мозг работает иначе: он интуитивно понимает, что это было кратковременное колебание, и оценивает среднюю температуру в комнате. Алгоритмы сглаживания, такие как простое скользящее среднее (SMA), позволяют научить контроллер HI действовать так же — реагировать не на сиюминутные скачки, а на стабильное, усредненное значение, что делает работу системы в целом более плавной, предсказуемой и энергоэффективной.

---

Алгоритм простого скользящего среднего (SMA): теория

Простое скользящее среднее (Simple Moving Average, SMA) — это один из самых базовых и широко используемых методов сглаживания данных. Его суть предельно проста: текущее сглаженное значение вычисляется как среднее арифметическое заданного количества (`N`) последних измерений.

Формально это можно записать так:

`SMA = (P1 + P2 + ... + PN) / N`

Где:

Понятие "скользящее" или "движущееся" происходит из-за того, что при поступлении нового значения (`PN+1`) окно сдвигается: самое старое значение (`P1`) отбрасывается, а самое новое (`PN+1`) добавляется в набор для расчета.

Пошаговый пример работы SMA

Давайте наглядно разберем, как работает алгоритм. Предположим, у нас есть датчик освещенности, и мы выбрали размер окна N=5.

| Шаг | Входное значение | Набор данных в "окне" (последние 5) | Сумма в окне | Результат SMA (Сумма / 5) |

| --- | ---------------- | ------------------------------------- | ------------- | -------------------------- |

| 1 | 402 люкс | `[402]` | 402 | - (недостаточно данных) |

| 2 | 405 люкс | `[402, 405]` | 807 | - (недостаточно данных) |

| 3 | 398 люкс | `[402, 405, 398]` | 1205 | - (недостаточно данных) |

| 4 | 410 люкс | `[402, 405, 398, 410]` | 1615 | - (недостаточно данных) |

| 5 | 408 люкс | `[402, 405, 398, 410, 408]` | 2023 | 404.6 |

| 6 | 401 люкс | `[405, 398, 410, 408, 401]` | 2022 | 404.4 |

| 7 | 395 люкс | `[398, 410, 408, 401, 395]` | 2012 | 402.4 |

Как видите, на шаге 6 пришло новое значение `401`. Самое старое значение из предыдущего окна (`402`) было отброшено. Таким образом, "окно" постоянно "скользит" по потоку данных. Выходной сигнал (результат SMA) гораздо более плавный, чем входной.

Ключевой компромисс: выбор размера окна N

Выбор `N` — это всегда компромисс между степенью сглаживания и инертностью системы.

| Параметр | Большое окно (например, N=30) | Малое окно (например, N=5) |

| ------------- | ------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------ |

| Плюсы | ✅ Сильное сглаживание, отличная фильтрация шума. | ✅ Быстрая реакция на реальные изменения физической величины. Низкая инертность. |

| Минусы | ❌ Высокая инертность. Система будет медленно реагировать на резкие, но реальные изменения. | ❌ Слабое сглаживание. Остаточный шум может все еще присутствовать в выходном сигнале. |

| Пример | Температура бетонной стяжки теплого пола. | Датчик освещенности для управления светом. |

> 💡 Подсказка: Выбирайте размер окна `N` исходя из инертности физического процесса. Для медленно меняющихся параметров, таких как температура в хорошо изолированном помещении или нагрев бойлера, можно использовать большое окно (N = 10-30). Для быстро меняющихся параметров, таких как уровень освещенности или давление в системе, необходимо малое окно (N = 3-5), чтобы реакция системы была адекватной скорости изменения параметра.

---

Реализация SMA в Node-RED с помощью ноды Function

Для реализации алгоритма скользящего среднего в Node-RED нам понадобится всего один узел `Function` и механизм для хранения последних `N` значений между вызовами этого узла. Идеальным инструментом для этой цели является контекст потока (flow context).

Контекст позволяет узлам на одной вкладке (flow) обмениваться данными и сохранять свое состояние. Для нашей задачи мы будем хранить массив последних измерений в переменной контекста.

Алгоритм в коде

Логика внутри узла `Function` будет следующей:

  • Задать размер окна `N`.
  • Получить массив предыдущих значений из `flow.context`. Если массив еще не существует (первый запуск), создать пустой массив.
  • Добавить новое значение из `msg.payload` в конец массива.
  • Если длина массива стала больше `N`, удалить самое старое значение из начала массива.
  • Если в массиве уже накоплено `N` значений, посчитать их сумму и разделить на `N`, чтобы получить среднее.
  • Поместить вычисленное среднее в `msg.payload`.
  • Сохранить обновленный массив обратно в `flow.context` для следующего вызова.
  • Передать сообщение дальше по потоку.
  • > ⚠️ Внимание: Не забывайте инициализировать массив в контексте. Первая проверка `flow.get('values') || []` критически важна, чтобы избежать ошибок `undefined` при первом запуске потока или после перезагрузки контроллера HI, когда контекст еще не содержит нужных данных. Без этой проверки поток завершится с ошибкой при попытке вызвать метод `.push()` на неопределенной переменной.

    Скелет кода для узла `Function`

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

    // 1. Задаем размер окна усреднения
    

    const windowSize = 10; // Например, усредняем по 10 последним значениям

    // 2. Получаем массив из контекста потока. Если его нет, создаем пустой.

    // 'sma_values' - это уникальное имя хранилища для этого конкретного датчика.

    let values = flow.get('sma_values') || [];

    // Получаем новое значение. Убедимся, что это число.

    let newValue = parseFloat(msg.payload);

    // Проверяем, что получили валидное число

    if (isNaN(newValue)) {

    // Если данные невалидны, просто пропускаем их и ничего не делаем.

    // Выводим предупреждение в лог.

    node.warn("Получено нечисловое значение: " + msg.payload);

    return null; // Прерываем выполнение и не передаем сообщение дальше

    }

    // 3. Добавляем новое значение в конец массива

    values.push(newValue);

    // 4. Если массив стал слишком длинным, удаляем самый старый элемент (первый)

    if (values.length > windowSize) {

    values.shift(); // .shift() удаляет первый элемент массива

    }

    // 5. Сохраняем обновленный массив обратно в контекст

    flow.set('sma_values', values);

    // 6. Проверяем, достаточно ли у нас данных для расчета среднего

    if (values.length === windowSize) {

    // Считаем сумму всех элементов в массиве

    const sum = values.reduce((a, b) => a + b, 0);

    // Вычисляем среднее

    const average = sum / windowSize;

    // 7. Формируем новое сообщение с усредненным значением

    // Сохраняем исходное "сырое" значение для отладки

    msg.raw_value = msg.payload;

    // В основной payload кладем усредненное значение

    msg.payload = average;

    // Обновляем статус ноды для визуальной диагностики

    // toFixed(2) округляет до 2 знаков после запятой

    node.status({ fill: "green", shape: "dot", text: "SMA: " + average.toFixed(2) });

    // 8. Передаем сообщение дальше

    return msg;

    } else {

    // Если данных еще не хватает, не отправляем ничего.

    // Ждем, пока "окно" заполнится.

    node.status({ fill: "yellow", shape: "ring", text: `Накопление данных: ${values.length}/${windowSize}` });

    return null;

    }

    Этот код является надежным шаблоном, который можно использовать для сглаживания любого числового потока данных.

    ---

    Пример: Сглаживание показаний датчика температуры PT1000 через Modbus

    Давайте применим полученные знания на практике. Представим типовую задачу: на объекте установлен контроллер HI, который считывает показания с датчика температуры PT1000, подключенного через модуль ввода-вывода по шине Modbus. Опрос происходит каждые 5 секунд. Показания слегка "шумят" из-за наводок. Наша цель — получить стабильное значение температуры.

    Поток в Node-RED будет выглядеть так:
    [Inject: 5s] --> [Modbus-Read] --> [Function: SMA Temp] --> [Debug: Result]
    
  • `Inject`: Настроен на запуск каждые 5 секунд для инициирования опроса.
  • `Modbus-Read`: Настроен на чтение регистра с температурой с Modbus-устройства (как мы разбирали в модуле по протоколам). На выходе он отдает `msg.payload` с числовым значением, например, `21.5`.
  • `Function: SMA Temp`: Сюда мы вставим наш код для SMA.
  • `Debug`: Позволит нам видеть результат работы — как исходные, так и сглаженные значения.
  • Исходный код для узла `Function: SMA Temp`

    Используем наш шаблонный код, адаптировав его под конкретную задачу.

    // ID: FLOW-AUTO-TEMP-SMA-001
    

    // Назначение: Сглаживание показаний датчика температуры бойлера PT1000

    // 1. Задаем размер окна усреднения

    const windowSize = 10; // Усредняем по 10 последним значениям (10 * 5с = 50с)

    // 2. Используем уникальное имя для контекста, чтобы не пересекаться с другими датчиками

    let values = flow.get('boiler_temp_values') || [];

    // Modbus-узел часто возвращает данные в массиве, извлекаем первое значение.

    // В вашем случае может быть просто msg.payload, если узел уже настроен.

    let newValue = parseFloat(Array.isArray(msg.payload) ? msg.payload[0] : msg.payload);

    // 3. Валидация

    if (isNaN(newValue) || newValue < -50 || newValue > 200) { // Разумные пределы для PT1000

    node.error("Получено невалидное значение температуры: " + newValue, msg);

    return null;

    }

    // 4. Добавляем новое значение в массив

    values.push(newValue);

    // 5. Обрезаем массив, если он превысил размер окна

    if (values.length > windowSize) {

    values.shift();

    }

    // 6. Сохраняем массив в контекст

    flow.set('boiler_temp_values', values);

    // 7. Расчет и отправка результата, если окно заполнено

    if (values.length === windowSize) {

    const sum = values.reduce((a, b) => a + b, 0);

    const average = sum / windowSize;

    // 8. Формируем исходящее сообщение по "Контракту сообщения"

    msg.payload = {

    value: parseFloat(average.toFixed(2)), // Округляем до 2 знаков

    unit: "°C",

    source: "PT1000_Boiler_Modbus_ID15",

    ts: Date.now()

    };

    // Добавляем мета-информацию для отладки

    msg.meta = {

    raw_value: newValue,

    window_size: windowSize,

    sma_values: values

    };

    node.status({ fill: "green", shape: "dot", text: `SMA: ${msg.payload.value} °C` });

    return msg;

    } else {

    // Отображаем статус накопления

    node.status({ fill: "yellow", shape: "ring", text: `Накопление: ${values.length}/${windowSize}` });

    return null;

    }

    Сравнение данных в панели Debug

    Подключив два узла `Debug` (один до узла `Function`, другой после), мы увидим наглядную разницу:

    Вход (`msg.payload` из Modbus-Read):
    21.3
    

    21.5

    21.4

    21.6

    21.2

    ...

    Выход (из узла `Function: SMA Temp`):

    После того как окно из 10 значений заполнится, мы начнем получать сглаженные сообщения.

    Пример входящего сообщения:

    {
    

    "payload": 21.5,

    "topic": "PT1000_Boiler"

    }

    Пример исходящего сообщения:

    {
    

    "payload": {

    "value": 21.48,

    "unit": "°C",

    "source": "PT1000_Boiler_Modbus_ID15",

    "ts": 1678886400000

    },

    "topic": "PT1000_Boiler",

    "meta": {

    "raw_value": 21.5,

    "window_size": 10,

    "sma_values": [21.4, 21.6, 21.2, 21.5, 21.7, 21.4, 21.5, 21.6, 21.4, 21.5]

    }

    }

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

    ---

    Итоги и комбинирование техник: SMA + Гистерезис

    В этом уроке мы освоили простое скользящее среднее (SMA) — мощный и простой в реализации алгоритм, который действует как фильтр низких частот, эффективно "очищая" сигнал от высокочастотного случайного шума.

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

    Синергия алгоритмов: SMA + Гистерезис

    Наилучших результатов в построении надежных систем управления можно добиться, комбинируя изученные нами техники. Рекомендуемая последовательность обработки сигнала выглядит следующим образом:

    [Сырой сигнал от датчика] -> [Узел SMA] -> [Узел Гистерезиса] -> [Логика управления]
  • Сначала мы пропускаем "шумный" сырой сигнал через фильтр SMA. На выходе получаем чистое, сглаженное значение.
  • Затем это сглаженное значение мы подаем на вход алгоритма гистерезиса.
  • Гистерезис, работая уже с чистыми данными, принимает решение о переключении (ВКЛ/ВЫКЛ), будучи полностью защищенным как от шума (благодаря SMA), так и от дребезга на пороге (благодаря своей логике двойного порога).
  • Такой подход создает многоуровневую защиту от ложных срабатываний и обеспечивает максимально плавную и корректную работу исполнительных механизмов, будь то клапан отопления, компрессор холодильной установки или группа освещения.

    > 🔗 Связанный материал: Для реализации логики гистерезиса на основе сглаженного значения обратитесь к уроку `COURSE-04-M05-L03`, где подробно разобран этот алгоритм и его реализация в узле `Function`.

    Что дальше?

    Простое скользящее среднее — это отличный стартовый инструмент. Однако у него есть особенность: все значения в "окне" имеют одинаковый вес. В некоторых задачах бывает полезно придавать больший вес более новым данным. Для этого существуют более сложные алгоритмы, такие как взвешенное скользящее среднее (WMA) и экспоненциальное скользящее среднее (EMA), которые мы рассмотрим в последующих уроках этого курса. Они позволяют добиться еще большей гибкости в настройке реакции системы.