Логистическая регрессия в Python собирается из трёх объектов scikit-learn: StandardScaler приводит признаки к одному масштабу, LogisticRegression учится на обучающей части выборки, а метод predict_proba возвращает вероятность класса для каждого нового объекта. Весь рабочий код занимает около десяти строк.
Ниже пример на встроенном датасете с реальным выводом интерпретатора: обучение, метрики, коэффициенты и отношение шансов, порог классификации, регуляризация и ошибки, на которых новички теряют больше всего времени. Если с регрессией вы пока не встречались, начните с разбора линейной регрессии и метода наименьших квадратов: логистическая модель устроена похоже.
Как работает логистическая регрессия: сигмоида и логиты

Логистическая регрессия решает задачу классификации: письмо спам или нет, клиент уйдёт или останется, опухоль злокачественная или доброкачественная. Модель отвечает вероятностью от 0 до 1, а класс получается сравнением этой вероятности с порогом.
Внутри всё начинается как в обычной регрессии: модель складывает признаки с весами и получает число z.
z = b0 + b1·x1 + b2·x2 + … + bn·xn
p = 1 / (1 + e^(−z))
Число z называют логитом. Он может быть любым, от минус до плюс бесконечности. Сигмоида (логистическая функция) сжимает его в вероятность p. Обратная операция выглядит так: логит равен ln(p / (1 − p)), то есть логарифму шансов.
Вот как сигмоида переводит разные значения z в вероятность:
import numpy as np
def sigmoid(z):
return 1 / (1 + np.exp(-z))
for z in (-4, -1, 0, 1, 4):
print(z, round(sigmoid(z), 3))
-4 0.018
-1 0.269
0 0.5
1 0.731
4 0.982
При z = 0 вероятность ровно 0,5. Чем дальше логит от нуля, тем увереннее модель.
Чем логистическая регрессия отличается от линейной
| Что сравниваем | Линейная регрессия | Логистическая регрессия |
|---|---|---|
| Задача | Предсказать число: цену, спрос, время | Предсказать класс и его вероятность |
| Выход модели | Любое число | Вероятность от 0 до 1 |
| Как подбираются веса | Метод наименьших квадратов | Максимум правдоподобия, функция потерь log loss |
| Класс в scikit-learn | LinearRegression |
LogisticRegression |
| Главная метрика | R², MAE, RMSE | ROC AUC, precision, recall, log loss |
Название сбивает с толку: «регрессия» здесь означает регрессию для логита, а итоговая задача остаётся классификацией. Где логистическая модель стоит среди деревьев, бустинга и нейросетей, разобрано в обзоре моделей машинного обучения.
Одна деталь из документации scikit-learn важна с первой строки кода: класс LogisticRegression по умолчанию применяет регуляризацию. В справке сказано прямо: «regularization is applied by default». К этому вернёмся в разделе про параметр C.
Логистическая регрессия на Python в scikit-learn: пошагово
Код ниже запускали на Python 3.14.7, scikit-learn 1.9.1, pandas 3.1.1 и NumPy 2.5.4. Удобнее всего проходить его в Jupyter Notebook: каждый шаг в отдельной ячейке.
Шаг 1: установить библиотеки
pip install scikit-learn pandas
Лучше ставить пакеты в виртуальное окружение, чтобы версии не конфликтовали с другими проектами.
Шаг 2: загрузить данные и разделить выборку
Возьмём датасет breast cancer, который входит в scikit-learn и скачивается вместе с библиотекой. В нём 569 опухолей, 30 числовых признаков по снимкам клеток и два класса. Что вообще считается датасетом и как его собрать под свою задачу, рассказали отдельно.
from sklearn.datasets import load_breast_cancer
from sklearn.model_selection import train_test_split
data = load_breast_cancer(as_frame=True)
X, y = data.data, data.target
print(X.shape)
print(y.value_counts())
X_train, X_test, y_train, y_test = train_test_split(
X, y, test_size=0.25, stratify=y, random_state=42
)
print(len(X_train), len(X_test))
(569, 30)
target
1 357
0 212
Name: count, dtype: int64
426 143
Обратите внимание на кодировку классов: в этом датасете 0 означает злокачественную опухоль (malignant), а 1 доброкачественную (benign). Модель будет считать вероятность класса 1, то есть доброкачественной.
Параметр stratify=y сохраняет долю классов в обеих частях. Без него в тестовую выборку на 143 объекта может случайно попасть заметно больше или меньше злокачественных случаев.
Шаг 3: масштабировать признаки и обучить модель
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)
print(model.predict(X_test[:5]))
print(model.predict_proba(X_test[:5]).round(3))
print(round(model.score(X_test, y_test), 3))
[1 0 1 1 0]
[[0.031 0.969]
[1. 0. ]
[0.441 0.559]
[0.061 0.939]
[0.825 0.175]]
0.986
make_pipeline склеивает масштабирование и модель в один объект. Среднее и разброс признаков StandardScaler запоминает только по обучающей части, а к тестовой применяет те же числа. Почему признаки вообще нужно приводить к одному масштабу, объясняет статья про нормализацию данных.
Шаг 4: прочитать вероятности
В выводе predict_proba два столбца, по одному на класс, в порядке model.classes_, здесь это [0 1]. Третья опухоль получила 0,559 для класса 1: модель отнесла её к доброкачественным, но почти наугад.
Чтобы взять только вероятность класса 1, пишут predict_proba(X_test)[:, 1]. Это двумерный срез: все строки, второй столбец.
score показывает долю верных ответов на тесте: 0,986, или 141 из 143. Одной этой цифры для решения мало, поэтому дальше смотрим метрики по классам.
Как оценить качество модели
Accuracy прячет главное: на каком классе модель ошибается. Матрица ошибок и отчёт по классам показывают это сразу.
from sklearn.metrics import confusion_matrix, classification_report, roc_auc_score
y_pred = model.predict(X_test)
proba = model.predict_proba(X_test)[:, 1]
print(confusion_matrix(y_test, y_pred))
print(classification_report(y_test, y_pred, target_names=data.target_names, digits=3))
print("ROC AUC:", round(roc_auc_score(y_test, proba), 4))
[[52 1]
[ 1 89]]
precision recall f1-score support
malignant 0.981 0.981 0.981 53
benign 0.989 0.989 0.989 90
accuracy 0.986 143
macro avg 0.985 0.985 0.985 143
weighted avg 0.986 0.986 0.986 143
ROC AUC: 0.9977
В первой строке матрицы 53 злокачественных опухоли: 52 модель нашла, одну пропустила. Во второй строке 90 доброкачественных, одну из них модель приняла за злокачественную.
| Метрика | Что показывает | Когда смотреть |
|---|---|---|
| Precision | Какая доля объектов, отнесённых к классу, действительно из него | Ложная тревога дорогая: блокировка карты, лишний звонок клиенту |
| Recall | Какую долю объектов класса модель нашла | Пропуск дорогой: болезнь, мошенничество, брак |
| F1 | Гармоническое среднее precision и recall | Нужна одна цифра для сравнения моделей |
| ROC AUC | Насколько хорошо модель ранжирует объекты по вероятности, без привязки к порогу | Сравнение моделей до выбора порога |
Кросс-валидация вместо одного разбиения
Одна тестовая выборка на 143 объекта может оказаться удачной или неудачной. cross_val_score пять раз обучает модель на разных частях обучающих данных и проверяет на оставшейся:
from sklearn.model_selection import cross_val_score
scores = cross_val_score(model, X_train, y_train, cv=5, scoring="roc_auc")
print(scores.round(3), round(scores.mean(), 3))
[0.993 0.999 0.979 0.997 0.998] 0.993
Разброс от 0,979 до 0,999 показывает, что результат почти не зависит от того, как разбили данные.
Калибровочная кривая
Если вероятности пойдут в расчёт денег или риска, проверьте калибровку: у хорошо откалиброванной модели среди объектов с прогнозом около 0,8 примерно 80 % действительно относятся к классу 1. Кривую строит calibration_curve, а строки вывода удобно собрать через f-строки:
import numpy as np
from sklearn.calibration import calibration_curve
frac_pos, mean_pred = calibration_curve(y_test, proba, n_bins=5)
counts = np.histogram(proba, bins=5, range=(0, 1))[0]
print("объектов в корзинах:", counts)
for p, f in zip(mean_pred, frac_pos):
print(f"предсказано {p:.2f} → на деле {f:.2f}")
объектов в корзинах: [49 4 4 1 85]
предсказано 0.02 → на деле 0.00
предсказано 0.33 → на деле 0.25
предсказано 0.58 → на деле 1.00
предсказано 0.70 → на деле 1.00
предсказано 0.98 → на деле 0.99
Крайние корзины, где 49 и 85 объектов, совпадают с прогнозом. В средних по одному–четыре объекта, и строка «0.58 → 1.00» посчитана всего по четырём опухолям. Калибровку проверяют на сотнях объектов в каждой корзине, на маленькой выборке кривая скачет.
Как читать коэффициенты и отношение шансов
Коэффициенты лежат в атрибуте coef_ последнего шага пайплайна. Экспонента коэффициента даёт отношение шансов (odds ratio): во сколько раз меняются шансы класса 1, если признак вырастет на единицу.
import numpy as np
import pandas as pd
clf = model[-1]
coefs = pd.Series(clf.coef_[0], index=X.columns)
top = coefs.reindex(coefs.abs().sort_values(ascending=False).index)[:5]
print(pd.DataFrame({"coef": top.round(3), "odds_ratio": np.exp(top).round(3)}))
print("intercept:", clf.intercept_.round(3))
coef odds_ratio
worst texture -1.250 0.286
radius error -1.070 0.343
worst symmetry -0.957 0.384
worst concave points -0.942 0.390
area error -0.941 0.390
intercept: [0.281]
Признаки прошли через StandardScaler, поэтому «единица» здесь равна одному стандартному отклонению. Рост worst texture на одно отклонение умножает шансы доброкачественной опухоли на 0,286, то есть снижает их примерно в 3,5 раза. Минус у всех пяти коэффициентов логичен: модель считает вероятность класса 1, а высокие значения этих признаков чаще встречаются у злокачественных опухолей.
Сравнивать коэффициенты по модулю можно только после масштабирования. На сырых данных признак в тысячах (площадь) и признак в долях (гладкость) получат несопоставимые веса.
P-значения и доверительные интервалы через statsmodels
scikit-learn не считает p-значения. Если нужен статистический отчёт, с которым работают в эконометрике и медицинской статистике, используйте Logit из statsmodels (ставится командой pip install statsmodels, мы запускали версию 0.15.0). Свободный член библиотека сама не добавляет, его подключает sm.add_constant:
import numpy as np
import statsmodels.api as sm
cols = ["mean radius", "mean texture"]
X_sm = sm.add_constant(X_train[cols])
result = sm.Logit(y_train, X_sm).fit(disp=0)
print(result.summary2().tables[1].round(3))
print(np.exp(result.params[cols]).round(3))
Coef. Std.Err. z P>|z| [0.025 0.975]
const 19.317 2.004 9.638 0.0 15.389 23.245
mean radius -1.060 0.118 -8.993 0.0 -1.290 -0.829
mean texture -0.193 0.040 -4.796 0.0 -0.271 -0.114
mean radius 0.347
mean texture 0.825
dtype: float64
Здесь признаки не масштабированы, и коэффициент читается в исходных единицах датасета: каждая единица mean radius умножает шансы доброкачественной опухоли на 0,347. Ноль в столбце P>|z| получился из-за округления до трёх знаков, само значение меньше 0,0005.
Логика чтения p-значения та же, что в A/B-тестах: оно показывает, насколько вероятно получить такой или ещё более далёкий от нуля коэффициент, если признак на самом деле ни на что не влияет.
statsmodels по умолчанию обучает модель без регуляризации, а scikit-learn с ней. Поэтому коэффициенты двух библиотек на одних данных совпадать не обязаны.
Настройка модели: порог, регуляризация и веса классов

