Shanraq.org Shanraq.org
Первая модель: линейная регрессия
IT

Python: от данных до своей сводки Урок 44 из 56

Первая модель: линейная регрессия

Сорок третий урок курса по Python. Прямая, проведённая через шестнадцать лет цен, даёт R² 0.927 и проигрывает правилу «как в прошлом году» — на сотую долю пункта. Ошибка у неё не случайная, а с формой: плюс, минус, плюс. Прямая по логарифму ошибается вдвое меньше, потому что цены умножаются.

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

Связь из прошлого урока говорит «эти два столбца двигались вместе». Модель говорит больше: на сколько и что будет при таком-то значении. Первая и самая честная из них — прямая.

Цена за это — ошибка, которую теперь можно назвать числом. Урок не столько про то, как обучить модель (две строки), сколько про то, как посмотреть на её промахи и не обмануться красивым R².

Сразу целиком

Файл model.py. Ряд настоящий: индекс потребительских цен Казахстана с 2010 года, Всемирный банк. Библиотека новая — scikit-learn, pip install scikit-learn==1.9.1.

"""Урок 43: первая модель — прямая через цены.

Ряд настоящий: индекс потребительских цен Казахстана, 2010 = 100, показатель
FP.CPI.TOTL Всемирного банка. Библиотека — scikit-learn.
"""

import numpy as np
import pandas as pd
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, r2_score

CPI = pd.Series({
    2010: 100.00, 2011: 108.45, 2012: 114.09, 2013: 120.87, 2014: 129.15,
    2015: 137.77, 2016: 157.56, 2017: 169.28, 2018: 179.72, 2019: 189.30,
    2020: 202.02, 2021: 218.27, 2022: 251.07, 2023: 287.54, 2024: 312.53,
    2025: 348.12,
})

# sklearn ждёт таблицу признаков: строка — наблюдение, столбец — признак.
# Здесь признак один, год, поэтому столбец один — но столбец, а не ряд.
X = CPI.index.to_numpy().reshape(-1, 1)
y = CPI.to_numpy()

print("== Прямая через шестнадцать лет")
line = LinearRegression().fit(X, y)
print("  за год индекс прибавляет:", round(float(line.coef_[0]), 2), "пункта")
print("  R²:", round(r2_score(y, line.predict(X)), 3))
print("  средняя ошибка:", round(mean_absolute_error(y, line.predict(X)), 2), "пункта")

print()
print("== Ошибка, а не только совпадение")
table = pd.DataFrame({"индекс": CPI, "прямая": line.predict(X).round(1)})
table["ошибка"] = (table["индекс"] - table["прямая"]).round(1)
print(table.to_string())
worst = table["ошибка"].abs().idxmax()
print("  худший год:", worst, "— промах", round(float(table.loc[worst, "ошибка"]), 1), "пункта")

print()
print("== У ошибки есть форма")
signs = "".join("+" if e > 0 else "-" for e in table["ошибка"])
print("  знаки ошибок по годам:", signs)
print("  групп подряд идущих знаков:", 1 + sum(1 for a, b in zip(signs, signs[1:]) if a != b))
print("  у случайной ошибки их было бы около", round(len(signs) / 2 + 1))

print()
print("== Прямая в логарифмах")
log_line = LinearRegression().fit(X, np.log(y))
growth = float(np.exp(log_line.coef_[0]) - 1)
predicted = np.exp(log_line.predict(X))
print("  за год цены умножаются на:", round(float(np.exp(log_line.coef_[0])), 4),
      "— это", round(growth * 100, 2), "% в год")
print("  средняя ошибка:", round(mean_absolute_error(y, predicted), 2), "пункта")
print("  худший промах:", round(float(np.max(np.abs(y - predicted))), 2), "пункта")

print()
print("== Предсказание и его цена")
next_year = np.array([[2026]])
print("  прямая обещает на 2026 год:", round(float(line.predict(next_year)[0]), 1))
print("  прямая в логарифмах:", round(float(np.exp(log_line.predict(next_year)[0])), 1))
print("  разница между обещаниями:",
      round(float(np.exp(log_line.predict(next_year)[0]) - line.predict(next_year)[0]), 1), "пункта")

Вывод:

