Фрея · риск-менеджер необанка «Монетка»
Мне нужен прогноз трат каждого клиента на второй квартал: под него планируем лимиты и бюджет кешбэка. И скажи, от чего траты зависят. Говорят, клиенты с кредиткой тратят больше. Может, раздать кредитки всем?
В уроке про корреляцию и регрессию мы проводили прямую через облако точек по одному признаку. В прошлом уроке научились честно проверять модель: обучение, тест, базовая модель. Сегодня соединяем: прямая по нескольким признакам сразу, проверка на тесте и чтение коэффициентов, на которых аналитики спотыкаются чаще всего.
Данные: в clients_spend.csv по строке на каждого из 2000 клиентов «Монетки». Всё, кроме ответа, известно на 1 апреля 2026: сегмент, город, возраст, card_months (сколько месяцев прошло с открытия первой карты), has_credit (была ли кредитка), q1_tx и q1_rub (число и сумма одобренных операций за январь-март). Ответ q2_rub: сколько клиент потратил с апреля по июнь. Утечки нет: все признаки посчитаны до начала второго квартала.
Одна прямая
Начнём с того, что уже умеем. Чем больше клиент тратил в первом квартале, тем больше потратит во втором:
import pandas as pd
from scipy import stats
clients = pd.read_csv("clients_spend.csv")
stats.linregress(clients["q1_rub"], clients["q2_rub"])
# slope=0.636, intercept=10639, rvalue=0.561
Каждый рубль первого квартала добавляет к прогнозу 64 копейки. Теперь то же в scikit-learn, с разбиением из прошлого урока:
import numpy as np
from sklearn.linear_model import LinearRegression
from sklearn.metrics import mean_absolute_error, root_mean_squared_error, r2_score
from sklearn.model_selection import train_test_split
y = clients["q2_rub"]
X_train, X_test, y_train, y_test = train_test_split(
clients[["q1_rub"]], y, test_size=0.25, random_state=42)
model = LinearRegression().fit(X_train, y_train)
r2_score(y_test, model.predict(X_test)) # 0.242
mean_absolute_error(y_test, np.full(len(y_test), y_train.mean())) # 17237
Базовая модель «всем среднее» ошибается на 17 237 ₽. Прямая по тратам объясняет 24% разброса на тесте.
А если взять не рубли, а число операций q1_tx? R² на тесте 0,396. Сумма в рублях шумит: одобренная покупка в магазине техники стоит в среднем 15 821 ₽ и раздувает квартал, хотя клиент не стал тратить чаще. Число операций честнее показывает, насколько клиент живёт с картой.
Несколько признаков
Одна прямая не знает, что премиальные клиенты при том же числе операций платят больше. Добавим сегмент. Модель теперь выглядит так:
прогноз = b0 + b1 * q1_tx + b2 * segment_premium
Это и есть линейная регрессия: каждый признак умножаем на свой коэффициент и складываем. Модель подбирает коэффициенты так же, как linregress подбирал наклон: чтобы квадраты промахов на обучении были как можно меньше.
X = pd.get_dummies(clients[["q1_tx", "segment"]], drop_first=True, dtype=int)
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.25, random_state=42)
model = LinearRegression().fit(X_train, y_train)
dict(zip(X.columns, model.coef_)), model.intercept_
# ({'q1_tx': 1525.6, 'segment_premium': 16901.3}, -606.2)
Читается так. Если у двух клиентов одинаковый сегмент, а у одного на одну операцию в первом квартале больше, модель даст ему на 1526 ₽ больше трат во втором. Если у двух клиентов одинаковое число операций, а один премиальный, ему модель даст на 16 901 ₽ больше. Слова «при прочих равных» здесь главные: коэффициент описывает разницу между клиентами, которые совпадают по всем остальным признакам модели.
Категории в числа
Модель умножает признаки на числа, а segment это текст. pd.get_dummies делает из него столбцы из нулей и единиц, по одному на значение. Это one-hot кодирование:
| segment | segment_mass | segment_premium |
|---|---|---|
| mass | 1 | 0 |
| premium | 0 | 1 |
Столбцы segment_mass и segment_premium всегда в сумме дают 1, значит один из них лишний: он целиком выражается через другой и свободный член. drop_first=True выбрасывает первый. Отброшенное значение, здесь mass, становится точкой отсчёта, а коэффициент segment_premium это разница с ней. Без drop_first прогноз останется тем же, но коэффициенты потеряют смысл: модель сможет перекладывать рубли между свободным членом и обоими столбцами как угодно.
У города 14 значений, и get_dummies превратит его в 13 столбцов. В scikit-learn то же делает OneHotEncoder: он запоминает категории обучающей выборки, и тест кодируется теми же столбцами, даже если какой-то город в нём не встретился.
урок из курса
Урок из курса «Аналитика данных с нуля». Курс покупается один раз и остаётся навсегда: 500 ₽.
Оплата пока не подключена: премиум и курсы открываются промокодом.