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

Ваня. Прочитал 5 книг, но больше практиковался. Не имеет высшего образования. Сделал свой проект, который стал успешным. Его купила другая крупная фирма. Зарплата руководителя проекта - 300 т.р.

Алексей. Всю жизнь положил на погружение и подготовку. Имеет 2 высших: инженерное и математическое. Прочитал 50 книг. Но устроился работать простым разработчиком встроенных систем в маленькую фирму. Зарплата - 120 т.р.

Вера. С детства увлекается математикой. Закончила мехмат. Прочитала 10 книг. Но открыла в себе талант руководителя. Чаще ставит задачи, иногда тестирует, иногда программирует. Зарплата - 200 т.р.

Ярослав. Гик и людоман. Обожает книги (150) и игры. Когда-то открыл книгу про Ромеро и Кармака. Работал программистом. Сделал инди-игру. Она не выстрелила. Сделал вторую. Она зашла. Работает на основной работе и делает игры дома. Суммарная зарплата - 160 т.р.

Игорь. У него нормально было с математикой в школе. Поддавшись тренду, поступил в универ на ВМСС. Открыл 1 книгу по программированию, но она оказалась неудачной. Решил, что все книги такие. Увлекся КВН, на 2 курсе остался в академе. Бросил универ. Работает в Макдаке. Зарплата - 40 т.р.

Марина. Имеет разряд по шахматам. Прирожденный хакер. Прочитала множество книг (25) по взлому. Устроилась специалистом по цифровой безопасности в крупный банк. Зарплата - 180 т.р.

Феофан. Закончил физтех. Прочитал 30 книг. Устроился специалистом по data science в банк. Зарплата - 80 т.р.

Попытаемся выяснить, как количество прочитанных книг по программированию влияет на зарплату. В задаче будем считать, что месячная зарплата зависит только от этого фактора, хотя в реальности это не так. Упорядочим данные по количеству прочитанных книг.


Игорь

Ваня

Вера

Марина

Феофан

Алексей

Ярослав

x

1

5

10

25

30

50

150

y

40

300

200

180

80

120

160

Изобразим точки нашей дискретной функции на графике в Octave:

x=[1 5 10 25 30 50 150];
y=[40 300 200 180 80 120 160];
plot(x,y,'+');
grid on;
xlabel('x - books count');
ylabel('y - monthly offer');

Дискретная зависимость ежемесячной зарплаты от количества прочитанных книг
Дискретная зависимость ежемесячной зарплаты от количества прочитанных книг

Линейная регрессия

Мы хотим некое предсказание по "накопленным опытным данным". Какое идеальное количество книг для получения максимальной зарплаты? Построим предсказание в виде линейного графика регрессии. То есть нам надо найти зависимость размера ежемесячной зарплаты y от количества книг в виде y = f(x) = ax + b. Нам надо найти коэффициенты a и b. В соответствии с выкладками для этого нам надо решить систему линейных уравнений:

Полученная система уравнений, которую можно решить по табличным данным, для нахождения a и b
Полученная система уравнений, которую можно решить по табличным данным, для нахождения a и b

Вычислим в Libre Office Calc нужные суммы:
Σx, Σy, Σxy, Σx^2 .

Руками в libre office calc нашли нужные суммы
Руками в libre office calc нашли нужные суммы

Подставляем полученные значения и получаем систему уравнений относительно a и b:

1080 = a*271 + b*7
40440 = a*26651 + b*271

Выражаем из первого уравнения b, подставляем во второе и находим искомые коэффициенты:

a = -0.085
b = 157.576

Выразим коэффициенты a и b из системы уравнений в общем виде:

sum(x*y) = a*sum(x^2) + b*sum(x)
sum(y) = a*sum(x) + b*n

Отсюда получаем формулу для нахождения a и b через суммы:

a = ( sum(y) - b*n ) / sum(x)
b = ( sum(x*y)*sum(x) - sum(y)*sum(x^2) ) / ( (sum(x))^2 - n*sum(x^2) )