== Прямая через шестнадцать лет
  за год индекс прибавляет: 15.45 пункта
  R²: 0.927
  средняя ошибка: 17.17 пункта

== Ошибка, а не только совпадение
      индекс  прямая  ошибка
2010  100.00    73.2    26.8
2011  108.45    88.7    19.8
2012  114.09   104.1    10.0
2013  120.87   119.6     1.3
2014  129.15   135.0    -5.8
2015  137.77   150.5   -12.7
2016  157.56   165.9    -8.3
2017  169.28   181.4   -12.1
2018  179.72   196.8   -17.1
2019  189.30   212.3   -23.0
2020  202.02   227.7   -25.7
2021  218.27   243.2   -24.9
2022  251.07   258.6    -7.5
2023  287.54   274.1    13.4
2024  312.53   289.5    23.0
2025  348.12   305.0    43.1
  худший год: 2025 — промах 43.1 пункта

== У ошибки есть форма
  знаки ошибок по годам: ++++---------+++
  групп подряд идущих знаков: 3
  у случайной ошибки их было бы около 9

== Прямая в логарифмах
  за год цены умножаются на: 1.0849 — это 8.49 % в год
  средняя ошибка: 7.39 пункта
  худший промах: 24.09 пункта

== Предсказание и его цена
  прямая обещает на 2026 год: 320.4
  прямая в логарифмах: 351.5
  разница между обещаниями: 31.1 пункта

Разбор

Модель — это две строки

model = LinearRegression().fit(X, y)
model.predict(X)

fit подбирает прямую, predict спрашивает её. Всё остальное в уроке — про то, как понять, годится ли она.

Одна тонкость с формой данных: X должен быть таблицей признаков — строка на наблюдение, столбец на признак, — поэтому reshape(-1, 1). Признак здесь один, год, но столбец всё равно нужен: sklearn не угадывает, один у вас признак или сто.

R² красив и почти ничего не значит

  R²: 0.927
  средняя ошибка: 17.17 пункта

R² — доля разброса, которую модель объяснила: 0.927 звучит как «модель права на 93 %». На деле рядом с ним стоит средняя ошибка в 17 пунктов при индексе, который начинался со ста.

Причина простая: R² сравнивает модель с базовым прогнозом на уровне среднего. При R² = 0 модель равна этой базе, а отрицательный R² означает, что она хуже базы; это не «самая слабая возможная» модель. Обыграть её легко у любого ряда, который растёт. Поэтому R² почти никогда не бывает ответом на вопрос «хороша ли модель».

У ошибки есть форма

  знаки ошибок по годам: ++++---------+++
  групп подряд идущих знаков: 3
  у случайной ошибки их было бы около 9

Это полезная эвристика, а не окончательный диагноз. Если остатки независимы, их знаки обычно чаще чередуются; ожидаемое число серий зависит от количества положительных и отрицательных остатков. Для формального вывода нужен тест серий и график остатков, а «примерно половина точек» верно лишь как грубый ориентир при сбалансированных знаках. Здесь групп три: модель сначала систематически завышает, потом столько же лет занижает, потом опять завышает.

Так выглядит не шум, а недостающая часть модели. Прямая не может согнуться, а ряд гнётся.

Цены умножаются — значит, прямая нужна в логарифмах

Тридцать восьмой урок: проценты не складываются, а перемножаются. Ряд, который каждый год умножается на примерно одинаковое число, — это не прямая, это экспонента. Логарифм превращает умножение в сложение, и в логарифмах та же самая прямая становится правильной формой:

  за год цены умножаются на: 1.0849 — это 8.49 % в год
  средняя ошибка: 7.39 пункта

Ошибка упала больше чем вдвое, а коэффициент стал читаемым: 8.49 % в год — это та самая среднегодовая ставка из тридцать восьмого урока, только полученная моделью.

Заметьте, что и она ошибается: 7.39 пункта в среднем и 24 в худший год. Ряд, в котором был 2022-й, не описывается одной постоянной ставкой — и это честный предел обеих моделей, а не повод искать третью.

Предсказание — это два числа, а не одно

  прямая обещает на 2026 год: 320.4
  прямая в логарифмах: 351.5

