Урок курса

LogisticRegression: fit, predict и classes_

Машинное обучение с нуля на Python: первая модель

В прошлом уроке мы разобрались, что метка класса — это дискретное значение 0 или 1, а не непрерывное число. Теперь задача: взять DataFrame с такими метками и обучить на нём классификатор. Интерфейс окажется до боли знакомым — тот же fit, тот же predict, что и у LinearRegression, — но есть несколько деталей, которые важно зафиксировать сразу.

Импорт LogisticRegression и разделение данных на X и y

Импорт выглядит так:

from sklearn.linear_model import LogisticRegression

clf = LogisticRegression()

Объект создан, но он пока не знает ничего о данных: никаких весов, никакой границы решения. Это просто контейнер с настройками по умолчанию, пригодными для числовых признаков.

Теперь — данные. Создадим игрушечный DataFrame df с четырьмя столбцами: distance_km, weight_kg, warehouse_zone и is_delayed. Последний — целевой, содержит 0 и 1.

import pandas as pd

df = pd.DataFrame({
    'distance_km':    [30, 45, 60, 75, 90, 35, 50, 65, 80, 95,
                       40, 55, 70, 85, 100, 25, 110, 120, 130, 140],
    'weight_kg':      [1.0, 1.6, 2.2, 2.8, 3.4, 1.2, 1.8, 2.4, 3.0, 3.6,
                       1.4, 2.0, 2.6, 3.2, 3.8, 0.8, 4.0, 4.5, 5.0, 5.5],
    'warehouse_zone': [1, 2, 3, 1, 2, 3, 1, 2, 3, 1,
                       2, 3, 1, 2, 3, 1, 2, 3, 1, 2],
    'is_delayed':     [1, 0, 0, 1, 0, 0, 1, 0, 1, 0,
                       1, 0, 1, 0, 1, 0, 1, 1, 0, 1]
})

Первым делом отделяем целевой столбец и разбиваем данные на train и test:

from sklearn.model_selection import train_test_split

X_raw = df.drop(columns=['is_delayed'])
y = df['is_delayed']

X_train_raw, X_test_raw, y_train, y_test = train_test_split(
    X_raw, y, test_size=0.2, random_state=42
)

Только после разбиения кодируем warehouse_zone. Это важный порядок действий: если закодировать категориальный признак до разбиения — на полном df, — то набор dummy-столбцов определяется по всем строкам сразу, включая тестовые. В результате кодировщик «знает» о категориях из тестовой части ещё до того, как мы сделали вид, что этих данных не существует. Если в тесте окажется категория, которой нет в трейне, кодировщик всё равно создаст для неё столбец — и это искусственно меняет представление признаков. Чтобы тестовые данные никак не влияли на то, как устроено признаковое пространство, кодировщик нужно обучать только на X_train_raw.

warehouse_zone — категориальный признак: зона 1, зона 2, зона 3. Если оставить его числом, модель воспримет номера зон как количественную упорядоченную шкалу с равными интервалами — то есть будет считать, что между зоной 1 и зоной 2 такое же расстояние, как между зоной 2 и зоной 3, и что зоны вообще можно сравнивать по величине. На деле это просто идентификаторы без какого-либо числового смысла. Используем OneHotEncoder через ColumnTransformer, обучаем только на X_train_raw:

from sklearn.preprocessing import OneHotEncoder
from sklearn.compose import ColumnTransformer

preprocessor = ColumnTransformer(transformers=[
    ('ohe', OneHotEncoder(handle_unknown='ignore', sparse_output=False), ['warehouse_zone'])
], remainder='passthrough')

X_train = preprocessor.fit_transform(X_train_raw)
X_test = preprocessor.transform(X_test_raw)

fit_transform на X_train_raw запоминает, какие категории существуют. transform на X_test_raw применяет ту же зафиксированную схему — без «подглядывания» в тестовые данные.