Порог классификации
predict относит объект к классу 1, если вероятность больше 0,5. Для медицины и антифрода такой порог часто плохой: пропуск дороже ложной тревоги. Порог можно сдвинуть вручную через predict_proba. Посчитаем, как меняется поиск злокачественных опухолей:
from sklearn.metrics import recall_score, precision_score
p_malignant = model.predict_proba(X_test)[:, 0]
y_true = (y_test == 0)
for t in (0.5, 0.3, 0.1):
y_hat = p_malignant >= t
print(t, "recall:", round(recall_score(y_true, y_hat), 3),
"precision:", round(precision_score(y_true, y_hat), 3))
0.5 recall: 0.981 precision: 0.981
0.3 recall: 0.981 precision: 0.897
0.1 recall: 1.0 precision: 0.828
При пороге 0,1 модель находит все 53 злокачественные опухоли, но поднимает 11 ложных тревог вместо одной. Порог 0,3 здесь ничего не даёт: пропуск остаётся тот же, а ложных тревог становится шесть.
Решение: выбирайте порог по цене ошибок в вашей задаче на отдельной валидационной выборке. Тестовую оставьте для финальной проверки.
Регуляризация: параметр C и l1_ratio
Параметр C задаёт обратную силу регуляризации. Чем меньше C, тем сильнее штраф за большие коэффициенты. Значение C=np.inf выключает штраф полностью.
В scikit-learn 1.8 параметр penalty объявили устаревшим, его уберут в версии 1.10. Вместо penalty="l1" теперь пишут l1_ratio=1, вместо penalty="l2" пишут l1_ratio=0 (это значение по умолчанию), промежуточные значения дают elastic net. Старый код пока работает, но на 1.9.1 выдаёт FutureWarning:
FutureWarning: 'penalty' was deprecated in version 1.8 and will be removed in 1.10.
L1-штраф обнуляет часть коэффициентов, то есть сам отбирает признаки. Проверим на разных C:
for C in (0.01, 0.1, 1, 10):
m = make_pipeline(StandardScaler(),
LogisticRegression(C=C, l1_ratio=1, solver="liblinear", random_state=0))
m.fit(X_train, y_train)
nonzero = np.count_nonzero(m[-1].coef_)
print(C, "признаков в модели:", nonzero, "точность:", round(m.score(X_test, y_test), 3))
0.01 признаков в модели: 4 точность: 0.909
0.1 признаков в модели: 7 точность: 0.972
1 признаков в модели: 16 точность: 0.993
10 признаков в модели: 22 точность: 0.958
При C=0.01 из 30 признаков остаются 4, и модель недообучается. При C=10 в модели 22 признака, а точность на тесте падает. Подбирать C лучше через GridSearchCV или LogisticRegressionCV на кросс-валидации.
random_state=0 в коде нужен из-за liblinear: этот решатель перемешивает данные, и без фиксированного зерна у нас при повторных запусках при C=0.1 в модели оставалось то 8, то 7 признаков.
Не каждый решатель умеет каждый штраф. По руководству scikit-learn:
| solver | Какой l1_ratio поддерживает | Многоклассовая задача |
|---|---|---|
| lbfgs (по умолчанию) | только 0 (L2) | да |
| liblinear | 0 или 1 | нет |
| newton-cg, newton-cholesky, sag | только 0 (L2) | да |
| saga | от 0 до 1, включая elastic net | да |
Несбалансированные классы и class_weight
Если класс 1 встречается в 5 % строк, модель может почти всегда отвечать «0» и получать высокую accuracy. Сгенерируем такую выборку и сравним модель с весами классов и без них:
from sklearn.datasets import make_classification
from sklearn.metrics import accuracy_score
X, y = make_classification(n_samples=5000, n_features=10, weights=[0.95],
class_sep=0.8, random_state=1)
print("доля класса 1:", round(y.mean(), 3))
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, random_state=1)
for cw in (None, "balanced"):
m = LogisticRegression(class_weight=cw).fit(X_tr, y_tr)
p = m.predict(X_te)
print(cw, "accuracy:", round(accuracy_score(y_te, p), 3),
"recall класса 1:", round(recall_score(y_te, p), 3))
доля класса 1: 0.054
None accuracy: 0.956 recall класса 1: 0.25
balanced accuracy: 0.813 recall класса 1: 0.794
Без весов модель находит только четверть редкого класса. С class_weight="balanced" находит 79 %, а accuracy падает до 0,813, потому что ложных срабатываний становится больше. Веса scikit-learn считает по формуле n_samples / (n_classes * np.bincount(y)): редкий класс получает вес во столько раз больше, во сколько раз он реже.
Многоклассовая логистическая регрессия
Для трёх и более классов ничего переключать не нужно. Все решатели, кроме liblinear, сами обучают мультиномиальную модель, softmax-обобщение сигмоиды. Параметр multi_class из старых туториалов в 1.9.1 уже не существует.
from sklearn.datasets import load_iris
X, y = load_iris(return_X_y=True)
X_tr, X_te, y_tr, y_te = train_test_split(X, y, stratify=y, random_state=0)
m = LogisticRegression(max_iter=1000).fit(X_tr, y_tr)
print(m.coef_.shape)
print(m.predict_proba(X_te[:2]).round(3))
print(round(m.score(X_te, y_te), 3))
(3, 4)
[[0.968 0.032 0. ]
[0.966 0.034 0. ]]
1.0
У модели теперь по строке коэффициентов на каждый из трёх сортов ириса, а вероятности в строке predict_proba в сумме дают единицу. Точность 1,0 объясняется учебным датасетом: виды хорошо разделяются по размерам лепестков, на реальных данных такого не ждите.
Частые ошибки и что с ними делать

