Python: от данных до своей сводки Урок 46 из 56
Классификация и матрица ошибок
Сорок пятый урок курса по Python. Модель отвечает «да» или «нет»: будет ли в следующем году инфляция выше десяти процентов. Точность 0.75 против 0.675 у модели, которая всегда говорит «нет», — и матрица ошибок показывает, что при этом пропущено двадцать высоких лет из двадцати шести.
Зачем это нужно
До сих пор модель отвечала числом. Теперь она отвечает «да» или «нет» — и это другой род ошибок: не «промахнулась на семь пунктов», а «сказала не то».
Ошибок здесь ровно два вида, и они стоят разного. Ложное «да» — зря поднятая тревога. Пропуск — не поднятая, когда было нужно. Одно число вроде точности их смешивает, и весь урок про то, как их разделить и как выбрать между ними.
Сразу целиком
Файл klassifikaciya.py. Данные настоящие: годовая инфляция десяти соседей с 2000 года, Всемирный банк. Вопрос к каждому году: будет ли в следующем инфляция десять процентов или выше.
"""Урок 45: классификация и матрица ошибок.
Ряды настоящие: годовая инфляция десяти соседей, показатель FP.CPI.TOTL.ZG
Всемирного банка. Вопрос — да или нет: будет ли в следующем году инфляция
десять процентов или выше.
"""
import numpy as np
import pandas as pd
from sklearn.linear_model import LogisticRegression
from sklearn.metrics import accuracy_score, confusion_matrix
# Страна: с какого года идёт ряд и сами ставки, в процентах.
INFLYACIYA = {
"Азербайджан": (2000, [1.81, 1.55, 2.77, 2.23, 6.71, 9.68, 8.33, 16.7, 20.85, 1.46,
5.73, 7.86, 1.07, 2.42, 1.37, 4.03, 12.44, 12.94, 2.27, 2.61,
2.76, 6.65, 13.85, 8.79, 2.21]),
"Армения": (2000, [-0.79, 3.15, 1.06, 4.72, 6.96, 0.64, 2.89, 4.41, 8.95, 3.41,
8.18, 7.65, 2.56, 5.79, 2.98, 3.73, -1.4, 0.97, 2.52, 1.44,
1.21, 7.18, 8.64, 1.98, 0.27]),
"Беларусь": (2000, [168.62, 61.13, 42.54, 28.4, 18.11, 10.34, 7, 8.43, 14.84, 12.95,
7.74, 53.23, 59.22, 18.31, 18.12, 13.53, 11.84, 6.03, 4.87, 5.6,
5.55, 9.46, 15.21, 5, 5.79]),
"Грузия": (2000, [4.06, 4.65, 5.59, 0.84, 5.66, 8.25, 9.16, 9.24, 10, 1.73,
7.11, 8.54, -0.94, -0.51, 3.07, 4, 2.13, 6.04, 2.62, 4.85,
5.2, 9.57, 11.9, 2.49, 1.11]),
"Казахстан": (2000, [13.18, 8.35, 5.84, 6.44, 6.88, 7.58, 8.72, 10.85, 17.14, 7.32,
7.4, 8.45, 5.2, 5.94, 6.85, 6.68, 14.36, 7.44, 6.16, 5.33,
6.72, 8.04, 15.03, 14.53, 8.69]),
"Кыргызстан": (2000, [18.7, 6.92, 2.13, 2.97, 4.11, 4.34, 5.55, 10.23, 24.52, 6.84,
7.97, 16.64, 2.77, 6.61, 7.53, 6.5, 0.39, 3.18, 1.54, 1.13,
6.33, 11.91, 13.92, 10.75, 5]),
"Молдова": (2000, [31.3, 9.76, 5.3, 11.75, 12.52, 11.96, 12.78, 12.37, 12.78, -0.06,
7.48, 7.69, 4.55, 4.6, 5.09, 9.68, 6.36, 6.57, 3.05, 4.84,
3.77, 5.11, 28.74, 13.42, 4.68]),
"Россия": (2000, [20.8, 21.48, 15.79, 13.66, 10.89, 12.69, 9.67, 9.01, 14.11, 11.65,
6.85, 8.44, 5.07, 6.75, 7.82, 15.53, 7.04, 3.68, 2.88, 4.47,
3.38, 6.69, 13.74, 5.87, 8.43]),
"Турция": (2000, [54.92, 54.4, 44.96, 21.6, 8.6, 8.18, 9.6, 8.76, 10.44, 6.25,
8.57, 6.47, 8.89, 7.49, 8.85, 7.67, 7.78, 11.14, 16.33, 15.18,
12.28, 19.6, 72.31, 53.86, 58.51]),
"Узбекистан": (2011, [13.78, 13.21, 11.84, 9.28, 8.75, 8.13, 13.88, 17.52, 14.53,
12.87, 10.85, 11.45, 9.96, 9.63]),
}
HIGH = 10.0 # «высокая инфляция» — это сколько
CUT = 2015 # последний год обучения
def pairs(data):
"""Пары «этот год → следующий». Ответ известен только там, где есть оба."""
rows = []
for country, (first, values) in data.items():
for i in range(len(values) - 1):
rows.append({"страна": country, "год": first + i,
"инфляция": values[i], "следующая": values[i + 1]})
table = pd.DataFrame(rows)
table["высокая"] = (table["следующая"] >= HIGH).astype(int)
return table
table = pairs(INFLYACIYA)
train = table[table["год"] <= CUT]
test = table[table["год"] > CUT]
print("== Что предсказываем")
print(" пар «год → следующий год»:", len(table))
print(" из них с высокой инфляцией:", int(table["высокая"].sum()),
f"({table['высокая'].mean():.0%})")
print(" обучение:", len(train), "пар по", CUT, "год включительно")
print(" проверка:", len(test), "пар после него")
print()
print("== Модель, которая всегда говорит «нет»")
always_no = np.zeros(len(test), dtype=int)
print(" точность:", round(accuracy_score(test["высокая"], always_no), 3))
print(" и при этом она не нашла ни одного года из",
int(test["высокая"].sum()))
print()
print("== Логистическая регрессия")
model = LogisticRegression().fit(train[["инфляция"]], train["высокая"])
guess = model.predict(test[["инфляция"]])
print(" точность:", round(accuracy_score(test["высокая"], guess), 3))
print(" выигрыш у «всегда нет»:",
round(accuracy_score(test["высокая"], guess) - accuracy_score(test["высокая"], always_no), 3))
print()
print("== Матрица ошибок")
tn, fp, fn, tp = confusion_matrix(test["высокая"], guess).ravel()
print(pd.DataFrame(
[[tn, fp], [fn, tp]],
index=["на деле спокойно", "на деле высокая"],
columns=["сказали «нет»", "сказали «да»"],
).to_string())
print(" правильных «да»:", tp, "из", tp + fn)
print(" ложных «да»:", fp)
print(" пропусков:", fn)
print()
print("== Цена ложного «да»")
probability = model.predict_proba(test[["инфляция"]])[:, 1]
rows = []
for threshold in (0.2, 0.3, 0.5, 0.7):
said_yes = (probability >= threshold).astype(int)
tn, fp, fn, tp = confusion_matrix(test["высокая"], said_yes).ravel()
rows.append({"порог": threshold, "ложных «да»": fp, "пропусков": fn,
"точность": round(accuracy_score(test["высокая"], said_yes), 3)})
print(pd.DataFrame(rows).set_index("порог").to_string())
print(" порог — это решение о цене ошибки, а не про статистику")
Вывод:
== Что предсказываем
пар «год → следующий год»: 229
из них с высокой инфляцией: 69 (30%)
обучение: 149 пар по 2015 год включительно
проверка: 80 пар после него
== Модель, которая всегда говорит «нет»
точность: 0.675
и при этом она не нашла ни одного года из 26
== Логистическая регрессия
точность: 0.75
выигрыш у «всегда нет»: 0.075
== Матрица ошибок
сказали «нет» сказали «да»
на деле спокойно 54 0
на деле высокая 20 6
правильных «да»: 6 из 26
ложных «да»: 0
пропусков: 20
== Цена ложного «да»
ложных «да» пропусков точность
порог
0.2 14 4 0.775
0.3 10 10 0.750
0.5 0 20 0.750
0.7 0 23 0.713
порог — это решение о цене ошибки, а не про статистику
Разбор
Задача «да или нет» начинается с определения «да»
Высокая инфляция — это сколько? В уроке десять процентов, и это решение, а не данные: при пороге в пять «да» стало бы большинством, при пятнадцати — редкостью. Определение цели — часть модели, и его пишут рядом с ней, а не держат в уме.
Разрез по времени — из прошлого урока: учимся по 2015 год, проверяем на том, что после. Перемешивать здесь нельзя ровно по той же причине.
Точность врёт, когда «да» редкое
всегда «нет»: точность 0.675
логистическая: точность 0.75
Модель, которая вообще ничего не считает и на всё отвечает «нет», права в двух случаях из трёх — просто потому, что высокая инфляция случается в трети лет. Любая точность ниже 0.675 хуже, чем не иметь модели; 0.75 звучит прилично и оказывается прибавкой в семь сотых.
Отсюда правило: у задачи «да или нет» точность читают только рядом с долей «да». Само по себе число ничего не значит.
Матрица ошибок
сказали «нет» сказали «да»
на деле спокойно 54 0
на деле высокая 20 6
Четыре клетки вместо одного числа, и вся картина в них. Слева сверху — верные «нет», справа снизу — верные «да». Две другие клетки и есть два вида ошибок: ложное «да» (справа сверху) и пропуск (слева снизу).
Здесь у модели ноль ложных тревог — и двадцать пропусков из двадцати шести. Она почти всегда говорит «нет», а когда говорит «да», не ошибается ни разу. Такую модель легко назвать осторожной; на деле она бесполезна ровно там, где её собирались применять.
Порог выбирают на валидации, а не на тесте
ложных «да» пропусков точность
0.2 14 4 0.775
0.3 10 10 0.750
0.5 0 20 0.750
В этом небольшом учебном примере часть после временного разреза используется для подбора порога, поэтому она является валидационной, а не независимой тестовой выборкой. Окончательное качество нужно измерять один раз на более позднем периоде, которого не касался выбор модели или порога.
predict — это predict_proba плюс сравнение с 0.5. Эта половина не следует ни из чего: её выбрали за вас, и её можно двигать.
Сдвиньте порог к 0.2 — и модель находит 22 высоких года из 26 ценой четырнадцати ложных тревог. Сдвиньте к 0.7 — тревог нет вовсе, но и польза нулевая. Точность при этом почти не меняется: 0.775, 0.750, 0.713 — она не видит того, что здесь происходит.
Цену ошибки задают снаружи
Какой порог правильный? Тот, который дешевле — а это вопрос не к данным.
Если пропущенный год инфляции обходится в пять ложных тревог, лучший порог 0.2. Если наоборот, ложная тревога стоит впятеро дороже пропуска, лучший — 0.5. Модель одна и та же, данные те же, ответ разный. Это считается в задании, и это главный урок здесь: машина считает ошибки, а цену им назначает человек.
Чего этот счёт не умеет
Признак один — инфляция текущего года. С таким признаком модель в принципе не может отличить страну, которая входит в кризис, от страны, которая из него выходит: числа одинаковые, а дальше по-разному.
И проверочных пар восемьдесят. Матрица из четырёх клеток на восьмидесяти наблюдениях — это оценка с широкими краями: одно-два наблюдения туда-сюда заметно двигают и точность, и число найденных высоких лет. Считать такие клетки до третьего знака бессмысленно.
Карта урока
Скажите своими словами
Ответьте вслух или на бумаге, не подглядывая. Ответы — в конце урока.
- Почему точность 0.675 у модели, которая всегда говорит «нет»?
- Чем ложное «да» отличается от пропуска и почему их нельзя складывать?
- Откуда берётся порог 0.5 и что значит его сдвинуть?
Разминка
Три коротких шага перед заданием: предсказать, дополнить, починить. Ответы — в конце урока, но сначала ответьте сами.
1. Предскажите. Что напечатает программа, если из десяти лет высоким был один?
from sklearn.metrics import accuracy_score
truth = [0, 0, 0, 0, 0, 0, 0, 0, 0, 1]
always_no = [0] * 10
print("точность:", accuracy_score(truth, always_no))
print("найдено высоких:", sum(1 for t, g in zip(truth, always_no) if t == g == 1))
2. Заполните пропуск. Вместо ... достаньте из матрицы четыре числа.
# четыре числа матрицы: верных «нет», ложных «да», пропусков, верных «да»
from sklearn.metrics import confusion_matrix
truth = [0, 0, 1, 1, 0, 1]
said = [0, 1, 1, 0, 0, 1]
tn, fp, fn, tp = ...
print("ложных «да»:", fp, "— пропусков:", fn)
3. Почините. Программа печатает точность 0.8 и на этом останавливается.
# одной точности мало: она молчит о том, какая именно ошибка случилась
from sklearn.metrics import accuracy_score
truth = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
said = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
print("точность:", accuracy_score(truth, said))
Задание
Обязательное. Соберите пары «год → следующий год», разрежьте по времени и проверьте, что ни один год не попал в обе части. Обучите логистическую регрессию и напечатайте матрицу ошибок при пороге 0.5 — четырьмя строками со словами, а не массивом. Затем пройдите пороги от 0.1 до 0.7 и напечатайте таблицу: ложные «да», пропуски и точность. В конце посчитайте лучший порог для двух разных цен ошибки: когда пропуск дороже тревоги впятеро и когда наоборот.
Ожидаемый вывод:
== Разрез по времени
обучение: 149 пар, из них высоких 43
проверка: 80 пар, из них высоких 26
годы, попавшие в обе части: нет
== Матрица при пороге 0.5
верных «нет»: 54
ложных «да»: 0
пропусков: 20
верных «да»: 6
== Все пороги
ложных «да» пропусков точность
порог
0.1 38 0 0.525
0.2 14 4 0.775
0.3 10 10 0.750
0.4 5 15 0.750
0.5 0 20 0.750
0.6 0 22 0.725
0.7 0 23 0.713
== Два разных ответа на один вопрос
пропуск дороже тревоги в 5 раз: лучший порог 0.2 — ложных «да» 14, пропусков 4
тревога дороже пропуска в 5 раз: лучший порог 0.5 — ложных «да» 0, пропусков 20
модель одна, ответы разные: порог выбирают не по точности
Готово — это: вывод совпадает построчно; клетки матрицы названы словами; цена ошибки задаётся числом в коде, а не выбирается глазами; лучший порог выбирается по цене, а не по точности.
На своих данных. Придумайте свой вопрос «да или нет» — сдам ли зачёт, придёт ли посылка на неделе, хватит ли денег до зарплаты — и запишите два столбца: что вы ожидали и что вышло. Даже на двадцати строках матрица покажет, в какую сторону вы ошибаетесь чаще, а это ровно то, что нельзя узнать по одной точности.
По желанию.
- Поменяйте определение высокой инфляции с 10 % на 5 % и посмотрите, как изменится доля «да» и все четыре клетки.
- Добавьте второй признак — изменение инфляции за год — и посмотрите, что станет с пропусками.
- Постройте
precision_recall_curveи найдите на ней те пороги, которые вы считали руками.
Куда это встанет в проекте
Шага нет. Сводка отвечает числами, и превращать её в предсказателя «будет ли высокая инфляция» на трёх странах было бы ровно тем, от чего курс отговаривает с сорок второго урока.
Но одно правило из этого урока в сводке уже работает, и стоит назвать его вслух: у каждой цифры на странице должна быть видна цена ошибки. Поэтому тренд печатается с худшим промахом, связь — с числом стран, а отказ считать — вместе с тем, чего не хватило. Матрица ошибок — та же мысль для ответов «да» и «нет».
Ответы
Показать ответы
На вопросы
- Потому что высокая инфляция случается в трети пар: ответив «нет» всегда, модель права во всех остальных случаях. Это не качество модели, это доля редкого ответа в данных.
- Ложное «да» — это поднятая зря тревога; пропуск — не поднятая вовремя. Складывать их нельзя, потому что они стоят разного: где-то ложная тревога это потраченный час, а пропуск — потерянные деньги, где-то наоборот.
- Порог 0.5 — это умолчание
predict, а не свойство задачи. Сдвинуть его означает изменить, с какой уверенности модели мы согласны называть год высоким: ниже порог — больше тревог и меньше пропусков, выше — наоборот.
К разминке
- Девять лет из десяти спокойные, поэтому «всегда нет» право в девяти случаях. При этом единственный высокий год оно не нашло — и в этом весь смысл разминки.
точность: 0.9
найдено высоких: 0
confusion_matrix(truth, said, labels=[0, 1]).ravel(). Аргументlabelsне формальность: без него матрица на данных без единого «да» окажется размером 1 × 1, и распаковка в четыре числа сломается.
# четыре числа матрицы: верных «нет», ложных «да», пропусков, верных «да»
from sklearn.metrics import confusion_matrix
truth = [0, 0, 1, 1, 0, 1]
said = [0, 1, 1, 0, 0, 1]
tn, fp, fn, tp = confusion_matrix(truth, said, labels=[0, 1]).ravel()
print("ложных «да»:", fp, "— пропусков:", fn)
ложных «да»: 1 — пропусков: 1
- Точность 0.8 получена моделью, которая ни разу не сказала «да». Печатать надо и то, сколько «да» она нашла.
# одной точности мало: она молчит о том, какая именно ошибка случилась
from sklearn.metrics import accuracy_score, confusion_matrix
truth = [0, 0, 0, 0, 0, 0, 0, 0, 1, 1]
said = [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
tn, fp, fn, tp = confusion_matrix(truth, said, labels=[0, 1]).ravel()
print("точность:", accuracy_score(truth, said))
print("верных «да»:", tp, "из", tp + fn)
точность: 0.8
верных «да»: 0 из 2
К заданию
Две последние строки — ради них всё и считалось. При одной цене ошибки лучший порог 0.2, при другой — 0.5, и это не спор о модели: модель между этими строками не менялась вообще. Менялось то, что дороже, а это знание не из данных.
Обратите внимание на порог 0.1: пропусков ноль, но ложных тревог тридцать восемь при пятидесяти четырёх спокойных годах. Модель, которая кричит «высокая инфляция» почти на каждый год, формально не пропускает ничего — и ровно поэтому ей никто не поверит на второй раз.
Источники
- confusion_matrix, scikit-learn — порядок клеток и зачем нужен
labels. - LogisticRegression, scikit-learn — модель и её
predict_proba. - Инфляция потребительских цен, Всемирный банк — ряды, на которых построен урок.
Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом
Комментарии (0)
Войдите, чтобы оставить комментарий →
Пока нет комментариев. Будьте первым.