Параметр handle_unknown='ignore' определяет поведение кодировщика при встрече с незнакомой категорией: вместо ошибки он запишет нули во все dummy-столбцы для этой строки. Важно понимать, что это лишь способ не упасть с ValueError — не более. Ни один обучающий пример не кодировался таким образом, поэтому модель никогда не «видела» подобного вектора при обучении. Прогноз для строки с неизвестной зоной остаётся экстраполяцией: модель что-то вернёт, но это значение нельзя считать надёжным. Если неизвестные зоны реально возможны в боевых данных, правильные варианты — отклонять такие строки или явно помечать прогноз как ненадёжный.

В итоге X_train и X_test — NumPy-массивы с числовыми столбцами, готовые для передачи в clf.fit. y_train и y_test — Series с бинарными метками 0 и 1.

Принципиальный момент, который не меняется по сравнению с регрессией: is_delayed не должен попасть в X. Модель предсказывает y по X, а не ищет y внутри X. Состав и порядок признаков фиксируются здесь — всё, что модель получит при predict, должно в точности соответствовать этой схеме: те же столбцы, тот же порядок, прошедшие через тот же обученный preprocessor.

clf.fit, атрибут classes_ и контракт обучения

Вызов обучения выглядит точно так же, как у LinearRegression:

clf.fit(X_train, y_train)

Контракт тот же: первый аргумент — матрица признаков, второй — вектор целевых значений. До этого вызова классификатор — пустой объект; после — он знает всё, что нужно для предсказания.

Сразу после fit у clf появляется атрибут classes_:

print(clf.classes_)  # array([0, 1])

classes_ — это NumPy-массив уникальных меток из y_train, отсортированных по возрастанию. Если бы y содержал метки {1, 0} в любом порядке, classes_ всё равно был бы array([0, 1]). Сортировка гарантированная и не зависит от того, как метки встречались в данных.

Почему это важно? Во-первых, classes_ позволяет убедиться, что модель действительно увидела оба класса. Во-вторых, порядок в classes_ фиксирует соответствие между индексами и метками — это пригодится в следующем уроке, когда речь пойдёт о вероятностях.

До вызова fit обращение к classes_ вызовет AttributeError: атрибут просто не существует на необученном объекте. Это нормальное поведение sklearn — атрибуты, которые зависят от данных, создаются только во время обучения.

fit требует обоих классов. LogisticRegression не умеет обучаться, если в y_train присутствует только одна метка — вызов завершится ValueError. Поэтому перед обучением стоит проверить состав обоих наборов:

print(y_train.value_counts())
print(y_test.value_counts())

Если один из классов отсутствует в y_train, нужно сначала выяснить причину. Первый шаг — проверить исходный y до разбиения:

print(y.value_counts())

Дальнейшие действия зависят от того, что вы увидите.

Оба класса есть в исходном y, но один не попал в y_train — скорее всего, это неудачное случайное распределение при конкретном random_state. Попробуйте параметр stratify=y в train_test_split: он стремится сохранить пропорции классов в каждой части.

# 1. Заменяем разбиение
X_train_raw, X_test_raw, y_train, y_test = train_test_split(
    X_raw, y, test_size=0.2, random_state=42, stratify=y
)

# 2. Заново обучаем preprocessor на новом X_train_raw
X_train = preprocessor.fit_transform(X_train_raw)
X_test  = preprocessor.transform(X_test_raw)

# 3. Только теперь обучаем классификатор
clf.fit(X_train, y_train)

Но stratify — не универсальное решение. Для его корректной работы каждый класс должен встречаться в y хотя бы в двух экземплярах, иначе train_test_split завершится ValueError. Даже при достаточном числе экземпляров округление при разбиении может оставить редкий класс без представителей в одной из частей. Поэтому после применения stratify всегда проверяйте результат явно:

print(y_train.value_counts())
print(y_test.value_counts())

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