Тридцать один пункт разницы — это выбор формы модели, а не данных: ряд один и тот же. Отсюда правило, которое стоит усвоить раньше любых формул: предсказание без указания модели и её ошибки — это мнение, и цифра после запятой ему солидности не добавляет.

Чего этот счёт не умеет

Обе модели мерились на тех же годах, на которых учились. Так меряют только для того, чтобы увидеть форму ошибки; для вопроса «а как она справится с новым годом» это не годится совсем — модель уже видела ответы. Разделить ряд честно — следующий урок.

Карта урока

Карта урока: точки, прямая и ошибка

Скажите своими словами

Ответьте вслух или на бумаге, не подглядывая. Ответы — в конце урока.

  1. Зачем X нужно превращать в таблицу с одним столбцом?
  2. Почему R² 0.927 не значит, что модель хороша?
  3. Что означают три группы знаков у ошибки вместо девяти?

Разминка

Три коротких шага перед заданием: предсказать, дополнить, починить. Ответы — в конце урока, но сначала ответьте сами.

1. Предскажите. Что напечатает программа, если точки лежат на идеальной прямой?

import numpy as np
from sklearn.linear_model import LinearRegression

x = np.array([[1], [2], [3], [4]])
y = np.array([3.0, 5.0, 7.0, 9.0])
model = LinearRegression().fit(x, y)
print("наклон:", round(float(model.coef_[0]), 3))
print("сдвиг:", round(float(model.intercept_), 3))
print("для 10:", round(float(model.predict(np.array([[10]]))[0]), 3))

2. Заполните пропуск. Вместо ... приведите годы к форме, которую ждёт sklearn.

# sklearn ждёт таблицу признаков, а не ряд
import numpy as np
from sklearn.linear_model import LinearRegression

years = np.array([2020, 2021, 2022, 2023])
index = np.array([100.0, 108.0, 117.0, 126.0])
X = ...
model = LinearRegression().fit(X, index)
print("за год прибавляет:", round(float(model.coef_[0]), 2))

3. Почините. Программа меряет ошибку средней разностью и объявляет модель безупречной.

# ошибки складываются со своими знаками, и у прямой они гасят друг друга
import numpy as np

index = np.array([100.0, 108.0, 117.0, 126.0])
guess = np.array([99.0, 109.0, 118.0, 125.0])
print("средняя ошибка:", round(float((index - guess).mean()), 2))

Задание

Обязательное. Постройте прямую по годам для индекса цен и напечатайте по годам: индекс, предсказание и ошибку. Сообщите наклон, R² и худший год с размером промаха. Отдельно посчитайте знаки ошибок и число групп подряд идущих знаков — рядом с тем, сколько групп было бы у случайной ошибки. В конце сравните три способа предсказать год на одних и тех же годах: прямую, прямую в логарифмах и правило «как в прошлом году», — и назовите лучший.

Ожидаемый вывод:

== Прямая по годам
      индекс  прямая  ошибка
2010  100.00    73.2    26.8
2011  108.45    88.7    19.8
2012  114.09   104.1    10.0
2013  120.87   119.6     1.3
2014  129.15   135.0    -5.8
2015  137.77   150.5   -12.7
2016  157.56   165.9    -8.3
2017  169.28   181.4   -12.1
2018  179.72   196.8   -17.1
2019  189.30   212.3   -23.0
2020  202.02   227.7   -25.7
2021  218.27   243.2   -24.9
2022  251.07   258.6    -7.5
2023  287.54   274.1    13.4
2024  312.53   289.5    23.0
2025  348.12   305.0    43.1
  за год прибавляет: 15.45 пункта
  R²: 0.927
  худший год: 2025 — промах 43.1 пункта

== Форма ошибки
  знаки: ++++---------+++
  групп: 3 при 9 у случайной ошибки

== Сравнение на одних и тех же годах
прямая                 16.53
прямая в логарифмах     7.58
как в прошлом году     16.54
  лет в сравнении: 15
  лучшая: прямая в логарифмах
  прямая обыгрывает «ничего не делать» на: 0.01 пункта

Готово — это: вывод совпадает построчно; сравнение идёт по одним и тем же годам, а не по всем, какие есть у каждой модели; ошибка меряется по модулю; лучший способ выбирается кодом, а не глазами.