ConvergenceWarning: lbfgs failed to converge
Модель обучили на сырых признаках, без масштабирования:
clf = LogisticRegression()
clf.fit(X_train, y_train)
print(clf.n_iter_, round(clf.score(X_test, y_test), 3))
ConvergenceWarning: lbfgs failed to converge after 100 iteration(s) (status=1):
STOP: TOTAL NO. OF ITERATIONS REACHED LIMIT
[100] 0.958
Вывод сокращён: после строки STOP scikit-learn ещё советует поднять max_iter или масштабировать данные. Решатель упёрся в лимит max_iter=100, и точность упала с 0,986 до 0,958. Решение: добавьте StandardScaler в пайплайн. Если просто поднять max_iter до 10 000 и не масштабировать признаки, предупреждение уйдёт, а причина останется.
ValueError при L1-штрафе с решателем lbfgs
Ошибка появляется, если попросить L1-штраф у решателя по умолчанию: LogisticRegression(l1_ratio=1) или старое penalty="l1".
ValueError: Solver lbfgs supports only 'l2' or None penalties, got l1 penalty.
Решение: укажите solver="liblinear" или solver="saga", совместимость решателей и штрафов есть в таблице выше.
Масштабирование до разделения выборки
- Симптом: на тесте метрики отличные, на новых данных хуже.
- Причина:
StandardScaler().fit_transform(X)вызвали на всех данных доtrain_test_split, и среднее тестовой части просочилось в обучение. - Решение: поместите масштабирование внутрь
make_pipeline, тогда иcross_val_scoreпереобучает его на каждом фолде.
Перепутанный положительный класс
predict_proba(X)[:, 1] всегда возвращает вероятность класса, который стоит вторым в classes_. В breast cancer это доброкачественная опухоль, хотя по смыслу задачи «положительным» хочется считать злокачественную. Перед расчётом recall и выбором порога проверьте model.classes_ и data.target_names.
Accuracy на несбалансированных данных
Accuracy 0,956 в примере выше получилась у модели, которая пропускает три четверти редкого класса. Смотрите recall и precision по каждому классу, ROC AUC или PR AUC.
Ещё часто путают корреляция признака с целевой переменной и его коэффициент в модели, а это разные вещи. При сильно связанных признаках коэффициенты делят вклад между собой, и знак отдельного веса может удивить.
Где освоить машинное обучение на Python системно
Логистическую регрессию обычно изучают в первом модуле курсов по машинному обучению, сразу после линейной. Дальше идут деревья решений, бустинг, работа с признаками и вывод модели в продакшн. Подготовку данных под модель удобно отрабатывать на pandas, а путь в профессию с нуля мы разобрали в статье как стать Data Scientist.
Ниже курсы, где машинное обучение разбирают на Python с проектами и проверкой кода. Больше программ — в каталоге курсов по Python.
| Курс | Школа | Стоимость со скидкой | В рассрочку | Длительность | Обзор курса от Checkroi |
|---|---|---|---|---|---|
| Machine Learning с нуля до Junior Перейти на сайт курса | 123 543 ₽ | 4384 ₽/мес. | 9 месяцев | Обзор курса | |
| Бакалавриат «Data Science & Machine Learning» Перейти на сайт курса | 75 000 ₽ | - | 48 месяцев | Обзор курса | |
| Машинное обучение Перейти на сайт курса | 43 600 ₽ | 2598 ₽/мес. | 10 месяцев | Обзор курса | |
| Machine Learning. Продвинутый уровень Перейти на сайт курса | 65 000 ₽ | 8000 ₽/мес. | 5 месяцев | Обзор курса | |
| Machine Learning Engineer с нуля Перейти на сайт курса | 69 000 ₽ | - | 9 месяцев | Обзор курса |
Больше программ — в полном каталоге курсов по машинному обучению




