Занятие 8

Бинарная линейная классификация

Теоретическая часть

Предсказания в бинарной линейной классификации

Вспомнить из лекции:

  • Как выполняются предсказания в бинарной линейной классификации?
  • Как интерпретировать веса?
  • Вспомните обозначения $\langle \cdot, \cdot \rangle$, $sign(z)$, $[z>a]$.

Задача 1.

Какое предсказание вернет бинарный линейный классификатор $a(x) = sign(\langle w, x \rangle+w_0)$ для объекта $x=(1, 0, 0, 1, 1)$ при использовании весов $w=(0.1, -0.2, 0.5, -1.1, 0)$ и $w_0=0.35$?

Решение. Запишем скалярное произведение: $\langle w, x \rangle = w_1 x_1 + \dots + w_d x_d$, где $d$ - размерность обоих векторов (число признаков). В нашей задаче $d=5$. Итак, в скобках получится значение $0.1\cdot1 - 0.2\cdot0 + 0.5\cdot 0 -1.1 \cdot 1 + 0 \cdot 1 + 0.35 = -0.65$. Его знак отрицательный, значит $a(x) = -1$.

Задача 2.

Визуализируйте разделящую поверхность классификатора $a(x) = sign(\langle w, x \rangle+w_0)$ для $w=(-1, 2)$, $w_0=0.5$, задача бинарной классификации с двумя признаками.

Решение.

Предсказания в линейной классификации выполняются по формуле $a(x) = sign(\langle w, x\rangle +w_0)$, то есть класс +1, если выражение в скобках больше 0, и -1, если выражение в скобках меньше 0. Если выражение в скобках равно 0, считаем, что отказываемся от классификации или выбираем случайный класс (на практике такая ситуация встречается очень редко). Соответственно, мы можем сделать такое предсказание в каждой точке признакового пространства, то есть для любого возможного объекта, и получить области классов +1 и -1. Разделяющей границей этих двух областей будет прямая, поэтому классификация линейная.

Разберемся, почему разделяющей границей будет прямая. Вспомним, что уравнение $w_1 x_1 + w_2 x_2 + w_0 = 0$ задает прямую на плоскости в координатах $x_1-x_2$ (мы обычно обозначаем эти координаты $d_1-d_2$, чтобы не путать $x_1$ - первый признак или первый объект в выборке; в данном контексте - первый признак). Все точки $x=(x_1, x_2)$, для которых $w_1 x_1 + w_2 x_2 + w_0 > 0$, находятся с одной стороны от прямой, а все точки, для которых $w_1 x_1 + w_2 x_2 + w_0 < 0$, с другой стороны от прямой. А эти два условия как раз и проверяются в бинарном линейной классификаторе.

В нашем случае прямая задается уравнением $-x_1 + 2 x_2 + 0.5 = 0$. Построим ее по точкам: при $x_1=0$ выполнено $2 x_2+0.5=0$, то есть $x_2=-1/4$; при $x_1=1$ выполнено $2 x_2 -0.5=0$, то есть $x_2=1/4$. Проводим прямую через точки $(0, -1/4)$ и $(1, 1/4)$. Теперь выбираем любую точку, не принадлежащую прямой, например $(0, 1/4)$. Проверяем, какой будет знак выражения $-x_1 + 2 x_2 + 0.5$: $0 + 0.5 + 0.5 = 1 > 0$. Значит, в полуплоскости, где находится эта точка, мы предсказываем класс +1, а в другой - класс -1.

Вспомнить из лекции

  • Какие метрики бинарной классификации вы знаете?
  • Какие проблемы есть у метрики accuracy? в каких случаях она нам не подходит?
  • Что такое матрица ошибок?

Практическая часть

В практической части мы обучим линейный классификатор на данных кредитного скорринга, проанализируем веса модели, научимся работать с категориальными признаками в линейных моделях. Далее поработаем с метриками, сравних их между собой и найдем оптимиальные гиперпараметры в модели.

In [3]:
import pandas as pd

Мы будем работать с данными клиентов банка (задача кредитного скоринга). Для целей семинара данные были преобразованы в немного другой формат.

