В современном мире анализа данных регрессионный анализ занимает центральное место, предоставляя мощные инструменты для выявления и количественной оценки взаимосвязей между переменными. Он позволяет исследователям и аналитикам не только описывать существующие зависимости, но и прогнозировать поведение систем на основе имеющихся данных. Одним из наиболее распространенных методов регрессионного анализа является метод наименьших квадратов, который стремится минимизировать сумму квадратов отклонений между наблюдаемыми и предсказанными значениями.
При использовании метода наименьших квадратов можно выделить два основных подхода к аппроксимации: однофакторную и двухфакторную. Однофакторная аппроксимация рассматривает влияние одной независимой переменной на зависимую, что позволяет получить простую и интуитивно понятную модель. Однако в реальных сценариях, когда возникает необходимость учитывать влияние нескольких факторов одновременно, на помощь приходит двухфакторная аппроксимация. Она позволяет более полно отразить сложные взаимосвязи и взаимодействия между переменными.
Небольшая теория аппроксимации функции методом наименьших квадратов
Есть выборка данных зависимости значений  от 
0  | 
0  | 
1  | 
1  | 
2  | 
3  | 
5  | 
10  | 
Мы знаем, что, например,  при равным 1, 
 будет равен 1, но нам нужно узнать значения между узловыми точками или узнать значение 
 при 
 равным 10. В таком случае используют регрессионный анализ. Один из вариантов получения функции по заданным точкам это аппроксимация. В конечном итоге должна быть построена функция 
  , значения которой при 
 будут примерно равны значениям 
. 
0  | 
0  | 
≈0  | 
1  | 
1  | 
≈0  | 
2  | 
3  | 
≈0  | 
5  | 
10  | 
≈0  | 
Воспользуемся методом наименьших квадратов, чтобы составить условие, при котором сумма квадратов ошибок между требуемыми значениями и результатом аппроксимируемой функции стремилась к нулю.
Т.е. в результате должны быть найдены коэффициенты функции    , чтобы разница между точками 
 и значениями функции    
.
Для функции аппроксимирования я использую числовой ряд:
На моей практике максимальную повторяемость реальной функции даст n=4. Для простоты следующего примера я буду использовать функцию  n = 1 (линейную функцию )
Возьмем линейную функцию  , тогда аппроксимируемая функция будет выглядеть
Далее нужно найти коэффициенты a1, a2. Для этого составим систему уравнений из частных производных по данной функции.

Возьмем производные и получим следующую систему уравнений:

Выразим из второго уравнения   :
Тогда
Получившиеся формулы можно использовать для линейной регрессии, когда нужно провести усредняющую линию через всю выборку.
Подставим значения из выборки и найдем коэффициенты:
В конечном итоге мы построили регрессионную функцию, которая проходит через наши точки с минимально возможным отклонением. Обычно линейную функцию используют для определения тренда выборки, т.е. мы знаем, что с ростом значений , 
 будет возрастать в 2.07 раза. Для более “точного повторения” используют функции 4 или 5 степени.
0  | 
0  | 
0  | 
1  | 
1  | 
1.43  | 
2  | 
3  | 
3.5  | 
5  | 
10  | 
9.71  | 
Реализация на платформе .NET
В конечном итоге аппроксимирования методом наименьших квадратов все сводится к построению системы уравнения частных производных функции суммы ошибок между действительными значениями и значениями аппроксимируемой функции.
Для этого понадобится библиотека MathNet.Symbolics, которую можно установить через диспетчера пакетов NuGet . Данная библиотека будет вычислять частные производные, а также упрощать выражения.
Вычислять систему уравнений будем с помощью метода Гаусса https://en.wikipedia.org/wiki/Gaussian_elimination , реализацию можно найти в интернете или в моей библиотеке Regression в папке MathService.