Напишем программу для нахождения коэффициентов и соберем ее на Visual Studio Community Edition:

#include <vector>
#include <iostream>

#define mypoints std::vector<double>

void computeLinearRegression(mypoints x, mypoints y, double* a, double* b) 
{
    double sum_x = 0, sum_y = 0, sum_xy = 0, sum_x_squared = 0;
    uint32_t num_points = x.size();

    for (uint32_t i = 0; i < num_points; i++)
    {
        sum_x += x[i];
        sum_y += y[i];
        sum_xy += x[i] * y[i];
        sum_x_squared += x[i] * x[i];
    }

    double n = num_points;

    *b = (sum_xy * sum_x - sum_y * sum_x_squared) / (sum_x * sum_x - n * sum_x_squared);

    *a = (sum_y - *b * n) / sum_x;
}

    

int main() {
    mypoints x = {1,  5,   10,  25,  30, 50,  150};
    mypoints y = {40, 300, 200, 180, 80, 120, 160 };

    double slope, intercept;
    computeLinearRegression(x, y, &slope, &intercept);

    std::cout << "Linear Regression Results: ";
    std::cout << "Number of points: " << x.size() << std::endl;
    std::cout << "Slope (a): " << std::fixed << slope << std::endl;
    std::cout << "Y-Intercept (b): " << intercept << std::endl;
    std::cout << "Equation: y = " << slope << "x + " << intercept << std::endl;

    return 0;
}

После запуска получаем вывод:

Linear Regression Results: Number of points: 7
Slope (a): -0.084869
Y-Intercept (b): 157.571343
Equation: y = -0.084869x + 157.571343

Отобразим на графике с точками полученную линейную функцию y = f(x) = -0.085x + 157.6:

x=[1 5 10 25 30 50 150];
y=[40 300 200 180 80 120 160];
x1=0:0.2:200
y1 = -0.085*x1 + 157.6
plot(x, y, '+', x1, y1);
grid on;
xlabel('x - books count');
ylabel('y - monthly offer');

Линейная регрессия
Линейная регрессия

Мы видим отчетливый тренд на понижение среднемесячной зарплаты, связанный с увеличением прочитанных книг.

Полиномиальная регрессия

Теперь попробуем сделать предсказание в виде степенной зависимости y = f(x) = ax^2 + bx + c. Нам надо найти коэффициенты a, b и c. В соответствии с методом наименьших квадратов после выкладок имеем следующую систему уравнений:

Система из 3 уравнений для нахождения 3 неизвестных
Система из 3 уравнений для нахождения 3 неизвестных

Вычислим в Libre Office Calc следующие суммы:
Σx, Σy, Σxy, Σx^2 y,Σx^2, Σx^3, Σx^4

Слава Libre Office Calc
Слава Libre Office Calc

Получаем систему уравнений с безумными множителями:

1080 = a*26651 + b*271 + c*7
40440 = a*3543751 + b*26651 + c*271
4112040 = a*513711251 + b*3543751 + c*26651

Перепишем систему уравнений в матричном виде и решим ее методом Крамера. Найдем сначала общий определитель, убедимся, что он не равен нулю. Затем вычислим 3 дополнительных определителя, подставляя свободный столбец. Определитель 3 на 3 будем вычислять следующим образом:

Множители, находящиеся по красным диагоналям, входят в выражение со знаком +. Множители по синим диагоналям - со знаком -.
Множители, находящиеся по красным диагоналям, входят в выражение со знаком +. Множители по синим диагоналям - со знаком -.
Вышеперечисленное на практике
Вышеперечисленное на практике

На сайте mathprofi.ru есть xls-файл, в котором автоматизирован расчет определителей и нахождения неизвестных методом Крамера. Можно открыть файл в Libre Office Calc и пересохранить в ods и там посчитать все. В Octave определитель матрицы 3 на 3 можно посчитать при помощи функции det():

A = [26651 271 7;3543751 26651 271;513711251 3543751 26651]
A =

