Задача соревнования: по характеристикам человека и его заявки на медицинскую страховку предсказать степень риска, характерную для данной страховки.
Чем будем заниматься мы:
Целевой признак (степень риска) в задаче номинальный: целые числа от 1 до 8, а метрика качества, предложенная организаторами соревнования, оценивает степень согласованности двух рейтингов. Поэтому задачу можно решать и методами классификации, и методами регрессии (в последнем случае придется округлять предсказания). Это стоит учитывать при анализе результатов визуализации.
Мы будем визуализировать только обучающую выборку. Часто при решении конкурсов полезно визуализировать также тестовую выборку (на которой нужно сделать предсказание), чтобы убедиться, что данные идентичны.
Визуализацию в python часто делают с помощью библиотеки seaborn. Установить ее можно командой pip install seaborn. Если вы по каким-то причинам не можете установить библиотеку, вам придется строить некоторые графики самостоятельно или использовать их аналоги в pyplot. Pandas также использует seaborn, чтобы строить графики.
In [1]:
import numpy as np
import pandas
from matplotlib import pyplot as plt
import seaborn
%matplotlib inline
Считываем данные:
In [6]:
data = pandas.read_csv("train.csv", na_values="NaN")
In [7]:
data.head()
Out[7]:
Часто в соревнованиях смысл признаков не известен, в нашем соревновании это не совсем так. Вы можете посмотреть описание признаков на странице с данными. Резюме: значение известно только для нескольких признаков, для остальных известна только группа, к которой этот признак принадлежит (например, медицинские данные) и тип признака: вещественный, целочисленный или категориальный. При этом неясно, можно ли считать категориальные признаки упорядоченными.
Создадим три списка признаков, соответствующие их группам: вещественные, целочисленные и категориальные (эти списки даны на странице соревнования). Уберем признак Id, так как он не несет смысловой нагрузки.
Если бы типы признаков были не даны, нам бы пришлось вручную просматривать все 128 признаков, чтобы понять, какие значения они принимают.
In [8]:
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()
Есть набор стандартных приемов визуализации, которые нужно попробовать, когда вы начинаете работу с набором данных; к ним относятся построение гистограмм признаков (histogram, density estimation), л статистик, оценка зависимости целевого признака от остальных (boxplot, scatterplot, violinplot), визуализация пар признаков (как правило, scatterplot).
Сначала рассмотрим числовые признаки, затем - категориальные.
In [9]:
data[real_features].describe()
Out[9]:
In [10]:
data[discrete_features].describe()
Out[10]:
In [11]:
data.shape
Out[11]:
Ответьте на вопросы (Блок 1):
$^*$ Будем считать, что масштаб двух признаков одинаков, если их минимумы отличаются не более, чем в 2 раза, и аналогично с максимумами.
Мы видим, что в датасете есть признаки, которые не известны почти для всех объектов. Кроме того, легко проверить, что в выборке нет объектов, для которых известны все признаки, даже если рассматривать только вещественные. Значит, при решении задачи нужно было бы использовать более сложные методы обработки пропусков, чем удаление объектов.
Вместо того, чтобы в цикле по признакам строить отдельно каждую гистограмму, стоит воспользоваться методом hist датафрейма. Рекомендуется отдельно вывести гистограммы вещественных и целочисленных признаков. Установите размер изображения (20, 20) для первой группы признаков и (10, 10) для второй, bins=100.
In [12]:
data_real = data[real_features]
data_discrete = data[discrete_features]
In [13]:
data_real.hist(bins=100, figsize=(20,20))
Out[13]:
In [14]:
data_discrete.hist(bins=100, figsize=(10,10))
Out[14]:
Ответьте на вопросы (Блок 2):
$^*$ Плотность многомерного нормального распределения во всех проекциях на отдельные переменные должна быть симметрична, куполообразна, а значит, унимодальна.
Среди вещественных есть признаки с очень большим перекосом в сторону какого-то одного значения (например, Employmennt_Info_4), возможно, их стоило бы прологарифмировать при решении задачи. Кроме того, есть признаки со сложной структурой гистограммы. Распределения целочисленных признаков (относящихся к группе медицинских) имеют схожую структуру.
В целом данные разнородны, и описать их одним вероятностным распределением непросто.
In [15]:
seaborn.pairplot(data[real_features+["Response"]].drop(
["Employment_Info_4", "Employment_Info_6", "Insurance_History_5", "Product_Info_4"], axis=1),
hue="Response", diag_kind="kde")
Out[15]:
Классы накрывают друг друга, и графики не очень информативны. Но они позволяют ответить на некоторые вопросы о взаимоотношениях признаков и соотношений признаков и классов.
Ответьте на вопросы (Блок 3):
В целом облака точек выглядят достаточно компактно (в неформальном понимании этого слова), их можно приблизить искривленным кругом.
Теперь постройте такие же графики для целочисленных признаков (никакие признаки удалять не нужно, потому что таких признаков и так немного).
In [16]:
seaborn.pairplot(data[discrete_features+["Response"]], hue="Response", diag_kind="kde")
Out[16]:
Графики выглядят еще менее информативно. Заметна тенденция, что пары признаков сконцентрированы либо на сторонач квадрата [0, 240] x [0, 240], либо на его диагонали, то есть признаки как-то связаны.
Для визуализации матрицы попарных корреляций удобно использовать функцию seaborn.heatmap, она автоматически подпишет признаки на осях и покажет colorbar. Мы вычисляем корреляции только между вещественными признаками, для целочисленных признаков корреляции вычисляется по другим формулам.
In [17]:
seaborn.heatmap(data[real_features].corr(), square=True)
Out[17]:
In [18]:
data[real_features].corr()
Out[18]:
Ответьте на вопрос (Блок 4):
In [19]:
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()
Ответьте на вопросы (Блок 5):
У нас есть много признаков с сильными перекосами в сторону отдельного значения. Учитывая, что среди категориальных признаков много медицинских показателей, можно предположить, что именно эти признаки сильно влияют на увеличение риска (целевой признак). Проверить это предположение можно, построив такие же countplot с разбивкой каждого значения дополнительно по классам (то есть у нас будет несколько групп столбиков, и в каждой группе их будет 8). Это можно сделать, указав параметр hue в этой функции аналогично тому, как мы это делали выше.
Постройте графики countplot для признаков 'Medical_Keyword_23', 'Medical_Keyword_39', 'Medical_Keyword_45' (признаки выбраны случайно) с разбивкой по классам.
In [20]:
seaborn.countplot(x='Medical_Keyword_23', hue='Response', data=data)
Out[20]:
In [21]:
extra_plots=['Medical_Keyword_23', 'Medical_Keyword_39', 'Medical_Keyword_45']
In [22]:
fig, axes = plt.subplots(2, 3, figsize=(20, 20), sharey=True)
for i in range(len(extra_plots)):
seaborn.countplot(x=extra_plots[i], hue='Response', data=data, ax=axes[i / 10, i % 10])
fig.tight_layout()
Ответьте на вопрос (Блок 6):
Наличие каких из этих трех факторов сильно повышает риск? Будем считать, что наличие признака сильно повышает риск, если количество человек, имеющих этот признак (то есть он для них равен 1) и отнесенных к категории риска 8, больше, чем такая же величина для любой другой категории риска.
Из представленного выше графика можно заметить, что наличие признака 'Medical_Keyword_23' сильно повышает риск чего бы то ни было.
In [23]:
seaborn.countplot(data.Response)
Out[23]:
В категорию 8 люди попадают чаще, чем в другие категории.
Далее можно воспользоваться средствами понижения размерности. Для задачи с дискретным целевым признаком это позволит понять, какие классы хорошо разделяются, а какие - нет.
Такие методы строят матрицу попарных расстояний между объектами, которая в случае, когда объектов много, будет занимать много памяти. Кроме того, отображать много точек на scatter plot (а именно его используют для визуализации результата понижения размерности) неудобно. Поэтому мы перемешаем выборку (и далее будем использовать ее) и выберем првые 1000 объектов для понижения размерности.
In [2]:
from sklearn.utils import shuffle
from sklearn.preprocessing import scale
In [25]:
sdata = shuffle(data, random_state=321)
Методы sklearn не принимают матрицы с пропусками (nan). Чтобы избежать этой проблемы, не будем рассматривать признаки, которые имеют много пропусков (последние четыре в списке вещественных признаков). Кроме того, ограничимся рассмотрением вещественных признаков.
В следующей ячейке мы отбираем нужные признаки, затем находим объекты, у которых все признаки известны (нет пропусков в выбранных признаках), а затем создаем отдельно матрицу объекты-признаки для работы методов понижения размерности и отдельно вектор правильных ответов на этих объектах: data_subset и response_subset. Кроме того, мы дополнительно стандартизуем нашу маленькую выборку, потому что методы понижения размерности очень чувствительны к разномасштабным данным.
In [26]:
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.
In [27]:
from sklearn.manifold import TSNE
import matplotlib.cm as cm
Методы понижения размерности имеют такой же интерфейс, как классификаторы и регрессоры. Для построения визуализации t-SNE нужно создать объект класса и вызвать его метод fit_transform, который вернет матрицу размера число объектов x новая размерность; по умолчанию новая размерность равна 2. Выполните эти действия и запишите результат работы метода в переменную tsne_representation.
In [28]:
model=TSNE(random_state=321)
tsne_representation=model.fit_transform(data_subset)
Визуализируем полученное представление. Для этого создадим набор цветов по количеству классов, а затем в цикле по классам будем отображать представления точек, относящихся к этому классу. Будем указывать параметр alpha=0.5, чтобы сделать точки полупрозрачными, это лучше в ситуации, когда точки накладываются.
In [29]:
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()
Out[29]:
Теперь сделаем то же с MDS.
In [30]:
from sklearn.manifold import MDS
from sklearn.metrics.pairwise import pairwise_distances
In [31]:
model=MDS(random_state=321)
MDS_transformed=model.fit_transform(data_subset)
In [32]:
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)
Out[32]:
В t_SNE метрику можно указать при создании объекта класса TSNE, в MDS это реализуется несколько сложнее. Нужно указать dissimilarity="precomputed", а в fit_transform подать не матрицу объектов, а матрицу попарных расстояний между объектами. Создать ее можно с помощью функции pairwise_distances с параметрами: матрица объектов, метрика.
In [33]:
from sklearn.metrics.pairwise import pairwise_distances
precom_data=pairwise_distances(data_subset, metric='cosine', n_jobs=-1)
model=MDS(random_state=321, dissimilarity='precomputed')
MDS_transformed_cos=model.fit_transform(precom_data)
In [34]:
subset_l=1000
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()
Out[34]:
Ответьте на вопросы (Блок 7): Будем нумеровать визуализации от 1 до 3: tSNE, MDS по умолчанию, MDS с косинусной метрикой.
Визуализации разными методами достаточно сильно отличаются, но на каждой можно условно выделить направление, вдоль которого происходит увеличение риска от 1 до 8 (то есть точки постепенно меняют цвет). Если бы это не выполнялось, можно было бы предположить, что задача не решаема.
Воспользуемся методами поиска аномалий, рассмотренными в лекции.
Для простоты выберем вещественные признаки, которые известны для всех объектов: Product_Info_4, Ins_Age, Ht, Wt, BMI. Оставим только те, которые относятся к человеку, то есть не будем рассматривать Product_Info_4. Получится, что мы отбираем нестандартных по комбинации вес/рост/возраст людей.
In [35]:
from sklearn import svm
In [36]:
person_features = ["Ins_Age", "Ht", "Wt", "BMI"]
Два ключевых параметра OC_SVM - gamma и nu. Первый влияет на то, как хорошо граница будет приближать данные, второй - сколько точек нужно относить к выбросам. Вы можете попробовать разные значения, в том числе, значения по умолчанию, и убедиться, что при них алгоритм работает не очень адекватно. Запустите следующую ячейку, на ее выполнение может понадобиться некоторое время.
In [37]:
svm_ = svm.OneClassSVM(gamma=10, nu=0.01)
svm_.fit(sdata[person_features])
Out[37]:
In [38]:
labels = svm_.predict(sdata[person_features])
In [39]:
(labels==1).mean()
Out[39]:
У вас должно получиться, что около 98% объектов не признаются выбросами.
Попытаемся визуализировать, какие объекты отнесены к шумовым. Для этого нужно построить scatter-графики для каждой пары признаков в person_features (всего 6 графиков). На каждом графике нужно отдельно отобразить точки с labels==1 и labels==-1.
Создайте pyplot-фигуру с 6 графиками: 2 x 3, укажите размер фигуры (12, 8). Затем в цикле по парам признаков из person_features отобразите scatter-графики точек (сделайте точки с разными labels разных цветов: синие, c="blue", - обычные точки, у которых labels==1, красные, c="red", - шумовые, у которых labels==-1.) Для построения графика можно пользоваться командой axes[...].scatter(...), вместо ... - ваш код. Функция scatter принимает две вектора одинаковой длины, абсциссы и ординаты точек, и дополнительные параметры, например, цвет c и коэффициент прозрачности точек alpha. Подпишите оси названиями признаков, это можно сделать с помощью команды axes[...].set_xlabel(...) или axes[...].set_ylabel(...).
In [40]:
from itertools import combinations
for item in combinations(person_features,2):
print item[0], item[1]
In [41]:
labels_colors = ['b' if l==1 else 'r' for l in labels]
fig, axes = plt.subplots(2, 3, figsize=(12, 8))
num_graph = 0
for i in range(len(person_features) - 1):
for j in range(i + 1, len(person_features)):
ax = axes[num_graph / 3, num_graph % 3]
ax.set_xlabel(person_features[i])
ax.set_ylabel(person_features[j])
ax.scatter(sdata[person_features[i]], sdata[person_features[j]], c=labels_colors, alpha=0.5)
num_graph += 1
Вы должны увидеть, что, во-первых, синие точки заполняют почти все облако точек, а во-вторых, красные точки, как правило, находятся на границе или далеко от облака - это аномальные объекты.
In [42]:
new_params=['BMI','Employment_Info_1','Medical_History_32']
objects_with_nan = data.index[np.any(np.isnan(data[new_params].values), axis=1)]
data_subset = scale(data[new_params].drop(objects_with_nan, axis=0))
In [43]:
seaborn.distplot(data_subset[:,0],bins=50)
Out[43]:
In [44]:
seaborn.distplot(data_subset[:,1], bins=50)
Out[44]:
In [45]:
seaborn.distplot(data_subset[:,2],bins=50)
Out[45]:
На трех графиках видно, что в конце множества значений признака есть скачок, и это явление очень похоже на аномалию.