На своих данных. Возьмите любой свой ряд по времени — расходы по месяцам, вес, километры, счета. Постройте прямую, посмотрите на знаки ошибок и ответьте себе на один вопрос: они чередуются или идут полосами? Если полосами, прямая не та форма, и стоит попробовать логарифм.

По желанию.

  • Обучите прямую только на 2010–2019 годах и посмотрите, что она обещает на 2022-й. Это репетиция следующего урока.
  • Добавьте второй признак — например, квадрат года — и посмотрите, как падает ошибка и что происходит с формой знаков.
  • Сравните LinearRegression с numpy.polyfit(x, y, 1): коэффициенты обязаны совпасть, и это хороший способ убедиться, что модель делает ровно то, что вы думаете.

Куда это встанет в проекте

Шаг двадцать второй: в сводке появляется первая модель.

esep.trend строит прямую по логарифму индекса каждой страны и печатает два числа: годовой множитель, который эта прямая подразумевает, и худший промах в пунктах индекса. Второе стоит рядом с первым не для красоты — тренд без ошибки это утверждение без цены, а страны описываются по-разному: Казахстан 1.12 в год при промахе 3.56, Узбекистан 1.10 при промахе 1.02.

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

В requirements.txt проекта добавился scikit-learn с закреплённой версией: модель, у которой не записана версия библиотеки, — это модель, которую никто не повторит.

Ответы

Показать ответы

На вопросы

  1. Потому что sklearn принимает таблицу «наблюдения × признаки» и не угадывает, что вы имели в виду рядом из шестнадцати чисел: один признак с шестнадцатью наблюдениями или шестнадцать признаков с одним. reshape(-1, 1) говорит это явно.
  2. Потому что R² сравнивает модель с горизонтальной прямой на уровне среднего, а не с чем-то разумным. У растущего ряда её обыграет что угодно. Рядом с R² 0.927 здесь стоит средняя ошибка в 17 пунктов и проигрыш правилу «как в прошлом году».
  3. Что ошибка не случайна: модель систематически завышает на одном участке и занижает на другом. У случайной ошибки знак менялся бы часто — групп было бы около половины от числа точек. Три группы означают, что форма модели не та.

К разминке

  1. Точки лежат на прямой y = 2x + 1, поэтому наклон ровно 2, сдвиг ровно 1, а для десятки модель даёт 21.
наклон: 2.0
сдвиг: 1.0
для 10: 21.0
  1. years.reshape(-1, 1). Минус единица означает «это измерение вычисляется само», а единица — «один столбец».
# sklearn ждёт таблицу признаков, а не ряд: строка — наблюдение, столбец — признак
import numpy as np
from sklearn.linear_model import LinearRegression

years = np.array([2020, 2021, 2022, 2023])
index = np.array([100.0, 108.0, 117.0, 126.0])
X = years.reshape(-1, 1)
model = LinearRegression().fit(X, index)
print("за год прибавляет:", round(float(model.coef_[0]), 2))
за год прибавляет: 8.7
  1. Ошибки надо брать по модулю. Средняя разность у прямой равна нулю по построению — она не «хорошая модель», она вообще не мера качества.
# ошибку меряют по модулю: у прямой плюсы и минусы гасят друг друга
import numpy as np

index = np.array([100.0, 108.0, 117.0, 126.0])
guess = np.array([99.0, 109.0, 118.0, 125.0])
errors = index - guess
print("средняя по модулю:", round(float(np.abs(errors).mean()), 2))
print("а простое среднее:", round(float(errors.mean()), 2))
средняя по модулю: 1.0
а простое среднее: 0.0

К заданию

Главное число задания — последнее. Прямая с R² 0.927 обыгрывает правило «повторить прошлый год» на одну сотую пункта, то есть не обыгрывает вовсе. Это и есть цена, которую стоит платить за каждую модель: сравнение не с нулём, а с самым простым, что можно сделать без модели.

Прямая в логарифмах выигрывает по-настоящему — 7.58 против 16.5, — и выигрывает не потому, что сложнее, а потому, что у неё правильная форма. Модель, подобранная под форму данных, почти всегда обходит модель, подобранную под желание получить число.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

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

Пока нет комментариев. Будьте первым.