Исходный y содержит только один класс — проблема не в разбиении, а в самих данных: объектов второго класса просто нет. Ни stratify, ни смена параметров разбиения здесь не помогут. Нужны реальные примеры пропущенного класса.

Когда примеров редкого класса действительно мало, стоит рассмотреть сбор дополнительных данных или пересмотр схемы оценки. Например, кросс-валидация позволяет эффективнее использовать ограниченный набор, но не решает проблему автоматически: нужно убедиться, что оба класса представлены в обучающей части каждого фолда. Это выходит за рамки текущего урока, но важно понимать: никакой инструмент не заменяет достаточного числа примеров обоих классов.

clf.predict: дискретные метки и требования к схеме X_new

После обучения получаем предсказания:

y_pred = clf.predict(X_test)
print(y_pred)  # например: array([0, 1, 0, 1])

y_pred — NumPy-массив целых чисел длиной, равной числу строк X_test. При 20 строках и test_size=0.2 в X_test окажется четыре объекта, поэтому и y_pred содержит четыре элемента. Каждый из них — одна из меток из classes_: либо 0, либо 1. Никаких дробей, никакого «0.73». Это жёсткое решение: модель уже выбрала класс за вас.

Типичная ошибка. Иногда после predict пишут что-то вроде y_pred > 0.5, чтобы «применить порог». Это бессмысленно: элементы y_pred уже равны 0 или 1, порог встроен в логику predict. Если нужна вероятность, чтобы двигать порог самостоятельно, это делается через predict_proba — тема следующего урока.

Теперь о схеме столбцов. Когда вы передаёте в predict не X_test, а данные о новой поставке, важно помнить: X_train — это уже преобразованный NumPy-массив с dummy-столбцами для warehouse_zone, а не исходный DataFrame с сырым признаком. Поэтому новую строку нужно пропустить через тот же обученный preprocessor, прежде чем передавать её в clf.predict.

Правильный способ:

import pandas as pd

# Новая строка в сыром виде — те же столбцы, что в X_train_raw.
X_new_raw = pd.DataFrame({
    'distance_km': [80.0],
    'weight_kg': [3.5],
    'warehouse_zone': [2]
})

# Применяем обученный preprocessor — он создаёт те же dummy-столбцы,
# что и при fit. Используем transform, а не fit_transform.
X_new = preprocessor.transform(X_new_raw)

print(clf.predict(X_new))  # например: array([0])

preprocessor.transform (а не fit_transform) применяет схему, зафиксированную при обучении: те же категории warehouse_zone, тот же порядок выходных столбцов. Переобучать preprocessor на новой строке нельзя — fit_transform пересмотрит схему по одной строке и разрушит соответствие с тем, что видел классификатор. Если в X_new_raw передать неизвестное значение warehouse_zone, handle_unknown='ignore' не вызовет ошибку, но прогноз останется ненадёжным: такого вектора модель при обучении не видела.

Попробуйте решить

Первая метка задержки

Служба доставки накопила сведения об отправлениях: расстояние, вес и метку 0/1, показывающую, задержалась ли посылка. Постройте первый классификатор, который даст метку задержки для каждой записи отложенной части данных. Это конкретный ответ модели о доставке, а не вероятность или оценка её качества.

В коде уже есть deliveries с числовыми столбцами distance_km, weight_kg и целевым delayed. Подготовьте признаки X без целевого столбца и метки y. Для воспроизводимости используйте совместное разделение X и y с test_size=0.25, random_state=11 и stratify=y.

Результат должен содержать X_train, X_test, y_train, y_test; обученный только на тренировочной части clf типа LogisticRegression(max_iter=1000); known_classes — известные модели классы; predicted_labels — её метки в порядке строк X_test. Ввод через stdin и вывод через print не нужны.

РешениеPython
Без регистрации · результат не сохраняется

Продолжить с проверкой и прогрессом

Откройте интерактивный раннер с заданиями урока.

Перейти к интерактивному уроку