Функция скользящего среднего для регенерации на графике является самым обыденным механизмом, чтобы сделать график более читаемым с одной стороны, и, одним из вариантов нормализации данных на основании которых можно строить отчеты, с другой.
Типичными данными, которые нужно нормализовывать являются данные топливных датчиков.
Ниже я привожу код для C# который можно copy/paste для вашего использования.
Особенностями подхода является два момента:
-
Наличие функция для расчета стандартного отклонения. Эта функция вычисляет стандартное отклонение, которое измеряет, насколько значения в наборе данных отклоняются от среднего.
Более высокое стандартное отклонение указывает на более разбросанные данные, тогда как более низкое стандартное отклонение указывает на данные, которые более сконцентрированы вокруг среднего.
Зачем это нужно?
Для того, чтобы не заморачиваться с выбором "окна" усреднения и, поэтому, расчет окна усреднения на основе стандартного отклонения, выполняется динамически.
При усреднении данных происходит их потеря с обоих концов исходного массива. Простейшим решением является добавление одной или нескольких точек близких к уже вычисленным данным. В моем случае, я добавляю по одной точке с обоих сторон массива для компенсации линии тренда, но вы можете проделать самостоятельную работу и использовать среднее количество точек на интервале "окна" с тем, чтобы генерировать их и дополнить массив.
/// <summary>
/// Функция для нормализации данных (сглаживания), возвращающая сглаженные данные и соответствующие значения X.
/// Вычисляет стандартное отклонение и динамически определяет размер окна для скользящего среднего.
/// </summary>
/// <param name="xValues">Исходные значения X как DateTime</param>
/// <param name="yValues">Исходные значения Y</param>
/// <param name="isEndDataPoints">Достраивать концевые точки данных</param>
/// <returns>Кортеж со сглаженными значениями X и Y</returns>
public static (List<DateTime> smoothedX, List<double> smoothedY) NormalizeDataWithXTime(List<DateTime> xValues, List<double> yValues,
bool isEndDataPoints = true)
{
// Рассчитать стандартное отклонение данных.
var stdDev = CalculateStandardDeviationWithXTime(yValues);
// Динамический расчет windowSize на основе стандартного отклонения
var baseWindowSize = yValues.Count / 10; // Базовое окно 10% от количества данных
var windowSize = Math.Max(1, baseWindowSize + (int)(stdDev / 10)); // Увеличить окно на основе отклонения
// Применить скользящее среднее для сглаживания данных
var (smoothedX, smoothedY) = MovingAverageWithXTime(xValues, yValues, windowSize);
// Дополнить данными сокращение точек сначала и конца периода
if (isEndDataPoints && smoothedX.Count > 0 && smoothedY.Count > 0)
{
var littleAverage = windowSize / 2; // Сократим окно для извлечения точек округления
// Добавить одну усредненную точку данных в начале
double startAvgY = yValues.Take(littleAverage).Average();
smoothedY.Insert(0, startAvgY);
smoothedX.Insert(0, xValues[0]);
// Добавить одну усредненную точку данных в конце
double endAvgY = yValues.Skip(yValues.Count - littleAverage).Take(littleAverage).Average();
smoothedY.Add(endAvgY);
smoothedX.Add(xValues[^1]);
}
Console.WriteLine($"\nСтандартное отклонение: {stdDev}");
Console.WriteLine($"Динамический размер окна: {windowSize}");
return (smoothedX, smoothedY);
}
/// <summary>
/// Функция для скользящего среднего, также возвращающая соответствующие значения X.
/// </summary>
/// <param name="xValues">Исходные значения X как DateTime</param>
/// <param name="yValues">Исходные значения Y для сглаживания</param>
/// <param name="windowSize">Размер "окна"</param>
/// <returns>Кортеж со значениями X и Y для сглаженной линии</returns>
public static (List<DateTime> smoothedX, List<double> smoothedY) MovingAverageWithXTime(List<DateTime> xValues, List<double> yValues,
int windowSize = 3)
{
var smoothedY = new List<double>();
var smoothedX = new List<DateTime>();
for (var i = 0; i < yValues.Count - windowSize + 1; i++)
{
var averageY = yValues.Skip(i).Take(windowSize).Average();
var midXTicks = (long)xValues.Skip(i).Take(windowSize).Average(x => x.Ticks); // Средний X для окна
smoothedY.Add(averageY);
smoothedX.Add(new DateTime(midXTicks)); // Преобразовать обратно в DateTime
}
return (smoothedX, smoothedY);
}
/// <summary>
/// Функция для расчета стандартного отклонения.
///
/// Эта функция вычисляет стандартное отклонение, которое измеряет, насколько значения в наборе данных отклоняются от среднего.
/// Более высокое стандартное отклонение указывает на более разбросанные данные, тогда как более низкое стандартное отклонение указывает на данные, которые более сконцентрированы вокруг среднего.
/// </summary>
/// <param name="data">Исходные данные</param>
/// <returns>Значение стандартного отклонения</returns>
public static double CalculateStandardDeviationWithXTime(List<double> data)
{
// Найдите среднее значение.
var average = data.Average();
// Найдите отклонение каждого элемента от среднего значения. Возведите каждое отклонение в квадрат.
var sumOfSquaresOfDifferences = data.Select(val => (val - average) * (val - average)).Sum();
// Найдите среднее квадратов отклонений. Квадратный корень из дисперсии дает стандартное отклонение.
var stdDev = Math.Sqrt(sumOfSquaresOfDifferences / data.Count);
return stdDev; // Стандартное отклонение.
}
И код для формы
public partial class Form1 : Form
{
private readonly Series fuelLevelSeries;
private readonly Series avgFuelLevelSeries;
private readonly List<DateTime> xValues = [];
private readonly List<double> yValues = [];
public Form1()
{
InitializeComponent();
fuelLevelSeries = CreateSeries("Уровень топлива", Color.Blue, 3);
avgFuelLevelSeries = CreateSeries("Средний уровень топлива", Color.Red, 2);
chart1.Series.Add(fuelLevelSeries);
chart1.Series.Add(avgFuelLevelSeries);
ConfigureChartAxes();
chart1.MouseMove += Chart1_MouseMove;
GenerateData();
DrawGridLines();
}
private void ConfigureChartAxes()
{
chart1.ChartAreas[0].AxisX.LabelStyle.Format = "dd.MM.yy HH:mm:ss";
chart1.ChartAreas[0].AxisX.Interval = 5;
chart1.ChartAreas[0].AxisX.IntervalType = DateTimeIntervalType.Minutes;
}
private void DrawGridLines()
{
var chartArea = chart1.ChartAreas[0];
chartArea.AxisX.MajorGrid.LineColor = Color.Gray;
chartArea.AxisY.MajorGrid.LineColor = Color.Gray;
}
private void Chart1_MouseMove(object? sender, MouseEventArgs e)
{
var result = chart1.HitTest(e.X, e.Y);
if (result.ChartElementType == ChartElementType.DataPoint)
{
DisplayTooltip(result.Series, result.PointIndex, e.Location);
}
else
{
ResetCursorAndTooltip();
}
}
private void DisplayTooltip(Series series, int dataPoint, Point location)
{
chart1.Cursor = Cursors.Cross;
if (dataPoint >= 0)
{
var label = series.Points[dataPoint].YValues[0].ToString();
toolTip1.Show(label, chart1, location);
}
}
private void ResetCursorAndTooltip()
{
chart1.Cursor = Cursors.Default;
toolTip1.Hide(chart1);
}
private Series CreateSeries(string name, Color color, int borderWidth = 1)
{
return new Series(name)
{
ChartType = SeriesChartType.Line,
Color = color,
BorderWidth = borderWidth
};
}
/// <summary>
/// Симуляция потребления топлива
/// </summary>
private void GenerateData()
{
xValues.Clear();
yValues.Clear();
Random random = new Random();
DateTime startTime = DateTime.Now;
for (int i = 0; i <= 120; i++)
{
double baseFuelConsumption = 0.5;
double randomVariation = random.NextDouble() * 0.2;
double totalConsumption = baseFuelConsumption + randomVariation;
double fuelLevel = 100 - (i * totalConsumption);
if (i == 60)
{
fuelLevel += 20;
}
xValues.Add(startTime.AddMinutes(i));
yValues.Add(fuelLevel);
}
fuelLevelSeries.Points.DataBindXY(xValues, yValues);
}
private void ButtonAvg_Click(object sender, EventArgs e)
{
var (xValuesOut, yValuesOut) = DataConverter.NormalizeDataWithXTime(xValues, yValues);
avgFuelLevelSeries.Points.DataBindXY(xValuesOut, yValuesOut);
}
}
Надеюсь вы сможете использовать этот код в вашей практике, который без особого труда может быть представлен в других языках программирования.
Комментарии (12)
adeshere
14.10.2024 05:52При усреднении данных происходит их потеря с обоих концов исходного массива.
С философской точки зрения, это совершенно не обязательно. Например, мы заменяем отсутствующие точки на Nan, а затем работаем с сигналом так, будто он бесконечный. После любой фильтрации длина ряда
остается такой же, как была
Точнее, у нас есть еще одна настройка - это разрешенный процент пропусков в окне. Если она задана равной нулю (т.е. пропуски запрещены), то алгоритм работает по классике: длина ряда уменьшается на половину окна на обоих концах. Фишка в том, что эту настройку можно менять от 0 до 99.9999%, и получать результат, оптимальный для текущей задачи
Идея в том, что обработка пропусков внутри ряда и на его границах идет по совершенно одинаковым алгоритмам
Подробнее
Вот пара статей, где можно об этом почитать (раз, два), а вот тут можно взять их полные тексты. Опенсорсная программа для Винды, в которой эти идеи реализованы, лежит вот тут. Но, там внутри есть не только сглаживание, а еще дохрена всякой другой ерунды, поэтому порог вхождения -
не пожелаешь врагу
это конечно лишь мое маргинальное мнение... но учитывая, что я не только автор проги, но и ее самый активный пользователь (с 30-летним стажем), советую все же прислушаться ;-)
А исходники к этой программе лежат вот тут. Но, WARNING-WARNING-WARNING: там все написано а) на фортране и б) научными сотрудниками, а не программистами. Поэтому слабонервным лучше посмотреть основные идеи (например, там же в архиве WinABD_Help.zip закопана справка к программе, она чуть подробнее, чем упомянутые статьи) и сделать самостоятельно ;-)
А кто знает теорию, тому проще написать самому чем адаптировать чужое.
Именно так ;-)
Простейшим решением является добавление одной или нескольких точек близких к уже вычисленным данным.
Так у Вас будет смещение, если на краю ряда присутствует заметный тренд. Сглаженный ряд загнется к горизонтали (кстати, на Вашем графике "Результат работы алгоритма" оно хорошо видно слева). Чтобы этого избежать, можно
SpiderEkb
14.10.2024 05:52Ваш комментарий навел на мысль. Есть же понятие "выбросов", которые нужно откидывать и "доверительного интервала" (те же "три-сигмы", например).
Т.е., условно говоря так: берем окно, считаем сигму. Дальше откидываем те точки, которые выходят за границы доверительного интервала, по оставшимся считаем среднее.
Но тут надо сразу оговориться, что все это будет работать только в случае линейной зависимости. Для нелинейных искажение линии тренда могут оказаться слишком велики - чем более нелинейно и больше окно, тем больше искажения в сторону спрямления линии тренда. Там лучше будет работать экспоненциальное (двойное, тройное...) сглаживание.
Ну, или если заморочиться, то можно приспособить фильтр Ходрика-Прескотта хотя он не очень прост в реализации.
А в целом, на небольших окнах, выбросы неплохо режутся обычной медианой.
adeshere
14.10.2024 05:52Но тут надо сразу оговориться, что все это будет работать только в случае линейной зависимости.
Так ведь никто не запрещает считать тренд в окне одной ширины, сигму в другом и т.д. Чтобы подобрать должную адаптивность на случай нелинейности тренда (и аналогично в других ситуациях). Даже больше скажу, мы сами обычно именно так и делаем. То есть, настройки каждой процедуры подгоняются под свойства сигнала индивидуально. А итоговый алгоритм строится путем последовательного применения этих всех процедур.
А в целом, на небольших окнах, выбросы неплохо режутся обычной медианой.
Дьявол, как обычно, в соотношении амплитуды тренда и выбросов. Мы поэтому чаще всего работаем по итеративной схеме. Причем, если на некотором шаге мы ищем выбросы, то на этой стадии все остальное, что есть в сигнале,
считается вредной гадостью
Которая мешает эти выбросы идентифицировать максимально качественно.
В ходе "чистки" этой "гадости", в частности, убираются тренды и др. Только после этого каким-либо способом строится собственно детектор выбросов.
А вот дальше ключевой момент: построенный под спойлером детектор выбросов применяется не к сигналу, очищенному от "гадости", а к исходному ряду (который с трендами и т.д.). Итого мы, с одной стороны, создаем максимально комфортные условия для работы детектора выбросов (и тогда не так уж важно, как именно он устроен), а с другой - после выбраковки выбросов получаем ничем не искаженный сигнал. Перефразируя одну известную присказку сомнительного происхождения, в нем убраны только выбросы, все выбросы, и ничего, кроме выбросов ;-)
Чуть подробнее
эта идея описана вот в этой статье (PDF тут).
Но, мне-таки легко давать такие советы, работая с уже лежащими в базе данных рядами. Чтобы прикрутить эти идеи к real-time, наверняка придется изобретать какие-то трюки или от чего-то отказываться...
...можно приспособить фильтр Ходрика-Прескотта
Лично мое мнение - что любые многокомпонентные модели такого рода почти всегда дают худшее качество обработки (декомпозиции), чем комбинация гораздо более простых нативных алгоритмов, применяемых, в идеале, итеративно. Наиболее очевидная причина в том, что входящие в комбинированную модель составляющие обычно не ортогональны. Что сразу же влечет целую кучу багов. Ну и просто реальная погрешность оценки параметров у комбинированной модели почти всегда много хуже. Тут фишка в том, что внутримодельные оценки точности результатов (которые любая правильная прога всегда печатает по окончании расчетов) в 99% случаев завышены,
причем чаще всего многократно
Формальная причина в том, что условия применимости почти любой матмодели не соблюдаются с достаточной строгостью почти никогда. Но что еще важнее, эти погрешности по определению не включают ошибку, связанную с неправильным выбором самой модели. А чем сложнее модель, тем сложнее именно эту ошибку оценить независимо.
Фактически те оценки погрешности, которые дает нам любая модель, надо трактовать, как условные: ЕСЛИ тренд действительно линейный, то погрешность коэффициента "a" вот такая... А если нет? Именно поэтому мы никогда не можем оценить тренд, экстраполировать его вперед и сказать "ошибка прогноза = 0.00000xxx", несмотря на то, что программа экстраполяции линейного тренда вывела на печать ровно "0.00000xxx". Ну или точнее сказать-то можем, только потом будет стыдно ;-) И это будет вовсе не ошибка кривой программы, а сугубо наша ошибка (приняли условную оценку за безусловную).
За подробностями
опять-таки отошлю к разделам I3 и I4 файла WinABD_Help.chm, который лежит вот тут.
При работе с "элементарными" (тривиальными) микро-моделями получить реалистичную оценку погрешности значительно проще. А это, имхо, едва ли не важнее, чем уменьшить эту погрешность. Собственно, я тут только что пытался высказываться на эту тему вот в этом комменте.
С другой стороны, далеко не все задачи требуют "ловли блох", когда затрата суперусилий для улучшения результата на пару десятков процентов будет оправданна. В тех случаях, когда речь не идет о критически важных моментах, наверное лучше (и проще) взять готовенький "черный ящик" и прикрутить его к своей системе, не особо парясь - что там внутри. Но это только Вы можете сами решить: никакой посторонний со стороны тут ничем не поможет...
SpiderEkb
14.10.2024 05:52Мне в свое время приходилось немного заниматься чисткой GPS данных. Там основная проблема в том, что нет никакой линейности и вообще никакой модели. И при этом, чем меньше скорость, те выше уровень шума. Стоя на месте вы вообще не поучите точку - позиция будет постоянно "гулять" ("дрейф позиции"), причем, с достаточно большей амплитудой (до десятков метров порой). А фильтровать надо 5 параметров - 3 координаты, скорость (которая в современных приборах определяется отдельно, по допплеровскому смещению частоты) и направление. Это то, что приходит с прибора в виде непрерывного потока данных.
Много чего перепробовал, но на 100% удовлетворительного решения так и не нашел, особенно в отношении дрейфа - направление и скорость тоже дрейфуют, зацепиться не за что.
Правда, сейчас все это уже в прошлом, так что интерес остался чисто теоретический.
yappari
14.10.2024 05:52Извиняюсь за оффтоп, но
может быть, что-нибудь посоветуете?
Есть два временных ряда (два датчика), просто в виде меток времени (относительных, отсчёт от момента родачи питания). Метки времени сигнализируют о возникновении события, которое регистрируется датчиками. Датчики никак не связаны, имеют свою частоту опроса (частоты, вообще говоря, могут быть не кратны; период может слегка плавать). Соответственно, интервал времени между двумя зарегистрированными событиями у датчиков может отличаться. Некоторые события могут быть зарегистрированными одним датчиком, но пропущены другим (обычно таких немного, но может случиться пропуск нескольких подряд). Количество событий достаточно большое (сотни).
Можете посоветовать способы корреляции таких рядов (хотя бы названия методик или в какую сторону копать)?adeshere
14.10.2024 05:52У нас в институте есть такой А.А.Любушин, у него один из основных коньков - это как раз поиск корреляций между точечными процессами. Очень похоже на Вашу задачу.
Между прочим
его методика анализа микромейсмического шума примерно за год до Тохоку (японское мегаземлетрясение 2011г) показала очень сильные аномалии в районе будущего очага, и он это все опубликовал еще до землетрясения. Фактически, это наполовину прогноз: место и сила будущего события оцениваются достаточно хорошо. Но, к сожалению, для практического использования такой прогноз пока не пригоден, так как время землетрясения прогнозируется на уровне "ожидается в течение года" (такие ограничения у методики).
Так вот, как раз на днях А.А. выложил свою программу для поиска корреляций между точечными потоками событий. Пересылаю его сообщение из
нашей внутриинститутсуой рассылки
Уважаемые коллеги!
Хочу обратить ваше внимание на программу оценки взаимного влияния 2-х точечных процессов. Вот описание метода в форме инструкции пользователя на русском языке:https://alexeylyubushin.narod.ru/Software_for_Monitoring_Systems/LinShares/LinShares_RUS.pdf
а вот сама программа в виде загрузочного модуля (exe-файл):
http://alexeylyubushin.narod.ru/Software_for_Monitoring_Systems/LinShares/LinShares.zip
Эта программа (матрицы влияния) является упрощенным вариантом более общей программы для анализа связей между несколькими сейсмическими последовательностями (1994):
https://alexeylyubushin.narod.ru/LinearModel_InteractionProcesses.pdf
Эта программа дает прекрасный инструмент для анализа связей между каким-то точечным процессом, типа последовательности землетрясений, и характерными временными точками протекания какого-нибудь важного временного ряда, типа локальных максимумов амплитуд огибающих или локальными экстремумами свойств временных рядов систем мониторинга.
Программа была использована в следующих недавних статьях:
1) для анализа связей между локальными экстремумами свойств глобального низкочастотного сейсмического шума и последовательностью землетрясений с магнитудой не ниже 7 (2022):
https://alexeylyubushin.narod.ru/Global_Seimic_Noise_Prop.pdf
2) для анализа связей между точками локальных максимумов отклика свойств сейсмического шума в Японии на неравномерность вращения Земли и последовательностью землетрясений с магнитудой не ниже 6 (2023):
https://alexeylyubushin.narod.ru/Seismic_hazard_indicators_Japan.pdf
3) для анализа связей между локальными экстремумами отклика вейвлет-корреляций флуктуаций глобального магнитного поля (сеть INTERMAGNET) на неравномерность вращения Земли и последовательностью землетрясений с магнитудой не ниже 7 (2024):
https://alexeylyubushin.narod.ru/Wavelet-based_correlations_of_the_global_magnetic_field.pdf
4) для выделения прогностических свойств локальных максимумов мгновенных амплитуд тремора земной поверхности в Японии, вычисляемых с помощью разложения Гильберта-Хуанга (2024):
https://alexeylyubushin.narod.ru/Prognostic_properties_amplitudes_maxima_earth_tremor.pdf
5) для выделения прогностических свойств локальных максимумов квадратичных когерентностей между показаниями 2-х крутильных маятников Кавендиша на базе 3000 км (Москва и Новосибирск) (2024):
https://portal.ifz.ru/journals/ntr/103-1/fulltext/02-NTR-103-1.pdf
Добавлю еще про оценки сейсмической опасности в Японии по свойствам низкочастотного сейсмического шума, представленных в презентации (обновляемой ежемесячно) по адресу:
https://alexeylyubushin.narod.ru/Prognostic_properties_seismic_noise_Japan.pdf
Там использование этой программы представлено на слайдах 12-18, из которых наиболее важен результат на слайде 16, из которого следует, что сейчас Японские острова находятся в стадии повышенной сейсмической опасности.
С уважением, А.А. Любушин.
Не уверен, что там решается в точности Ваша задача, но идеи очень близкие. Посмотрите, наверняка что-нибудь почерпнете. Если возникнут вопросы насчет
прямого использования алгоритмов/программ в каких-то продуктах
то у нас в научной среде обычно это приветствуется при условии ссылки на автора. Ну или если возникнут сомнения, всегда можно напрямую к автору обратиться за разъяснениями. Пишите в личку, я попробую вас связать
P.S.
Извиняюсь за оффтоп, но (...)
Лично мое мнение, что оффтоп - это когда Вы без предупреждения вставляете неудачную обидную шутку, не имеющую прямого отношения к вопросу. За такое тут и правда иногда минусуют... Но чтобы назвать офтопом вопрос по временным рядам в посте про временные ряды?!??
Baton34
14.10.2024 05:52var windowSize = Math.Max(1, baseWindowSize + (int)(stdDev / 10));
здесь точно надо сложить количество точек со средним отклонением данных? А если точек будет 10, а среднее отклонение 500?
randomsimplenumber
Без знания теории код не сильно полезен. Ну, у вас он что-то считает. Будет ли он применим не только для топливных датчиков? Нет ли там чего-то гвоздями прибитого?
А кто знает теорию, тому проще написать самому чем адаптировать чужое.
SpiderEkb
Соглашусь.
Хотелось бы видеть пояснения к алгоритму. Желательно с каким-то матобоснованием.
Тут даже не совсем понятно (не зная C#) - вот мы взяли "стандартное окно", посчитали среднее, посчитали СКО - что дальше? Новое значение СКО будет действовать для следующего окна, или вводится коррекция и пересчет текущего?
Также не понятно будет ли это работать для потокового режима работы - когда у нас еще нет всех данных. Т.е. вот идет поток данных и он "на лету" сглаживается - как тут быть?
В общем, приведет какой-то кусок кода совершенно без объяснения почему именно так.
adeshere
В общем, приведет какой-то кусок кода совершенно без объяснения почему именно так.
Имхо, это главный недостаток очень многих подобных статей. Возможно, в данном случае автор посчитал, что теория тривиальна и общеизвестна. Однако математика - это
такая штука
что заглянув в любой тихий омут, оттуда запросто можно выскочить с криками "тысяча чертей!!!"
что копать можно почти в любом месте, и там откроется бездонная глубина. Я не претендую на особую глубину... но если интересно чуть поподробнее, то кое-какие идеи ляпнул в соседней ветке