static void Main()
     {
         //Символьное выражение членов полинома a1 * x + a2 
         var a1 = SymbolicExpression.Variable("a1");
         var a2 = SymbolicExpression.Variable("a2");
         //Аппроксимируемая выборка значений пара X Y
         Dictionary<double, double> XtoY = new()
         {
             { 0, 0 }, { 1, 1 }, { 2, 3 }, { 5, 10 }
         };
         //Построение функции F(x) 
         SymbolicExpression Fx = 0;
         foreach (var item in XtoY)
         {
             var expr = item.Value + (a1 * item.Key + a2);
             Fx += expr * expr;
         }
         //Частные производные в строковом виде
         var diffA1 = Fx.Differentiate(a1).RationalSimplify(a1).ToString().Trim().Split();
         var diffA2 = Fx.Differentiate(a2).RationalSimplify(a2).ToString().Trim().Split();
         List<string[]> polysList = new() { diffA1, diffA2 };
         // Step 1 Построение матрицы Y
         double[,] Y = new double[2, 1];
         for (int i = 0; i < Y.GetUpperBound(0) + 1; i++)
         {
             var yValues = GetArrayParseY(polysList[i]).ToArray();
             for (int j = 0; j < Y.GetUpperBound(1) + 1; j++)
             {
                 Y[i, 0] += yValues[j];
             }
         }
         // Step 2 Построение матрицы X
         double[,] X = new double[2, 2];
         for (int i = 0; i < X.GetUpperBound(0) + 1; i++)
         {
             var xValues = GetArrayParseX(polysList[i]).ToArray();
             for (int j = 0; j < X.GetUpperBound(1) + 1; j++)
             {
                 X[i, j] += xValues[j];
             }
         }
         // Step 3 Решение системы уравнений
         Gaus gaus = new();
         double[] result = gaus.Roots(X, Y);
     }
/// <summary>
 /// Вернуть из строк массив X значений
 /// </summary>
 /// <param name="strings"></param>
 /// <returns></returns>
 private static IEnumerable<double> GetArrayParseX(string[] strings)
 {
     var a = strings.Where(x => !x.Equals("+"));
     var listAr = new double[2];
     int sign = 1;
     foreach (var stringExpression in a)
     {
         if (stringExpression.Equals("-"))
         {
             sign = -1;
             continue;
         }
         if (!stringExpression.Contains("*a")) continue;
         var str = stringExpression.Remove(0, stringExpression!.IndexOf("a", StringComparison.Ordinal)+1);
         var index = int.Parse(str);
         var st1r = stringExpression.Substring(0, stringExpression!.IndexOf("*", StringComparison.Ordinal));
         listAr[index-1] = sign * double.Parse(st1r, CultureInfo.InvariantCulture);
         sign = 1;
     }
     return listAr;
 }
 /// <summary>
 /// Вернуть из строки массив Y значений
 /// </summary>
 /// <param name="strings"></param>
 /// <returns></returns>
 private static IEnumerable<double> GetArrayParseY(string[] strings)
 {
     var a = strings.Where(x => !x.Equals("+"));
     var sign = 1;
     foreach (var stringExpression in a)
     {
         if (stringExpression.Equals("-"))
         {
             sign = -1;
             continue;
         }
         if (stringExpression.Contains("*a")) continue;
         yield return sign * double.Parse(stringExpression, CultureInfo.InvariantCulture);
         sign = 1;
     }
 }
В результате выполнения кода
SymbolicExpression Fx = 0;
         foreach (var item in XtoY)
         {
             var expr = item.Value + (a1 * item.Key + a2);
             Fx += expr * expr;
         }
функция  будет равна
Далее частные производные функции по  - 
 и 
 - 
 вернут значения
