Функция скользящего среднего для регенерации на графике

от автора

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

Типичными данными, которые нужно нормализовывать являются данные топливных датчиков.

Ниже я привожу код для C# который можно copy/paste для вашего использования.

Особенностями подхода является два момента:

  1. Наличие функция для расчета стандартного отклонения. Эта функция вычисляет стандартное отклонение, которое измеряет, насколько значения в наборе данных отклоняются от среднего.

    Более высокое стандартное отклонение указывает на более разбросанные данные, тогда как более низкое стандартное отклонение указывает на данные, которые более сконцентрированы вокруг среднего.

    Зачем это нужно?

    Для того, чтобы не заморачиваться с выбором «окна» усреднения и, поэтому, расчет окна усреднения на основе стандартного отклонения, выполняется динамически.

  2. При усреднении данных происходит их потеря с обоих концов исходного массива. Простейшим решением является добавление одной или нескольких точек близких к уже вычисленным данным. В моем случае, я добавляю по одной точке с обоих сторон массива для компенсации линии тренда, но вы можете проделать самостоятельную работу и использовать среднее количество точек на интервале «окна» с тем, чтобы генерировать их и дополнить массив.

  /// <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);     }          }
Результат работы алгоритма

Результат работы алгоритма

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


ссылка на оригинал статьи https://habr.com/ru/articles/850352/


Комментарии

Добавить комментарий

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