2.6651e+04 2.7100e+02 7.0000e+00
3.5438e+06 2.6651e+04 2.7100e+02
5.1371e+08 3.5438e+06 2.6651e+04

det(A)
ans = -2.4611e+12

Посчитаем определитель вручную:
D = 26651^3 + 513 711 251*271^2 + 7*3 543 751^2 - 513 711 251*26651*7 -
26651*3 543 751*271 - 3543751*271*26651 = - 2 461 126 728 000

Основной определитель не равен нулю. Значит наша система имеет одно решение. Для следующей системы:

Система уравнений 3 на 3 в общем виде
Система уравнений 3 на 3 в общем виде

Для нахождения неизвестных надо найти общий определитель и дополнительные определители. Например, дополнительный определитель для 1-го столбца выглядит так

Дополнительный определитель для 1 столбца
Дополнительный определитель для 1 столбца

D1 = 1080*26651^2 + 4 112 040*271^2 + 7*40440*3 543 751 -
7*26651*4 112 040 - 271*1080*3 543 751 - 271*40440*26651 = -24 134 366 400
D2 = 3 960 549 854 400
D3 = - 441 160 125 408 000

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

Наши неизвестные
Наши неизвестные

a = -24 134 366 400 / (- 2 461 126 728 000) = 0.0098
b = 3 960 549 854 400 / (- 2 461 126 728 000) = -1.6092
c = - 441 160 125 408 000 / (- 2 461 126 728 000) = 179.2513

Напишем программу для нахождения неизвестных коэффициентов методом Крамера.Так как у нас присутствуют числа порядка 10^8 (513711251)(FLT_EPSILON = 2−23 ≈ 1.19e-07 ), надо для вычислений с плавающей запятой использовать тип double (DBL_EPSILON = 2−52 ≈ 2.20e-16).

#include <vector>
#include <iostream>

#define MATR_SIZE 3
#define SUBMATR_SIZE 2

#define mat_ext std::vector<double>


// квадратная матрица 2 степени, которая получается из исходной квадратной матрицы 3 степени 
// вычеркиванием i-го столбца и j-й строки
mat_ext mysubmatr(SUBMATR_SIZE*SUBMATR_SIZE, 0);

#define SUBMATR(i,j) mysubmatr[i*SUBMATR_SIZE + j]


// временная таблица, в которой i-й столбец заменен столбцом свободных членов
mat_ext tempMatrix(MATR_SIZE*MATR_SIZE, 0);

#define TEMPMATR(i,j) tempMatrix[i*MATR_SIZE + j]

// искомые значения a, b, c в виде вектора
mat_ext solutions(MATR_SIZE, 0);


void makeSubmatrix(mat_ext matr, uint8_t row, uint8_t col)
{
// опредение макроса для удобства работы с линейным вектором в виде матрицы
#define MATR(r,c) matr[(r)*MATR_SIZE + c]

    // сдвиг номера строки подматрицы относительно старой
    int di = 0;

    // сдвиг номера колонки подматрицы относительно старой
    int dj = 0;

    // цикл относительно новой подматрицы
    for (int i = 0; i < SUBMATR_SIZE; i++)
    {
        if (row == i)
            di = 1;

        dj = 0;
        for (int j = 0; j < SUBMATR_SIZE; j++)
        {
            if (j == col)
                dj = 1;

            SUBMATR(i, j) = MATR(i+di, j+dj);
        }
    }
}

// рекурсивное вычисление определителя
double computeDeterminantVector(mat_ext matr, uint8_t n)
{
// опредение макроса для удобства работы с линейным вектором в виде матрицы
#define LOCALMATR(i,j) matr[(i)*n + j]

    if (n == 1)
        return LOCALMATR(0,0);

    if (n == 2)
        return LOCALMATR(0,0) * LOCALMATR(1,1) - LOCALMATR(0,1) * LOCALMATR(1,0);

    double det = 0;
    int sign = 1;

    uint8_t target_row = 0;

    // цикл вычисления детерминанта степени 3 в виде разложения на сумму детерминантов 2 степени
    // раскадываем по первой (нулевой) строке
    for (int j = 0; j < MATR_SIZE; j++)
    {
        // формируем submatrix 
        makeSubmatrix(matr, target_row, j);

        det += sign * LOCALMATR(0,j) * computeDeterminantVector(mysubmatr, n - 1);
        sign = -sign;
    }

    return det;
}