Остается составить матрицу для решения системы уравнений методом Гаусса вида 
Матрица A
Столбец свободных коэффициентов B
Столбец переменных
Для составления матриц из массива строк были применены методы GetArrayParseX и GetArrayParseY
В результате получаем массив коэффициентов полинома:
result[0] = 2.07
result[1] = -0.64
Таким образом можно реализовывать многофакторную регрессию, когда на входе больше, чем одно значение X. Сделать это можно перемножением полиномов друг на друга:
Возьмем полином третьей степени вида:
Тогда двухфакторная аппроксимируемая функция будет выглядеть как:
Практическое применение
Рассмотрим инженерную проблему. При разработке датчика давления необходимо было построить функцию с помощью аппроксимирования, где входные данные — это код АЦП, а выходные — это давление. Выяснилось, что код АЦП давления изменяется от температуры, т.е. на одном и том же давлении код АЦП разный, следовательно для аппроксимирования необходимо было ввести еще один фактор – код температуры.
Давление на эталоне:

Код давления и температуры при 16 градусах:


Код давления и температуры при 18 градусах


Код давления и температуры при 21 градусе


Код давления и температуры при 23 градусах


Код давления и температуры при 27 градусах


Объединим все наши коллекции:



Функция аппроксимирования полиномом третьей степени будет выглядеть:


Решим систему уравнений частных производных силами MathCad:


Приложение RegressionFromExcel
Пользоваться MathCad было не очень удобно:
· Время расчета коэффициентов могло достигать до 1 минуты
· Вносить и забирать данные в MathCad было проблематично, оператор мог ошибиться, копируя данные
Необходимо было приложение, которое бы считывало данные из листа Excel и в определенные ячейки сохраняло результаты коэффициентов. Так же сохраняло json файл с коэффициентами для определенных задач.
Так как излишний функционал с сохранением файлов востребован только на производстве, я решил создать открытый проект без данных функций сохранений.
RegressionFromExcel/README.md at main · Georgiy-smr/RegressionFromExcel
В папке Exemple лежит файл TestData, в котором уже есть тестовые данные, а также посчитанные коэффициенты. Для расчета нужно заполнить столбцы с данными и выбрать степень полинома в приложении 2 или 3. Далее приложение откроет файл, считает данные и вычислит коэффициенты, которые вставит в отдельные ячейки справа (как в примере TestData).

Более подробно о работе приложения будет описано во второй части.
Сравним результаты вычислений MathCad и RegressionFromExcel.


Мы видим достаточную сходимость погрешностей на уровне 2% от допустимой погрешности на диапазон преобразователя давления.
Использованная литература:
Кобзарь А. И. Прикладная математическая статистика. — М.: Физматлит, 2006.
Комментарии (3)

leremin
02.02.2025 08:54Ну все таки выбирать степень полинома от балды - неправильно. Нужно опираться на физическую модель. Например, коды ацп температурного датчика скорее всего линейно зависят от температуры - вводя бОльшие порядки, вы просто дефекты конкретного датчика будете описывать.

ALexKud
02.02.2025 08:54На практике все немного не так с реальным датчиком давления.. Нужен технологический запас по погрешности, как по основой так и по температурной. Поэтому обычно используют пять точек давления и пять точек температур, включая крайние точки диапазона давления и рабочего диапазона температур. Метод Гаусса не дает высокой точности при обратном расчёте.. Метод Гивенса для расчета коэффициентов предпочтительнее. Данные лучше хранить в базе данных. В моей статье на хабре (в профиле) я описал свою спроектированную рабочую систему для датчиков давления, где используется LabVIEW для сбора данны и расчета коэффициентов и SQL Server для хранения данных и формирования матриц для расчёта. Ехсеll для экспериментов. Производство требует более основательного подхода. Я бы в статье с примером датчика давления упомянул физическую суть двухфакторной аппроксимации. Сенсор давления невозможно линеаризовать двумя отдельными полиномами, Основной полином по давлению должен иметь коэффициенты-полиномы по температуре. Математика математикой, но проще подходить к вопросу с точки зрения физической сущности линеаризации датчика давления. Расчет коэффициентов полинома сводится к решению СЛАУ и для пяти точек это 25 yравнений.
          
 
win32nipuh
Отличная статья.
Там в одном месте "NUuGet"->"NuGet"