Трансформация данных – все, что вам нужно
В обширной сфере машинного обучения и науки о данных сложные алгоритмы и модели часто оказываются в центре внимания. Однако иногда суть успешной прогностической модели заключается не только в выборе правильного алгоритма, но и в представлении данных таким образом, чтобы алгоритм мог извлечь из них максимальную пользу.
В этой небольшой статье, родившейся в результате моего промедления, рассматривается линейная регрессионная модель в свете возможностей визуализации и преобразования данных. Моя цель – показать, как простое изменение в представлении данных может улучшить работу модели.
Задание: Основы линейной регрессии
Чтобы проиллюстрировать этот тезис, рассмотрим набор данных, отражающий связь между телевизионной рекламой и продажами. Начнем исследование с загрузки данных и визуализации взаимосвязи.
import numpy as np
import pandas as pd
from sklearn.model_selection import train_test_split
# jupyter labextension install plotlywidget
pd.options.plotting.backend = "plotly"
ADS_DATA_RI = "https://raw.githubusercontent.com/justmarkham/scikit-learn-videos/master/data/Advertising.csv"
dataf = pd.read_csv(ADS_DATA_RI, index_col=0)
# split my data to test the performance of the model
X, y = dataf[["TV"]], dataf["Sales"]
X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=.8, random_state=7)
(dataf
.assign(split=lambda d: np.where(d.index.isin(X_train.index), "train", "test"))
.plot(kind="scatter",
x="TV",
y="Sales",
color="split",
trendline="ols",
trendline_scope="overall",
trendline_color_override="darkblue",
opacity=.7,
title="Sales ~ TV Ads")
)
Диаграмма рассеяния показывает не совсем линейную зависимость между телерекламой и продажами, и наша модель линейной регрессии не совсем точно отражает динамику продаж при меньших затратах на телерекламу.
Обратимся к уравнению линейной модели с помощью scikit-learn:
from numpy.typing import NDArray
from sklearn.linear_model import LinearRegression
from sklearn import metrics
lm = LinearRegression(fit_intercept=True)
lm.fit(X_train, y_train)
def get_equation(features:NDArray[str], coef:NDArray[float], intercept:float, y:str="Sales", ) -> str:
equation = " + " .join([*[f"{coef:.5f} * {feature}"
for feature, coef in zip(features, coef)],
f"{intercept:.5f}"])
return f"{y} ~ {equation}"
y_func = get_equation(features=lm.feature_names_in_, coef=lm.coef_, intercept=lm.intercept_)
print(y_func)
# 'Sales ~ 0.04727 * TV + 7.17408'
А показатель R-квадрат, дающий представление о том, насколько хорошо наша модель объясняет изменчивость данных об откликах вокруг своего среднего значения, составляет:
y_pred = lm.predict(X_test)
R2 = f"{metrics.r2_score(y_true=y_test, y_pred=y_pred):.7f}"
print(R2)
# '0.6779489'
Однако при построении графика остатков, представляющего собой разность между фактическими и прогнозными значениями, мы наблюдаем закономерность:
...
(pd
.DataFrame(y_test - y_pred)
.rename(columns={"Sales": "Resuduals"})
.assign(TV = X_test,
Position = X_test.index)
.plot(kind="scatter",
y="Resuduals",
x="Position",
title=f"True Sales - Predicted Sales: {R2=} ",
**fig_size
)
.add_hline(y=0)
.update_traces(mode="markers", hovertemplate=None)
.update_layout(hovermode="y unified")
)
Такая картина в остатках говорит о том, что наша линейная модель не идеальна. Остатки не должны иметь никакой закономерности, если наша модель точно отражает структуру исходных данных.
Bend It Like Beckham (Наклонись, как Бекхэм)
Визуализация показывает, что связь между телевизионной рекламой и продажами не является строго линейной, а изначально имеет обратно-экспоненциальный характер. Это говорит о том, что первые примерно $1-$50, потраченные на рекламу, могут принести более медленную отдачу в продажах, прежде чем набрать обороты.
Простой способ отразить эту потенциальную взаимосвязь – преобразовать входные (ТВ-реклама) и выходные данные (продажи) с помощью логарифма.
# Data Transformation
...
(dataf
.assign(split=lambda d: np.where(d.index.isin(X_train.index), "train", "test"))
.plot(kind="scatter",
x="TV",
y="Sales",
color="split",
trendline="ols",
trendline_options=dict(log_x=True, log_y=True), # power of transformation
trendline_scope="overall",
trendline_color_override="darkblue",
opacity=.7,
title="log(Sales) ~ log(TV) Ads + c",
**fig_size)
)
Наша преобразованная диаграмма рассеяния показывает более линейную зависимость между телерекламой и продажами, преобразованными в логарифмы. Это явный признак того, что наша первоначальная гипотеза о логарифмической зависимости может оказаться верной.
Преобразованная линейная регрессия: Лучшее решение?
Воспользуемся логтрансформированными данными и подгоним линейную регрессионную модель:
from sklearn.compose import TransformedTargetRegressor
from sklearn.preprocessing import FunctionTransformer
from sklearn.pipeline import make_pipeline
def inverse_log10(X:float)->float:
return 10 ** X
transformer = FunctionTransformer(func=np.log10, inverse_func=inverse_log10)
linear_model = TransformedTargetRegressor(regressor=LinearRegression(), func=np.log10, inverse_func=inverse_log10)
lmx = make_pipeline(transformer, linear_model)
lmx.fit(X_train, y_train)
R-квадрат для этой новой модели составляет:
y_pred = lmx.predict(X_test)
R2 = f"{metrics.r2_score(y_true=y_test, y_pred=y_pred):.7f}"
print(R2)
# 0.6962380
Этот показатель выше, чем у нашей исходной модели (0,67). Это говорит о том, что наша преобразованная модель объясняет большую изменчивость данных о продажах.
Визуализируя нашу преобразованную модель вместе с фактическими данными, мы получаем:
(dataf
.assign(split=lambda d: np.where(d.index.isin(X_train.index), "train", "test"))
.plot(kind="scatter",
x="TV",
y="Sales",
color="split",
opacity=.7,
title="log10(Sales) ~ log10(TV) + c",
**fig_size)
).add_traces(
(dataf
.assign(pred = lmx.predict(dataf[["TV"]]))
.sort_values(by=["TV"])
.plot(kind="line",
x="TV",
y="pred",
**fig_size
).update_traces(line_color="darkblue")
).data)
Следующий шаг: Байесовская модель линейной регрессии
Мы заметили, что по мере увеличения размера ТВ-рекламы значения все больше отклоняются от нашей подогнанной линии. Использование байесовской линейной регрессии было бы лучшим подходом, поскольку мы получили бы не одну линию, а распределение линий и, таким образом, по-разному отразили бы неопределенность для разных ТВ-реклам.
Вывод: 🍔
Главный вывод из этой статьи заключается в том, что успех прогностической модели не всегда заключается в выборе более сложного алгоритма. Прежде чем обратиться к более сложной модели, сделайте шаг назад и подумайте, не может ли преобразование данных упростить ситуацию. Помните, что иногда преобразование данных – это все, что вам нужно.
А пока продолжайте кодировать данные в упрощенном режиме.