Значение признаков:

  • account: банковский счет (-1: отриц. баланс, 0: нет счета, 1: до 200 ед., 2: более 200 ед.)
  • duration: на какой период запрашивают кредит
  • credit_history: рейтинг по кредитной истории (от 0 - отличная кр. история до 4 - критическая ситуация с кредитами)
  • amount: на какую сумму запрашивают кредит
  • savings: сберегательный счет (0: нет, 1: < 100, 2: 100 <= ... < 500, 3: 500 <= ... < 1000, 4: >= 1000)
  • employment: срок работы на текущей позиции (0: не работает, 1: до 1 года, 2: от 1 до 4 лет, 3: от 4 до 7 лет, 4: более 7 лет)
  • guarantors: 1 - есть поручители, 0 - нет
  • residence: сколько лет резидент
  • age: возраст, деленный на 100
  • credits_number: число кредитов
  • job: 0: не работает, 1: неквалифицированный, 2: квалифицированный специалист, 3: высокая должность или бизнес
  • maintenance_people: число людей, которых обеспечивает заемщик
  • telephone: указан ли телефон (1: да, 0: нет)
  • foreign: 1: иностранец, 0: нет
  • real_estate: 1: есть недвижимость, 0: нет недвижимости
  • life_insurance: 1: оформлено страхование жизни, 0, нет страховки
  • car: 1: есть автомобиль, 0: нет автомобиля
  • housing_rent: 1: платит за съем жилья, 0: не платит за съем жилья
  • sex: пол - 1: муж., 0: жен.
  • purpose: на какую цель запрашивают кредит (из нескольких вариантов)
  • target: 1: кредит выдан, 0: в кредите отказано

Требуется решить задачу предсказания значения в последнем столбце, то есть задачу бинарной классификации.

По описанию данных понятно, что все признаки числовые (включая вещественные, порядковые, бинарные), кроме предпоследнего, который является категориальным.

In [3]:
#!pip install xlrd
Collecting xlrd
  Downloading xlrd-2.0.1-py2.py3-none-any.whl (96 kB)
     |████████████████████████████████| 96 kB 1.7 MB/s eta 0:00:011
Installing collected packages: xlrd
Successfully installed xlrd-2.0.1
WARNING: You are using pip version 20.2.4; however, version 21.0.1 is available.
You should consider upgrading via the '/Users/filatovartm/Library/Caches/pypoetry/virtualenvs/dpeter-16pq_eh8-py3.7/bin/python -m pip install --upgrade pip' command.
In [4]:
tab = pd.read_excel("https://github.com/nadiinchi/voronovo_seminar_materials/blob/master/base_track/seminars/scoring.xls?raw=true")
In [6]:
len(tab["purpose"].unique())
Out[6]:
10
In [6]:
tab.head() # вывести первые строки
Out[6]:
account duration credit_history amount savings employment guarantors residence age credits_number ... maintenance_people telephone foreign real_estate life_insurance car housing_rent sex purpose target
0 1 0.18 2 0.13110 2 2 1 4 0.34 1 ... 1 0 1 0 0 1 0 1 business 1
1 -1 0.18 0 0.15520 1 3 1 1 0.31 1 ... 1 1 1 0 1 0 0 1 business 1
2 -1 0.12 2 0.04510 1 3 1 4 0.21 1 ... 1 0 1 0 1 0 1 1 retraining 0
3 -1 0.30 0 0.40360 0 1 1 3 0.25 3 ... 1 0 1 0 0 1 0 1 business 1
4 0 0.36 2 0.09095 1 2 1 4 0.37 1 ... 1 1 1 0 0 0 0 1 education 0

5 rows × 21 columns

По строкам - объекты (клиенты), по столбцам - признаки, последний столбец - целевая переменная (1 - кредит выдан, 0 - в кредите отказано).

In [7]:
tab.dtypes # типы столбцов
Out[7]:
account                 int64
duration              float64
credit_history          int64
amount                float64
savings                 int64
employment              int64
guarantors              int64
residence               int64
age                   float64
credits_number          int64
job                     int64
maintenance_people      int64
telephone               int64
foreign                 int64
real_estate             int64
life_insurance          int64
car                     int64
housing_rent            int64
sex                     int64
purpose                object
target                  int64
dtype: object

Признаки в основном числовые.

In [8]:
tab["target"].value_counts()
Out[8]:
1    300
0    300
Name: target, dtype: int64

Классы сбалансированы.

Создаем матрицу объекты-признаки и матрицу ответов. Удалим пока столбец с категориальной переменной, чтобы оставить только числовые признаки.

