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

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

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

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

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

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

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

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

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

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

  /// 
  /// Функция для нормализации данных (сглаживания), возвращающая сглаженные данные и соответствующие значения X.
  /// Вычисляет стандартное отклонение и динамически определяет размер окна для скользящего среднего.
  /// 
  /// Исходные значения X как DateTime
  /// Исходные значения Y
  /// Достраивать концевые точки данных
  /// Кортеж со сглаженными значениями X и Y
  public static (List smoothedX, List smoothedY) NormalizeDataWithXTime(List xValues, List 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);
  }

  /// 
  /// Функция для скользящего среднего, также возвращающая соответствующие значения X.
  /// 
  /// Исходные значения X как DateTime
  /// Исходные значения Y для сглаживания
  /// Размер "окна"
  /// Кортеж со значениями X и Y для сглаженной линии
  public static (List smoothedX, List smoothedY) MovingAverageWithXTime(List xValues, List yValues,
      int windowSize = 3)
  {
      var smoothedY = new List();
      var smoothedX = new List();

      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);
  }

  /// 
  /// Функция для расчета стандартного отклонения.
  /// 
  /// Эта функция вычисляет стандартное отклонение, которое измеряет, насколько значения в наборе данных отклоняются от среднего.
  /// Более высокое стандартное отклонение указывает на более разбросанные данные, тогда как более низкое стандартное отклонение указывает на данные, которые более сконцентрированы вокруг среднего.
  /// 
  /// Исходные данные
  /// Значение стандартного отклонения
  public static double CalculateStandardDeviationWithXTime(List 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 xValues = [];
    private readonly List 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
        };
    }

    /// 
    /// Симуляция потребления топлива
    /// 
    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);
    }         
}

?utm_source=habrahabr&utm_medium=rss&utm

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

© Habrahabr.ru