Виузализация данных - первый шаг в решении практически любой задачи анализа данных, в частности, при участии в соревновании. Визуализация помогает ответить на следующие вопросы:
В этом семинаре мы рассмотрим основные приемы визуализации данных на примере задачи с kaggle, и познакомимся с некоторыми примерами удачного применения визуализации для решения задачи. Также в конце кратко пообсуждаем визуализацию результатов решения задачи.
Мы будем работать c данными kaggle-соревнования Prudential Life Insurance Assessment, завершившегося зимой 2016 года.
Задача соревнования: по характеристикам человека и его заявки на медицинскую страховку предсказать степень риска, характериную для данной страховки.
Целевой признак (степень риска) в задаче номинальный: целые числа от 1 до 8, а метрика качества, предложенная организаторами соревнования, оценивает степень согласованности двух рейтингов. Поэтому задачу можно решать и методами классификации, и методами регрессии (в последнем случае придется округлять предсказания). Это стоит учитывать при анализе результатов визуализации.
Метрика качества: quadratic weighted kappa. Метрика измеряет качество согласованности двух рейтингов и изменяется от 0 (случайная расстановка меток) до 1 (полная согласованность).
Чем будем заниматься мы:
Для визуализации мы будем использовать модуль seaborn.
%matplotlib inline
import numpy as np
import pandas
from matplotlib import pyplot as plt
import seaborn
plt.style.use('ggplot')
Считываем данные:
data = pandas.read_csv("data/prudential-train.csv")
test = pandas.read_csv("data/prudential-test.csv")
data.head()
Id | Product_Info_1 | Product_Info_2 | Product_Info_3 | Product_Info_4 | Product_Info_5 | Product_Info_6 | Product_Info_7 | Ins_Age | Ht | ... | Medical_Keyword_40 | Medical_Keyword_41 | Medical_Keyword_42 | Medical_Keyword_43 | Medical_Keyword_44 | Medical_Keyword_45 | Medical_Keyword_46 | Medical_Keyword_47 | Medical_Keyword_48 | Response | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 2 | 1 | D3 | 10 | 0.076923 | 2 | 1 | 1 | 0.641791 | 0.581818 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8 |
1 | 5 | 1 | A1 | 26 | 0.076923 | 2 | 3 | 1 | 0.059701 | 0.600000 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 4 |
2 | 6 | 1 | E1 | 26 | 0.076923 | 2 | 3 | 1 | 0.029851 | 0.745455 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8 |
3 | 7 | 1 | D4 | 10 | 0.487179 | 2 | 3 | 1 | 0.164179 | 0.672727 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8 |
4 | 8 | 1 | D2 | 26 | 0.230769 | 2 | 3 | 1 | 0.417910 | 0.654545 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 8 |
5 rows × 128 columns
test.head()
Id | Product_Info_1 | Product_Info_2 | Product_Info_3 | Product_Info_4 | Product_Info_5 | Product_Info_6 | Product_Info_7 | Ins_Age | Ht | ... | Medical_Keyword_39 | Medical_Keyword_40 | Medical_Keyword_41 | Medical_Keyword_42 | Medical_Keyword_43 | Medical_Keyword_44 | Medical_Keyword_45 | Medical_Keyword_46 | Medical_Keyword_47 | Medical_Keyword_48 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | 1 | 1 | D3 | 26 | 0.487179 | 2 | 3 | 1 | 0.611940 | 0.781818 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
1 | 3 | 1 | A2 | 26 | 0.076923 | 2 | 3 | 1 | 0.626866 | 0.727273 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
2 | 4 | 1 | D3 | 26 | 0.144667 | 2 | 3 | 1 | 0.582090 | 0.709091 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
3 | 9 | 1 | A1 | 26 | 0.151709 | 2 | 1 | 1 | 0.522388 | 0.654545 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 1 | 1 |
4 | 12 | 1 | A1 | 26 | 0.076923 | 2 | 3 | 1 | 0.298507 | 0.672727 | ... | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
5 rows × 127 columns
Среди признаков присутствует Id. Смысловой нагрузки он, как правило, не несет, если только он не предназначен для связывания нескольких таблиц. Однако по Id иногда можно узнать принцип упорядоченности данных. Если среди признаков есть Id и время, можно строить scatter этой пары. У нас признака времени нет, попробуем строить scatter пары Id - Response (целевой) и Id - другой признак.
# scatter Id - Respose для обучающей выборки
plt.figure(figsize=(20, 2))
_ = plt.scatter(data["Id"], data["Response"], alpha=0.1, color="blue")
По графику видно только то, что категории 3 и 4 реже других встречаются в Response. Обратите внимание, что при построении scatter'ов рекомендуется делать точки полупрозрачными (alpha < 1). Это позволяет обнаружить области с меньшей плотностью, а также уменьшает влияние основного недостатка scatter'ов - зависимости от порядка отрисовки точек. Например, если мы сначала рисуем синие точки, а потом красные, и все точки плотные, то красные могут просто закрыть синие. К сожалению, даже маленькие значения alpha не всегда спасают. Построим scatter Id - Insurance_History_2:
# scatter Id - Insurance_History_2 для обучения (синие точки) и теста (красные точки)
plt.figure(figsize=(20, 2))
feature = "Insurance_History_2"
plt.scatter(data["Id"], data[feature], alpha=0.1, color="blue")
_ = plt.scatter(test["Id"], test[feature], alpha=0.1, color="red")
По этому графику можно сделать ошибочный вывод, что в тестовой выборке все значения Insurance_History_2 равны 1. Поменяем порядок отрисовки:
# scatter Id - Insurance_History_2 для обучения (синие точки) и теста (красные точки)
plt.figure(figsize=(20, 2))
feature = "Insurance_History_2"
plt.scatter(test["Id"], test[feature], alpha=0.1, color="red")
_ = plt.scatter(data["Id"], data[feature], alpha=0.1, color="blue")
Итак, здесь снова можно сделать вывод только о разном размере категорий (в категорию 1 объекты попадают реже).
Остается посмотреть на распределение номеров между обучением и тестом:
# scatter Id - 0 (объект из теста) и 1 (объект из обучения)
plt.figure(figsize=(20, 2))
plt.scatter(test["Id"], [0]*test.shape[0], alpha=0.1)
_ = plt.scatter(data["Id"], [1]*data.shape[0], alpha=0.1)
Видно, что номера распределены случайно равномерно. Итог: можем удалить Id из признаков.
Часто в соревнованиях смысл признаков не известен, в нашем соревновании это не совсем так. Согласно описанию признаков на странице с данными, значение известно только для нескольких признаков, для остальных известна только группа, к которой этот признак принадлежит (например, медицинские данные) и тип признака: вещественный, целочисленный или категориальный.
Создадим три списка признаков, соответствующие их группам: вещественные, целочисленные и категориальные (эти списки даны на странице соревнования).
Если бы типы признаков были не даны, нам бы пришлось вручную просматривать все 128 признаков, чтобы понять, какие значения они принимают.
real_features = ["Product_Info_4", "Ins_Age", "Ht", "Wt", "BMI", "Employment_Info_1", "Employment_Info_4", "Employment_Info_6",
"Insurance_History_5", "Family_Hist_2", "Family_Hist_3", "Family_Hist_4", "Family_Hist_5"]
discrete_features = ["Medical_History_1", "Medical_History_10", "Medical_History_15", "Medical_History_24", "Medical_History_32"]
cat_features = data.columns.drop(real_features).drop(discrete_features).drop(["Id", "Response"]).tolist()
Есть набор стандартных приемов визуализации, которые нужно попробовать, когда вы начинаете работу с набором данных; к ним относятся:
Конкретный вид графика, который вам подходит, зависит от типа признаков, хороший обзор приведен в разделе Plotting functions туториала seaborn.
Сначала рассмотрим чиловые признаки, затем - категориальные.
data[real_features].describe()
Product_Info_4 | Ins_Age | Ht | Wt | BMI | Employment_Info_1 | Employment_Info_4 | Employment_Info_6 | Insurance_History_5 | Family_Hist_2 | Family_Hist_3 | Family_Hist_4 | Family_Hist_5 | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|
count | 59381.000000 | 59381.000000 | 59381.000000 | 59381.000000 | 59381.000000 | 59362.000000 | 52602.000000 | 48527.000000 | 33985.000000 | 30725.000000 | 25140.000000 | 40197.000000 | 17570.000000 |
mean | 0.328952 | 0.405567 | 0.707283 | 0.292587 | 0.469462 | 0.077582 | 0.006283 | 0.361469 | 0.001733 | 0.474550 | 0.497737 | 0.444890 | 0.484635 |
std | 0.282562 | 0.197190 | 0.074239 | 0.089037 | 0.122213 | 0.082347 | 0.032816 | 0.349551 | 0.007338 | 0.154959 | 0.140187 | 0.163012 | 0.129200 |
min | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 0.076923 | 0.238806 | 0.654545 | 0.225941 | 0.385517 | 0.035000 | 0.000000 | 0.060000 | 0.000400 | 0.362319 | 0.401961 | 0.323944 | 0.401786 |
50% | 0.230769 | 0.402985 | 0.709091 | 0.288703 | 0.451349 | 0.060000 | 0.000000 | 0.250000 | 0.000973 | 0.463768 | 0.519608 | 0.422535 | 0.508929 |
75% | 0.487179 | 0.567164 | 0.763636 | 0.345188 | 0.532858 | 0.100000 | 0.000000 | 0.550000 | 0.002000 | 0.579710 | 0.598039 | 0.563380 | 0.580357 |
max | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 1.000000 | 0.943662 | 1.000000 |
Видим, что признаки имеют одинаковый масштаб (см. min, max). Есть признаки с пропущенными значениями, в том числе с большим количеством пропусков (например, Family_Hist_5).
data[discrete_features].describe()
Medical_History_1 | Medical_History_10 | Medical_History_15 | Medical_History_24 | Medical_History_32 | |
---|---|---|---|---|---|
count | 50492.000000 | 557.000000 | 14785.000000 | 3801.000000 | 1107.000000 |
mean | 7.962172 | 141.118492 | 123.760974 | 50.635622 | 11.965673 |
std | 13.027697 | 107.759559 | 98.516206 | 78.149069 | 38.718774 |
min | 0.000000 | 0.000000 | 0.000000 | 0.000000 | 0.000000 |
25% | 2.000000 | 8.000000 | 17.000000 | 1.000000 | 0.000000 |
50% | 4.000000 | 229.000000 | 117.000000 | 8.000000 | 0.000000 |
75% | 9.000000 | 240.000000 | 240.000000 | 64.000000 | 2.000000 |
max | 240.000000 | 240.000000 | 240.000000 | 240.000000 | 240.000000 |
Целочисленные признаки так же приведены к одному масштабу, но они в большинстве своем известны для малого числа объектов. Возможно, такие признаки лучше исключить из рассмотрения.
Вместо того, чтобы в цикле по признакам строить отдельно каждую гистограмму, стоит воспользоваться методом hist датафрейма. Выведем отдельно гистограммы вещественных и целочисленных признаков.
_ = data[real_features].hist(figsize=(20, 20), bins=100)
_ = data[discrete_features].hist(figsize=(10, 10), bins=100)
Среди числовых признаков нет константных, но есть такие, у которых доминирует одно значение (например, Insurance_History_5, Employment_Info_5, Medical_History_15, Medical_History_32). Для таких признаков имеет смысл создавать дополнительные бинарные переменные, показывающие, равно ли значение признака доминирующему (правда, если верить форуму, в этом соревновании feature engineering особенно не помогал). То же можно делать при наличии в распределении некоторых "точек перелома", как у признака "Ins_Age" ("разлом" в районе 0.3).
Кроме того, по гистограммам можно находить шумовые (нетипичные) объекты. Например, на гистограмме BMI видно всплеск около 1.
Если предполагается использование вероятностных методов, по гистограммам можно оценивать распределение данных. В данном случае видно, что данные не стоит описывать многомерным нормальным распределением, потому что есть признаки, распределение которых скошено или имеет тяжелые хвосты.
Аналог гистограммы - непараметрическое восстановление плотности. Применим этот метод визуализации распределений признаков для сравнения распределений в обучении и контроле:
fig, axes = plt.subplots(3, 6, figsize=(18, 9))
i = 0
for feature in real_features + discrete_features:
ax = axes[i // 6, i % 6]
seaborn.kdeplot(data[feature].dropna(), ax=ax, label="Train")
seaborn.kdeplot(test[feature].dropna(), ax=ax, label="Test")
ax.set_title(feature)
i += 1
fig.tight_layout()
Видим, что распределение данных в обучениии контроле практически одинаковое. Если бы это было не так, модель, обученная на первой части выборки, вероятно плохо работала бы на новых (тестовых) данных.
Построим scatterplot для пар вещественных признаков. Для этого в seaborn есть функция pairplot. Исключим признаки, распределение которых не похоже на колокол (хотя бы скошенный и неровный), а также содержащие большое количество пропусков, тем самым мы уменьшим размер таблицы пар. На диагоналях таблицы будут стоять оценки распределений признаков (гистограммы или восстановленные плотности, diag_kind="hist" или "kde"). Если указать параметр hue = дискретный целевой признак, то разные его значения будут отображаться разными цветами.
features_to_drop = [
"Employment_Info_4",
"Employment_Info_6",
"Insurance_History_5",
"Product_Info_4",
"Family_Hist_3",
"Family_Hist_5",
]
data_to_plot = data[real_features+["Response"]].drop(features_to_drop, axis=1).dropna()
vars_to_plot = set(data_to_plot.columns) - {"Response"}
_ = seaborn.pairplot(data_to_plot, vars=vars_to_plot,
hue="Response", diag_kind="kde", dropna=True)
Классы накрывают друг друга, и графики не очень информативны. Тем не менее, по диагональным и боковым графикам можно увидеть, что многие признаки меняют распределение для разных значений целевого признака, т. е. эти признаки должны быть важными при построении модели. Можно сказать, что признаки не являются независимыми.
Кроме того, вновь можно заметить шумовые (нестандартные) объекты, которые, возможно, стоит удалить во время обучения.
Теперь построим такие же графики для целочисленных признаков (удалим только один признак, в котором слишком много пропусков).
features_to_drop = [
"Medical_History_10",
]
data_to_plot = data[discrete_features+["Response"]].drop(features_to_drop, axis=1).dropna()
vars_to_plot = set(data_to_plot.columns) - {"Response"}
_ = seaborn.pairplot(data_to_plot, vars=vars_to_plot,
hue="Response", diag_kind="hist")
Графики выглядят еще менее информативно. Заметна тенденция, что пары признаков сконцентрированы либо на сторона квадрата [0, 240] x [0, 240], либо на его диагонали, то есть признаки как-то связаны.
Для визуализации матрицы попарных корреляций удобно использовать функцию seaborn.heatmap, она автоматически подпишет признаки на осях и покажет colorbar. Мы вычислим корреляции только между вещественными признками.
_ = seaborn.heatmap(data[real_features].corr(), square=True)
У нас есть пара сильно коррелирующих признаков: Family_Hist_2 и Family_Hist_4.
Перейдем к визуализации категориальных признаков.
Для этого создаем новую pyplot-фигуру, указываем, сколько графиков на ней будет, задаем размер; параметр sharey говорит не подписывать все оси отдельно, а подписать их только один раз слева; здесь это уместно, потому что масштабы всех счетчиков одни и те же.
В цикле по всем категориальным признакам строим countplot признака с помощью seaborn. Указываем параметр data, какой признак виузализировать, а также передаем ссылку на конкретную ячейку таблицы, в которой нужно изобразить график. Признаков всего 108, поэтому последние две ячейки таблицы графиков размером 11 x 10 останутся пустыми.
fig, axes = plt.subplots(11, 10, figsize=(20, 20), sharey=True)
for i in range(len(cat_features)):
seaborn.countplot(x=cat_features[i], data=data, ax=axes[i // 10, i % 10])
fig.tight_layout()
Среди признаков нет константных, но есть бинарные с редкими категориями. Учитывая, что среди категориальных признаков много медицинских показателей, можно предположить, что именно эти признаки сильно влияют на увеличение риска (целевой признак). Проверить это предположение можно, построив такие же countplot с разбивкой каждого значения дополнительно по классам (то есть у нас будет несколько групп столбиков, и в каждой группе 8). Это можно сделать, указав параметр hue в этой функции аналогично тому, как мы это делали выше.
Построим графики countplot для признаков 'Medical_Keyword_23', 'Medical_Keyword_39', 'Medical_Keyword_45' (признаки выбраны случайно) с разбивкой по классам.
fig, axes = plt.subplots(1, 3, figsize=(20, 8))
seaborn.countplot(x='Medical_Keyword_23', data=data, hue="Response", ax=axes[0]) # 98, 104
seaborn.countplot(x='Medical_Keyword_39', data=data, hue="Response", ax=axes[1])
_ = seaborn.countplot(x='Medical_Keyword_45', data=data, hue="Response", ax=axes[2])
Признак Medical_Keyword_23 меняет распределение при Response=1, а два других не меняют.
_ = seaborn.countplot(data.Response)
В категорию 8 люди попадают чаще, чем в другие категории.
Такая визуализация полезна, чтобы оценить важность отдельных признаков (конечно, приблизительно, потому что иногда признаки влияют на целевую переменную в комбинации с несколькими другими), а также применимость разных моделей (например, линейных).
Для вещественных признаков можно строить:
_ = plt.scatter(data["Medical_History_10"], data["Response"], alpha=0.1)
В данной задаче по scatter-графикам для вещественных признаков увидеть что-либо интересное сложно. Посмотрим на boxplot:
fig, axes = plt.subplots(3, 6, figsize=(18, 9))
i = 1
for feature in real_features+discrete_features:
plt.subplot(3, 6, i)
seaborn.boxplot(y=feature, x="Response", data=data)
i += 1
fig.tight_layout()
Квантильные характеристики распределения признака отличаются в зависимости от BMI, Employment_Info_6, группы признаков Medical_History.
Далее можно воспользоваться средствами понижения размерности. Для задачи с дискретным целевым признаком это позволит понять, какие классы хорошо разделяются, а какие - нет.
Такие методы строят матрицу попарных расстояний между объектами, которая в случае, когда объектов много, будет занимать много памяти. Кроме того, отображать много точек на scatter plot (а именно его используют для визуализации результата понижения размерности) неудобно. Поэтому мы перемешаем выборку (и далее будем использовать ее) и выберем првые 1000 объектов для понижения размерности.
from sklearn.utils import shuffle
from sklearn.preprocessing import scale
sdata = shuffle(data, random_state=321)
Методы sklearn не принимают матрицы с пропусками (nan). Чтобы избежать этой проблемы, не будем рассматривать признаки, которые имеют много пропусков (последние четыре в списке вещественных признаков). Кроме того, ограничимся расмотрением вещественных признаков.
В следующей ячейке мы отбираем нужные признаки, затем находим объекты, у которых все признаки известны (нет пропусков в выбранных признаках), а затем создаем отдельно матрицу объекты-признаки для работы методов понижения размерности и отдельно вектор правильных ответов на этих объектах: data_subset и response_subset. Кроме того, мы дополнительно стандартизуем нашу маленькую выборку, потому что методы понижения размерности очень чувствительны к разномасштабным данным (это отдельно указано в туториале в sklearn)
subset_l = 1000
selected_features = real_features[:-4]
objects_with_nan = sdata.index[np.any(np.isnan(sdata[selected_features].values), axis=1)]
data_subset = scale(sdata[selected_features].drop(objects_with_nan, axis=0)[:subset_l])
response_subset = sdata["Response"].drop(objects_with_nan, axis=0)[:subset_l]
Будем строить визуализацию с помощью наибоее популярных методов нелинейного понижения размерности: t-SNE и MDS.
from sklearn.manifold import TSNE
import matplotlib.cm as cm # импортируем цветовые схемы, чтобы рисовать графики.
tsne = TSNE(random_state=321)
tsne_representation = tsne.fit_transform(data_subset)
colors = cm.rainbow(np.linspace(0, 1, len(set(response_subset))))
for y, c in zip(set(data.Response), colors):
plt.scatter(tsne_representation[response_subset.values==y, 0],
tsne_representation[response_subset.values==y, 1], c=c, alpha=0.5, label=str(y))
_ = plt.legend()
from sklearn.manifold import MDS
from sklearn.metrics.pairwise import pairwise_distances
mds = MDS(random_state=321)
MDS_transformed = mds.fit_transform(data_subset)
colors = cm.rainbow(np.linspace(0, 1, len(set(response_subset))))
for y, c in zip(set(response_subset), colors):
plt.scatter(MDS_transformed[response_subset.values==y, 0],
MDS_transformed[response_subset.values==y, 1],
c=c, alpha=0.5, label=str(y))
plt.legend()
plt.xlim(-5, 5) # масса точек концентриурется в этом масштабе
_ = plt.ylim(-5, 5) # рекомендуем сначала отобразить визуализацию целиком, а замем раскомментировать эти строки.
Для построения представления можно пробовать разные метрики, их список доступен в документации scipy (потому что sklearn использует метрики scipy).
В t_SNE метрику можно указать при создании объекта класса TSNE, в MDS это реализуется несколько сложнее. Нужно указать metric="precomputed", а в fit_transform подать не матрицу объектов, а матрицу попарных расстояний между объектами. Создать ее можно с помощью функции pairwise_distances с параметрами: матрица объектов, метрика.
Применим MDS с косинусной метрикой:
mds = MDS(metric="precomputed", random_state=321)
MDS_transformed_cos = mds.fit_transform(pairwise_distances(data_subset, metric="cosine"))
/home/alexey/.local/lib/python3.5/site-packages/sklearn/manifold/mds.py:407: UserWarning: The MDS API has changed. ``fit`` now constructs an dissimilarity matrix from data. To use a custom dissimilarity matrix, set ``dissimilarity='precomputed'``. warnings.warn("The MDS API has changed. ``fit`` now constructs an"
colors = cm.rainbow(np.linspace(0, 1, len(set(response_subset))))
for y, c in zip(set(response_subset), colors):
plt.scatter(MDS_transformed_cos[response_subset.values[:subset_l]==y, 0],
MDS_transformed_cos[response_subset.values[:subset_l]==y, 1],
c=c, alpha=0.5, label=str(y))
_ = plt.legend()
Мы видим, что точки с большим значением Response хорошо отделяются от точек в маленьким значением целевого признака, а вот промежуточные значения сильно перемешаны.
Раз уж мы достаточно хорошо познакомились с данными соревнования, разберем основные приемы, которые применяли победители.
Данные о пользовательских транзакциях по категориям, задача предсказания сумм трат. Мы уже видели этот пример на первом семинаре, давайте кратко вспомним.
Если в соревновании значения признаков даны участникам (признаки не анонимизированы), их часто стараются трансформировать так, чтобы объекты нельзя было распознать. Например, в недавно завершившемся соревновании Сбербанка вместо дат было дано число дней, прошедших с некоторого стартового дня. Кроме того, суммы транзакций были трансформированы. Однако с помощью простейшей визуализации можно было обратно восстановить исходные значения.
График общих сумм покупок по дням (из этой визуализации А. Дьконова):
Точка резкого падения сумм - это Новый год. Значит, мы восстановили числа и месяцы.
Далее, если визуализировать суммы покупок мужчин в категории "цветы", то можно найти 14 февраля и 8 марта (из визуализации В. Журавлева):
Тогда можно сделать вывод, что год невисокосный.
Задача многоклассовой классификации (с непересекающимися классами) - предсказать тип лесного покрова.
Визуализация двух признаков из датасета с цветовой дифференциацией по классу:
В результате визуализации добавляют разницу двух указанных признаков, и качество решения повышается.
Упражнение: вспомните методы, изученные на курсе, и скажите, для каких из них этот метод может быть полезен.
Задача бинарной классификации с несбалансированными классами и большим числом разреженных признаков.
В ходе решения соревнования обнаружен "magic feature": если отсортировать объекты по времени и взять попарные (по соседним объектам) разности ID в качестве признака, то качество существенно повышается.
Визуализация признака (по оси x - индекс объекта в датасете, по оси y - значение magic feature, левый и правый график соответствуют объектам со значением целевого признака 0 и 1 соответственно):
Для объектов класса 1 значения magic feature концентрируются около 0.
Напоследок немного поговорим про визуализацию после того, как у вас уже есть обученная модель, и необходимо ее отладить или проанализировать.
Примечание: все значения ниже взяты с потолка.
Довольно часто в результате обучения каждой модели вы фактически получаете одно число — качество по выбранной метрике. Например, так:
Method | Accuracy |
---|---|
Old method | 85% |
New method | 90% |
Если у алгоритма есть какой-то элемент случайности при обучении, то можно делать несколько запусков и исследовать распределение качества. Также можно специально вносить какие-то пертрубации, чтобы исследовать устойчивость метода к ним (например, брать случайные 90% обучающей выборки и обучать метод на них).
q1 = [0.843, 0.852, 0.845, 0.861, 0.854]
q2 = [0.75, 0.81, 0.97, 0.98, 0.99]
_ = plt.boxplot([q1, q2], labels=['Old method', 'New method'])
В итоге может оказаться, что, хотя качество нового метода и выше в среднем, из-за разбороса его неудобно применять на практике (либо его нужно еще подебагать).
Еще может иметь смысл посмотреть на распределение ошибок по объектам тестовой выборки. Это можно сделать тремя способами: отсортировать ошибки, построить гистограмму или CDF.
test_errors = [
1.2, 1.1, 2.1, 0.3, 0.1, 0.7, 0.9, 2.5, 1.7, 0.2,
0.8, 0.1, 0.05, 0.4, 8.4, 1.3, 0.7, 7.2, 0.1, 0.01,
0.3, 1.7, 1.3, 0.9, 1.8, 1.9, 1.5, 1.3, 1.7, 0.1
]
plt.figure(figsize=(9, 4))
plt.subplot(1, 3, 1)
plt.plot(sorted(test_errors))
plt.subplot(1, 3, 2)
plt.hist(test_errors)
plt.subplot(1, 3, 3)
_ = plt.hist(test_errors, normed=True, cumulative=True, histtype='step')
На объекты с сильно выделяющейся ошибкой имеет смысл посмотреть отдельно. Это может помочь найти ошибку в коде, пересмотреть модель, или же придумать какой-нибудь костыль, чтобы такие объекты не сильно портили жизнь в продакшене.
Можно смотреть на распределение качества в зависимости от значений конкретных признаков, это может помочь в отладке или придумывании костылей.
x = np.random.normal(size=(1000,)).astype(int)
y = (np.random.uniform(size=(1000,))*4).astype(int)
z = np.random.exponential(size=(1000,)).astype(int)
err = np.random.normal(2.5, 0.1, size=(1000,)) + 0.1 * z
df = pandas.DataFrame({'x': x, 'y': y, 'z': z, 'err': err})
plt.subplot(1, 3, 1)
seaborn.boxplot(x='x', y='err', data=df)
plt.subplot(1, 3, 2)
seaborn.boxplot(x='y', y='err', data=df)
plt.subplot(1, 3, 3)
seaborn.boxplot(x='z', y='err', data=df)
<matplotlib.axes._subplots.AxesSubplot at 0x7f397d787240>
Последний признак явно сильно коррелирует с ошибкой, на него имеет смысл посмотреть внимательнее.
При подборе оптимальных значений гиперпараметров можно строить графики вдоль некоторых из них (например, мы раньше строили такие графики для количества деревьев в композиции). На таких графиках удобно делать кривые и для обучающей выборки, чтобы сразу видеть, где в высокой ошибке виновато переобучение или недообучение.
depths = [1, 2, 3, 4, 5, 6, 7, 8]
accuracy_depth_train = [0.65, 0.75, 0.8, 0.85, 0.9, 0.94, 0.98, 1.0]
accuracy_depth_test = [0.64, 0.73, 0.78, 0.8, 0.79, 0.75, 0.68, 0.59]
reg = [0, 0.01, 0.02, 0.05, 0.1, 0.2, 0.5, 1.0]
accuracy_reg_train = [0.95, 0.9, 0.89, 0.88, 0.85, 0.8, 0.72, 0.65]
accuracy_reg_test = [0.52, 0.52, 0.53, 0.54, 0.55, 0.57, 0.59, 0.61]
plt.figure(figsize=(8, 4))
plt.subplot(1, 2, 1)
plt.plot(depths, accuracy_depth_train, label='train')
plt.plot(depths, accuracy_depth_test, label='test')
plt.legend(loc='best')
plt.xlabel('depth')
plt.ylabel('accuracy')
plt.subplot(1, 2, 2)
plt.plot(reg, accuracy_reg_train, label='train')
plt.plot(reg, accuracy_reg_test, label='test')
plt.legend(loc='best')
plt.xlabel('regularizer')
plt.ylabel('accuracy')
<matplotlib.text.Text at 0x7f397d729d30>
Обратите внимание, что на втором графике у тестовой выборки максимум скорее всего находится вне нашего рассматриваемого диапазона, поэтому имеет смысл его расширить.