In [9]:
X = tab[tab.columns[:-2]]
y = tab["target"]
In [10]:
X.head()
Out[10]:
account duration credit_history amount savings employment guarantors residence age credits_number job maintenance_people telephone foreign real_estate life_insurance car housing_rent sex
0 1 0.18 2 0.13110 2 2 1 4 0.34 1 2 1 0 1 0 0 1 0 1
1 -1 0.18 0 0.15520 1 3 1 1 0.31 1 2 1 1 1 0 1 0 0 1
2 -1 0.12 2 0.04510 1 3 1 4 0.21 1 2 1 0 1 0 1 0 1 1
3 -1 0.30 0 0.40360 0 1 1 3 0.25 3 2 1 0 1 0 0 1 0 1
4 0 0.36 2 0.09095 1 2 1 4 0.37 1 2 1 1 1 0 0 0 0 1
In [11]:
X.shape, y.shape # атрибут shape показывает размерности матрицы
Out[11]:
((600, 19), (600,))

Разделение выборки

In [12]:
from sklearn.model_selection import train_test_split
# функция для разделения выборки на обучающую и тестовую
In [13]:
X_train, X_test, y_train, y_test  = train_test_split(X, y, \
                                                     test_size=0.3,\
                                                     shuffle=True,
                                                     random_state=0)
In [14]:
y_train.value_counts()
Out[14]:
1    222
0    198
Name: target, dtype: int64
In [15]:
y_test.value_counts()
Out[15]:
0    102
1     78
Name: target, dtype: int64
In [16]:
X_train.shape, y_train.shape
Out[16]:
((420, 19), (420,))

Нормируем данные

In [17]:
from sklearn.preprocessing import StandardScaler
In [18]:
scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)

Импортируем класс модели

In [21]:
from sklearn.linear_model import LinearRegression
In [22]:
from sklearn.metrics import accuracy_score # функция оценки качества
In [23]:
clf_lr = LinearRegression()
clf_lr.fit(X_train, y_train)
Out[23]:
LinearRegression()
In [24]:
y_pred = clf_lr.predict(X_test)
In [28]:
accuracy_score(y_test, y_pred > 0.5)
Out[28]:
0.6388888888888888

Важности и веса признаков

Большинство алгоритмов умеют так или иначе оценивать важности признаков. В линейной модели в качестве важностей можно рассматривать веса признаков. Они хранятся в атрибуте coef_ и появляются, конечно, только после вызова процедуры обучения.

In [29]:
clf_lr.coef_
Out[29]:
array([ 0.03344627, -0.0779239 ,  0.08754819, -0.02066605,  0.03067396,
        0.06774134, -0.00975411,  0.00109616,  0.0185817 , -0.04661195,
        0.00099741, -0.00569433,  0.01735849, -0.03505761,  0.06359324,
       -0.0134745 ,  0.06344944, -0.0416773 ,  0.05088774])

Задание. Оформить веса признаков в виде датафрейма: первый столбец - имя признака, второй столбец - вес, и отсортировать датафрейм по увеличению веса.

Решение:

In [ ]:
 

Признаки отсортировались по логичным критериям: плата за съем жилья, число кредитов, заемщик-иностранец уменьшают шанс получить кредит; наличие собственности, машины, работы, счета в банке - увеличивают шансы.

Впрочем, некоторые признаки отсортировались менее логично: например, наличие поручителя тоже голосует в "минус", хотя и с маленьким весом.

Обратите внимание: интерпретировать величину весов можно, только если данные отнормированы. Иначе модуль веса будет зависеть от масштаба признака.

Работа с категориальным признаком

Применим метод one-hot-encoding к переменной "цель получения кредита", чобы включить ее в модель. Для этого воспользуемся функцией pd.get_dummies

In [40]:
tab_ohe = pd.get_dummies(tab, "purpose")
In [7]:
tab
Out[7]:
account duration credit_history amount savings employment guarantors residence age credits_number ... maintenance_people telephone foreign real_estate life_insurance car housing_rent sex purpose target
0 1 0.18 2 0.13110 2 2 1 4 0.34 1 ... 1 0 1 0 0 1 0 1 business 1
1 -1 0.18 0 0.15520 1 3 1 1 0.31 1 ... 1 1 1 0 1 0 0 1 business 1
2 -1 0.12 2 0.04510 1 3 1 4 0.21 1 ... 1 0 1 0 1 0 1 1 retraining 0
3 -1 0.30 0 0.40360 0 1 1 3 0.25 3 ... 1 0 1 0 0 1 0 1 business 1
4 0 0.36 2 0.09095 1 2 1 4 0.37 1 ... 1 1 1 0 0 0 0 1 education 0
... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ... ...
595 1 0.09 2 0.02290 1 2 1 3 0.24 1 ... 1 0 1 1 0 0 0 1 television 1
596 0 0.48 2 0.24220 1 0 1 2 0.33 1 ... 1 1 1 0 0 1 1 1 business 0
597 1 0.24 3 0.09825 0 2 1 4 0.42 2 ... 1 1 1 0 0 1 1 0 car_new 1
598 0 0.18 1 0.32290 1 4 1 4 0.39 2 ... 2 1 1 0 0 0 0 1 car_new 0
599 -1 0.12 0 0.05410 1 2 1 4 0.48 2 ... 1 0 1 0 0 1 0 1 car_new 0

