Shanraq.org Shanraq.org
Классификация и матрица ошибок
IT

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. Модель одна и та же, данные те же, ответ разный. Это считается в задании, и это главный урок здесь: машина считает ошибки, а цену им назначает человек.

Чего этот счёт не умеет

Признак один — инфляция текущего года. С таким признаком модель в принципе не может отличить страну, которая входит в кризис, от страны, которая из него выходит: числа одинаковые, а дальше по-разному.

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

Карта урока

Карта урока: ответ, матрица и цена ошибки

Скажите своими словами

Ответьте вслух или на бумаге, не подглядывая. Ответы — в конце урока.

  1. Почему точность 0.675 у модели, которая всегда говорит «нет»?
  2. Чем ложное «да» отличается от пропуска и почему их нельзя складывать?
  3. Откуда берётся порог 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 и найдите на ней те пороги, которые вы считали руками.

Куда это встанет в проекте

Шага нет. Сводка отвечает числами, и превращать её в предсказателя «будет ли высокая инфляция» на трёх странах было бы ровно тем, от чего курс отговаривает с сорок второго урока.

Но одно правило из этого урока в сводке уже работает, и стоит назвать его вслух: у каждой цифры на странице должна быть видна цена ошибки. Поэтому тренд печатается с худшим промахом, связь — с числом стран, а отказ считать — вместе с тем, чего не хватило. Матрица ошибок — та же мысль для ответов «да» и «нет».

Ответы

Показать ответы

На вопросы

  1. Потому что высокая инфляция случается в трети пар: ответив «нет» всегда, модель права во всех остальных случаях. Это не качество модели, это доля редкого ответа в данных.
  2. Ложное «да» — это поднятая зря тревога; пропуск — не поднятая вовремя. Складывать их нельзя, потому что они стоят разного: где-то ложная тревога это потраченный час, а пропуск — потерянные деньги, где-то наоборот.
  3. Порог 0.5 — это умолчание predict, а не свойство задачи. Сдвинуть его означает изменить, с какой уверенности модели мы согласны называть год высоким: ниже порог — больше тревог и меньше пропусков, выше — наоборот.

К разминке

  1. Девять лет из десяти спокойные, поэтому «всегда нет» право в девяти случаях. При этом единственный высокий год оно не нашло — и в этом весь смысл разминки.
точность: 0.9
найдено высоких: 0
  1. 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
  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: пропусков ноль, но ложных тревог тридцать восемь при пятидесяти четырёх спокойных годах. Модель, которая кричит «высокая инфляция» почти на каждый год, формально не пропускает ничего — и ровно поэтому ей никто не поверит на второй раз.

Источники

Если вы нашли ошибку или опечатку в тексте статьи, то сообщите нам об этом

Проверить задание

Сначала решите и запустите в VS Code — редактор покажет ошибку на месте. Готовое решение вставьте сюда. Проверяет модель: она укажет на ошибку, но не даст готовый ответ.

Чтобы проверить, нужно войти. Войти

Комментарии (0)

Пока нет комментариев. Будьте первым.