Подготовка признаков
Многие модели чувствительны к тому, в каких числах записаны признаки. Метод ближайших соседей считает расстояние: если доход — сотни тысяч рублей, а возраст — десятки лет, разница в доходе на 1000 рублей «весит» больше, чем разница в 50 лет. А строковые признаки (город, марка машины) модели вообще не принимают — их надо превратить в числа.
Стандартизация: StandardScaler
Для каждого признака вычитаем среднее и делим на стандартное отклонение. После этого у каждого признака среднее 0 и разброс 1 — признаки становятся сравнимыми.
from sklearn.preprocessing import StandardScaler
X = [[150, 50000], [170, 80000], [190, 110000]] # рост, доход
scaler = StandardScaler()
scaler.fit(X) # запоминает среднее и отклонение каждого столбца
print(scaler.mean_) # [ 170. 80000.]
print(scaler.transform(X)) # [[-1.22 -1.22] [0 0] [1.22 1.22]]
fit_transform(X) — то же, что fit(X), а затем transform(X).
Отклонение считается по формуле с делением на n (как numpy.std по умолчанию). Если столбец постоянный, отклонение нулевое — тогда StandardScaler делит на 1, и столбец превращается в нули.
Нормировка: MinMaxScaler
Другой способ — перевести каждый признак в отрезок от 0 до 1: минимум станет 0, максимум — 1.
from sklearn.preprocessing import MinMaxScaler
print(MinMaxScaler().fit_transform([[0], [5], [10]])) # [[0. ] [0.5] [1. ]]
Утечка данных
Масштабирование — тоже «обучение»: оно запоминает средние и отклонения. Поэтому правило то же, что для модели: fit — только на обучающей выборке, а тестовую выборку и новые данные только transform:
scaler = StandardScaler().fit(X_train)
X_train_s = scaler.transform(X_train)
X_test_s = scaler.transform(X_test) # те же среднее и отклонение, что у обучающей
Если обучить масштабирование на всех данных до разбиения, сведения о тестовой выборке «протекут» в обучение, и оценка качества окажется нечестной. И наоборот: если забыть преобразовать тестовую выборку, модель получит данные совсем в другом масштабе.
Категориальные признаки: OneHotEncoder
Строковый признак кодируют так: для каждого значения заводят свой столбец из нулей и единиц.
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder(handle_unknown="ignore")
enc.fit([["red"], ["green"], ["red"]])
print(enc.categories_) # [array(['green', 'red'], ...)]
print(enc.transform([["red"], ["blue"]]).toarray()) # [[0. 1.] [0. 0.]]
fitпринимает таблицу — даже для одного признака это список списков;- значения в
categories_отсортированы; transformвозвращает разреженную матрицу,.toarray()превращает её в обычный массив;- без
handle_unknown="ignore"незнакомое значение вызывает ошибку, с ним — даёт строку из нулей.
Почему не пронумеровать города числами 1, 2, 3? Тогда модель решит, что «город 3» втрое «больше» города 1, а это бессмыслица.
В pandas похожее делает pd.get_dummies(df["city"]), но он не запоминает список значений обучающей выборки, поэтому для моделей удобнее OneHotEncoder.
Конвейер: Pipeline
Чтобы не следить вручную, где fit, а где transform, шаги объединяют в конвейер:
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
model = make_pipeline(StandardScaler(), LogisticRegression())
model.fit(X_train, y_train) # обучает масштабирование и модель на X_train
print(model.score(X_test, y_test)) # масштабирует X_test теми же параметрами и считает accuracy
Конвейер ведёт себя как одна модель: у него есть fit, predict, score. Внутри fit каждый шаг, кроме последнего, делает fit_transform, а последний — fit. При predict все шаги делают только transform. Утечка данных исключена.
Если разные столбцы готовятся по-разному (числа — масштабировать, строки — кодировать), используют ColumnTransformer — он тоже кладётся в конвейер.
Итог
StandardScaler— среднее 0, отклонение 1;MinMaxScaler— отрезок [0, 1];fit— только на обучающей выборке,transform— на всех;OneHotEncoder(handle_unknown="ignore")— строковый признак → столбцы из нулей и единиц;make_pipeline(шаг1, …, модель)— подготовка и модель как одно целое.