void makeTempMatr(mat_ext mymatr, mat_ext myconst, uint8_t col)
{   
    // опредение макроса для удобства работы с линейным вектором в виде матрицы
#define MYMATR(i,j) mymatr[(i)*MATR_SIZE + j]

    // Copy original matrix
    for (int i = 0; i < MATR_SIZE; i++)
        for (int j = 0; j < MATR_SIZE; j++)
            if (j == col)
                // заменяем i-й столбец свободными членами 
                TEMPMATR(i, col) = myconst[i];
            else
                // другие стобцы просто присваиванием
                TEMPMATR(i, j) = MYMATR(i, j);
}

void solveUsingCramersRuleVector(mat_ext mymatr, mat_ext myconst, uint8_t n)
{
    double mainDeterminant = computeDeterminantVector(mymatr, n);

    // если детерминант равен нулю, тогда > 1 решений 
    if (mainDeterminant == 0)
    {
        std::cout << "The system has no unique solution (determinant is zero).\n";
        return;
    }

    // вычисляем детерминанты
    for (uint8_t i = 0; i < n; i++)
    {
        makeTempMatr(mymatr, myconst, i);

        // неизвестная
        solutions[i] = computeDeterminantVector(tempMatrix, n) / mainDeterminant;
    }

    // вектор найденных неизвестных
    std::cout << "\nSolutions:\n";
    for (int i = 0; i < n; i++) 
    {
        printf("x%d = %.2f\n", i + 1, solutions[i]);
    }
}

int main() 
{
    uint8_t n = MATR_SIZE;

    // 26651.0,     271.0,     7.0,     1080.0,
    // 3543751.0,   26651.0,   271.0,   40440.0,
    // 513711251.0, 3543751.0, 26651.0, 4112040.0

    mat_ext mymatr =  { 26651.0,     271.0,     7.0,
                        3543751.0,   26651.0,   271.0,
                        513711251.0, 3543751.0, 26651.0};

    mat_ext myconst = { 1080.0,
                        40440.0, 
                        4112040.0 };

    solveUsingCramersRuleVector(mymatr, myconst, n);

    return 0;
}

Получаем вывод:
Solutions:
x1 = 0.0098
x2 = -1.6092
x3 = 179.2513

Отобразим график функции
y = f(x) = 0.0098*x^2 - 1.6092*x + 179.2513

Код для octave:

x=[1 5 10 25 30 50 150];
y=[40 300 200 180 80 120 160];
x1=0:0.2:200;
y1 = 0.0098*x1.^2 - 1.6092*x1 + 179.2513;
plot(x, y, '+', x1, y1);
grid on;
xlabel('x - books count');
ylabel('y - monthly offer');
y = f(x) = 0.0098x^2 - 1.6092x + 179.2513

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

Комментарии (6)


  1. Rio
    03.01.2026 16:35

    Ну вот, обещали на Си, а код — на плюсах, да с набором антипаттернов, типа макросов где ни попадя. И зачем писать #define mypoints std::vector<double>, когда специально для такого есть typedef или using?


    1. adil21 Автор
      03.01.2026 16:35

      Да, от плана до окончания статьи прошло некоторое время. Сначала планировал на C, потом решил использовать вектор вместо обычных массивов


  1. el_mago
    03.01.2026 16:35

    Если пишите на С++, то чем не устроила eigen?


    1. adil21 Автор
      03.01.2026 16:35

      Спасибо, не знал про eigen


      1. pvvv
        03.01.2026 16:35

        alglib уж тогда


  1. vityo
    03.01.2026 16:35

    Кому интересно, гляньте ещё one euro filter (для "на лету")