600 rows × 21 columns

In [41]:
tab_ohe.head()
Out[41]:
account duration credit_history amount savings employment guarantors residence age credits_number ... purpose_business purpose_car_new purpose_car_used purpose_domestic_appliances purpose_education purpose_furniture purpose_others purpose_repairs purpose_retraining purpose_television
0 1 0.18 2 0.13110 2 2 1 4 0.34 1 ... 1 0 0 0 0 0 0 0 0 0
1 -1 0.18 0 0.15520 1 3 1 1 0.31 1 ... 1 0 0 0 0 0 0 0 0 0
2 -1 0.12 2 0.04510 1 3 1 4 0.21 1 ... 0 0 0 0 0 0 0 0 1 0
3 -1 0.30 0 0.40360 0 1 1 3 0.25 3 ... 1 0 0 0 0 0 0 0 0 0
4 0 0.36 2 0.09095 1 2 1 4 0.37 1 ... 0 0 0 0 1 0 0 0 0 0

5 rows × 30 columns

In [42]:
# удаляем целевую переменную с помощью метода drop
X_ohe = tab_ohe.drop("target", axis=1)
# axis=1 показывает, что мы отим удалить столбец, а не строку (axis=0)
In [43]:
X_train_ohe, X_test_ohe, y_train, y_test  = train_test_split(X_ohe, y, \
                                                     test_size=0.3,\
                                                     shuffle=True,
                                                     random_state=0)
In [44]:
scaler = StandardScaler()
X_train_ohe = scaler.fit_transform(X_train_ohe)
X_test_ohe = scaler.transform(X_test_ohe)

Благодаря фиксации random_state=0 мы получаем одно и то же разделение.

Оформим обучение классификатора и подсчет качества в виде функции:

In [48]:
def get_accuracy(clf):
    clf.fit(X_train_ohe, y_train)
    y_pred = clf.predict(X_test_ohe)
    return accuracy_score(y_test, y_pred > 0.5)
In [49]:
print(get_accuracy(LinearRegression()))
0.6666666666666666

Качество с новым признаком повысилось.

А что, если мы просто пронумеруем категории? Для этого воспользуемся классом LabelEncoder:

In [50]:
from sklearn.preprocessing import LabelEncoder
In [51]:
le = LabelEncoder()
tab["purpose"] = le.fit_transform(tab["purpose"])
In [52]:
X_le = tab[tab.columns[:-1]]
In [53]:
X_train_le, X_test_le, y_train, y_test  = train_test_split(X_le, y, \
                                                     test_size=0.3,\
                                                     shuffle=True,
                                                     random_state=0)
In [54]:
scaler = StandardScaler()
X_train_le = scaler.fit_transform(X_train_le)
X_test_le = scaler.transform(X_test_le)
In [58]:
def get_accuracy(clf):
    clf.fit(X_train_le, y_train)
    y_pred = clf.predict(X_test_le)
    return accuracy_score(y_test, y_pred > 0.5)
In [59]:
print(get_accuracy(LinearRegression()))
0.65

Посчитаем метрики нашей модели. Метрики разобранные на лекции уже реализованы в библиотеке scikit-learn.

In [60]:
from sklearn.metrics import precision_score, recall_score, confusion_matrix, f1_score
In [61]:
confusion_matrix(y_test, y_pred > 0.5)
Out[61]:
array([[53, 49],
       [16, 62]])
In [62]:
precision_score(y_test, y_pred > 0.5)
Out[62]:
0.5585585585585585
In [63]:
recall_score(y_test, y_pred > 0.5)
Out[63]:
0.7948717948717948
In [64]:
f1_score(y_test, y_pred > 0.5)
Out[64]:
0.656084656084656

Порог 0.5 мы выбрали самостоятельно, и мы можем его менять. Как увелечиение порога отразится на precision/recall?

На практике, перед нами всегда встает трейдофф - потерять часть сигнала взамен на большую точность или получить false positives взамен на больший recall.

Постройте на одном графике precision, recall и f1 score в зависимости от порога.

In [ ]:
 

Какой порог соответствует максимизации f1 score?

In [ ]:
 

Обучите линейную регрессию с регуляризацией и подберите параметр регуляризации исходя из максимального f1-score.

In [ ]: