Сглаживание данных с помощью алгоритма простого скользящего среднего (SMA)
Введение: Зачем нужно усреднение после гистерезиса?
В предыдущих уроках мы подробно изучили две ключевые проблемы, возникающие при работе с аналоговыми датчиками: флаттеринг (flapping) или "дребезг" на пороговых значениях и наличие постоянного шума в сигнале. Для борьбы с флаттерингом мы внедрили алгоритм гистерезиса, который создает "мертвую зону" между порогами включения и выключения, предотвращая частые переключения исполнительных устройств.
Однако гистерезис, решая одну задачу, не решает другую. Он не избавляет сам сигнал от шума. Показания датчика по-прежнему могут "прыгать" в небольшом диапазоне, даже если физическая величина (например, температура в комнате) остается стабильной. Эти скачки, хоть и отфильтрованные гистерезисом от прямого влияния на реле, все равно искажают реальную картину, мешают качественному мониторингу и могут негативно влиять на более сложные алгоритмы управления.
> 🔗 Связанный материал: Мы подробно рассмотрели природу и проблемы аналогового шума, включая электромагнитные и радиочастотные помехи (ЭМИ/РЧИ), в предыдущем уроке. См. `COURSE-04-M05-L04`.
Здесь в игру вступает сглаживание данных (data smoothing) — процесс фильтрации сигнала для удаления случайного шума и выявления основного тренда. Важно четко разграничить задачи этих двух алгоритмов:
- Гистерезис: Принимает решение о переключении состояния (ВКЛ/ВЫКЛ) на основе порогов. Его задача — предотвратить "дребезг" исполнительного механизма.
- Сглаживание (Усреднение): "Очищает" исходный сигнал от шума, делая его более стабильным и репрезентативным. Его задача — получить более точное представление о реальном значении измеряемой величины.
Представьте, что вы находитесь в комнате с термостатом. Если в комнату на секунду ворвался поток холодного воздуха от открывшейся двери, датчик температуры может кратковременно показать падение на 1-2 градуса. Без сглаживания система отопления может немедленно среагировать и включиться. Однако человеческий мозг работает иначе: он интуитивно понимает, что это было кратковременное колебание, и оценивает среднюю температуру в комнате. Алгоритмы сглаживания, такие как простое скользящее среднее (SMA), позволяют научить контроллер HI действовать так же — реагировать не на сиюминутные скачки, а на стабильное, усредненное значение, что делает работу системы в целом более плавной, предсказуемой и энергоэффективной.
---
Алгоритм простого скользящего среднего (SMA): теория
Простое скользящее среднее (Simple Moving Average, SMA) — это один из самых базовых и широко используемых методов сглаживания данных. Его суть предельно проста: текущее сглаженное значение вычисляется как среднее арифметическое заданного количества (`N`) последних измерений.Формально это можно записать так:
`SMA = (P1 + P2 + ... + PN) / N`
Где:
- `P1, P2, ... PN` — это N последних полученных от датчика значений.
- `N` — это размер окна (window size) или период усреднения. Это ключевой параметр, который определяет степень сглаживания.
Понятие "скользящее" или "движущееся" происходит из-за того, что при поступлении нового значения (`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` будет следующей:
> ⚠️ Внимание: Не забывайте инициализировать массив в контексте. Первая проверка `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]
Исходный код для узла `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] -> [Узел Гистерезиса] -> [Логика управления]Такой подход создает многоуровневую защиту от ложных срабатываний и обеспечивает максимально плавную и корректную работу исполнительных механизмов, будь то клапан отопления, компрессор холодильной установки или группа освещения.
> 🔗 Связанный материал: Для реализации логики гистерезиса на основе сглаженного значения обратитесь к уроку `COURSE-04-M05-L03`, где подробно разобран этот алгоритм и его реализация в узле `Function`.
Что дальше?
Простое скользящее среднее — это отличный стартовый инструмент. Однако у него есть особенность: все значения в "окне" имеют одинаковый вес. В некоторых задачах бывает полезно придавать больший вес более новым данным. Для этого существуют более сложные алгоритмы, такие как взвешенное скользящее среднее (WMA) и экспоненциальное скользящее среднее (EMA), которые мы рассмотрим в последующих уроках этого курса. Они позволяют добиться еще большей гибкости в настройке реакции системы.