{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Открытый курс по машинному обучению. Сессия № 3
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###
Индивидуальный проект по анализу данных
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Прогнозирование пристройства домашних животных
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "автор: Осипова Зоя" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Описание набора данных и признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Общее описание проблемы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проблема потерявшихся и бездомных животных стоит особенно остро в крупных городах. Актуальной задачей является поиск хозяев, социализация, и в случае не нахождения оных, пристройство животного в новые руки.\n", "Наиболее близким переводом на русский, отражающим деятельность центра, думаю, будет термин \"приют для животных\", который осуществляет поиск новых или старых хозяев, а также привлечение добровольцев для временной передержки и помощи. Данной центр является самым большим в США приютом для животных, проводящий политику \"no-kill\" (животные не усыпляются по прошествии какого-то времени нахождения в приюте)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Данные" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наборы данных взят непосредственно с сайта центра [Austin Animal Center](http://www.austintexas.gov/department/aac), 19 апреля 2018 года (датасет обновляется ежедневно и ведется с 2013 года).\n", "Данные были доступны по ссылкам на сайте центра: [раз](https://data.austintexas.gov/Health-and-Community-Services/Austin-Animal-Center-Intakes/wter-evkm) и [два](https://data.austintexas.gov/Health-and-Community-Services/Austin-Animal-Center-Outcomes/9t4d-g238), если не получается скачать (у меня несколько последних дней не открывались, возможно из-за РКН), то Яндекс-диск: [Outcomes](https://yadi.sk/i/USIwAMaR3UeosH) и [Intakes](https://yadi.sk/i/1J6Bher-3Ueoud)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Признаки" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Датасет содержит информацию о более чем 80 тысяч животных, присутствуют несколько категорий - собаки, кошки, птицы и остальные.\n", "Есть данные о возрасте, имени (если есть), времени поступлении в приют, и так далее. Данные содержатся в двух таблицах, рассмотрим подробнее:\n", "\n", "Таблица outcomes:\n", "- Animal ID (animal_id) - идентификатор животного\n", "- Name (animal_id) - имя животного\n", "- DateTime (outcome_time) - дата \"выпуска\" из центра\n", "- MonthYear (outcome_monthyear) - месяц-год выпуска\n", "- Date of Birth (date_of_birth) - дата рождения\n", "- Animal Type (animal_type) - вид животного (кошка, собака, птица и т.д.)\n", "- Sex upon Outcome (outcome_sex) - пол на момент выпуска (может меняться, так как кроме женского/мужского присутствуют указания кастрированно/стерилизованно ли животное)\n", "- Age upon Outcome (outcome_age) - возраст на момент выпуска\n", "- Breed (breed) - порода\n", "- Color (color) - цвет\n", "- Outcome Type (outcome_type) - интересующая нас целевая переменная, принимает 9 значений: Adoption, Died, Euthanasia, Disposal, Missing, Rto-adopt, Relocate. Тут в основном все понятно, единственное, затрудняюсь сказать что такое Rto-adopt, возможно повторное возвращение к человеку, однажды забравшему животного.\n", "- Outcome Subtype (outcome_subtype) - пояснения к Outcome Type, полупустой столбец\n", "\n", "\n", "Таблица intakes:\n", "Часть данных повторяется, помимо этого:\n", "- Intake Type (intake_type) - как поступило животное (найдено в дикой природе, кто-то привез, и т.д.)\n", "- Intake Condition (intake_condition) - в каком состоянии найдено животное\n", "- DateTime (intake_time) - дата поступления\n", "- MonthYear (intake_monthyear) - месяц-год поступления\n", "- Age upon Intake (intake_age) - возраст на момент поступления\n", "- Found Location (found_location) - где найдено\n", "- Sex upon Intake (intake_sex) - пол на момент поступления" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Цели и задачи" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Задачей данного проекта является поиск закономерностей и прогнозирование судьбы животных, попавших в приют. Понимание того, от каких признаков зависит пристройство животного, во-первых, могло бы позволить центру лучше прогнозировать ресурсы, во-вторых, возможно, проводить какие-то социальные кампании." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Первичный анализ признаков" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "### Импорт библиотек" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import warnings\n", "warnings.filterwarnings('ignore')\n", "%matplotlib inline\n", "from matplotlib import pyplot as plt\n", "import seaborn as sns\n", "import os\n", "import re\n", "import numpy as np\n", "import pandas as pd\n", "from sklearn.metrics import classification_report, f1_score, make_scorer\n", "from sklearn.preprocessing import StandardScaler, OneHotEncoder, LabelEncoder, FunctionTransformer\n", "from sklearn.model_selection import GridSearchCV, train_test_split, StratifiedKFold, validation_curve, learning_curve\n", "from sklearn.ensemble import RandomForestClassifier\n", "\n", "sns.set_style('whitegrid')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Загружаем данные из таблицы Outcomes" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes = pd.read_csv('Austin_Animal_Center_Outcomes.csv', sep=',', index_col=False)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.columns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Переименуем сразу столбцы для более удобного обращения к таблице." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.rename(columns={'Animal ID': 'animal_id', 'Name': 'name', 'DateTime':'outcome_date', 'MonthYear':'outcome_monthyear', \\\n", " 'Date of Birth':'date_of_birth','Outcome Type':'outcome_type', 'Outcome Subtype':'outcome_subtype', \\\n", " 'Animal Type':'animal_type','Sex upon Outcome':'outcome_sex','Age upon Outcome':'outcome_age', \\\n", " 'Breed':'breed', 'Color':'color'}, inplace=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на первые строчки таблицы и простую статистику по данным, а также на число уникальных значений в признаках." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.head(3)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.info()\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.describe(include = ['object', 'int64', 'float64'])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на распределение целевой переменной по категориям:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes['outcome_type'].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видно, что целевая переменная не сбалансирована по классам." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Предварительные наблюдения:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " - Данные категориальные, причем количество значений некоторых признаков очень велико (~2 тысяч для признака 'breed', например).\n", " - Распределение по классам в целевом признаке \"outcome_type\" несбалансированное.\n", " - Видим, что столбец 'outcome_monthyear' дублирует 'date', преобразуем и переименуем, остальные временные признаки приведем их к временному формату.\n", " - Столбец 'Outcome Subtype' содержит около 54% пропусков, от него лучше избавиться (к тому же учитывая, что в этом столбце содержатся в основном уточнения к 'outcome_type', например 'Suffering' при 'Euthanasia'). \n", " - Также много пропусков в 'name', но сам признак того, что у животного есть имя кажется мне полезным. Cоздадим бинарный признак 'is_name'.\n", " - Небольшое количество пропусков есть в 'outcome_sex', 'outcome_age' и 'outcome_type', здесь строчки с пропуском выкинем.\n", " - По количеству уникальных значений (73422 из 81322) столбца animal_id видно, что около 9.7% животных попадают в центр повторно." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Работаем с временными признаками: приводим даты к временному формату, исправляем и переименовываем \"MonthYear\", сортируем по дате:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes['outcome_date'] = outcomes['outcome_date'].apply(pd.to_datetime)\n", "outcomes['date_of_birth'] = outcomes['date_of_birth'].apply(pd.to_datetime)\n", "outcomes = outcomes.sort_values(by='outcome_date')\n", "outcomes['outcome_monthyear'] = outcomes['outcome_date'].apply(lambda x: x.year*100+x.month)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выкидываем столбец 'outcome_subtype', заполняем пропуски в именах и создаем бинарный признак is_name, выкидываем строчки с пропущенными значениями в 'outcome_sex', 'outcome_age' и 'outcome_type':" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcomes.drop(['outcome_subtype'], axis=1, inplace=True)\n", "outcomes['name'] = outcomes['name'].fillna('Unknown').astype(str)\n", "outcomes['is_name'] = outcomes['name'].apply(lambda x: 1 if x != 'Unknown' else 0)\n", "outcomes.dropna(inplace=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Удалим все записи с повторяющимся animal_id, предварительно создав колонку с признаком is_uniq (по умолчанию drop_duplicates оставляет первую из дублированных записей, а так как мы отсортировали таблицу по дате, это будет запись о первом пристройстве животного):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# проверям animal_id на повторы, параметр keep=False означает что мы ищем все записи, встречающиеся больше одного раза\n", "# (если оставить keep по умолчанию, то первая запись из дублированных не будет отмечаться как True)\n", "outcomes['is_uniq'] = outcomes['animal_id'].duplicated(keep=False).map({False:1, True:0})\n", "\n", "# выбрасываем дублированные записи кроме первого встреченного раза\n", "outcomes.drop_duplicates(['animal_id'], inplace=True, keep='first')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Использование дополнительных данных из таблицы Intakes" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Помимо таблицы Outcomes, в нашем распоряжении есть данные о поступлении животных в центр, посмотрим, сможем ли мы взять что-нибудь полезное из нее." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "intakes = pd.read_csv('Austin_Animal_Center_Intakes.csv', sep=',', index_col=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Также переименуем columns:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "intakes.rename(columns={'Animal ID': 'animal_id', 'Name': 'name', 'DateTime':'intake_date', 'MonthYear':'intake_monthyear', \\\n", " 'Date of Birth':'date_of_birth','Intake Type':'intake_type', 'Intake Condition':'intake_condition', \\\n", " 'Animal Type':'animal_type','Sex upon Intake':'intake_sex','Age upon Intake':'intake_age', \\\n", " 'Breed':'breed', 'Color':'color', 'Found Location':'found_location'}, inplace=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "intakes.head(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Попробуем понять, какая информация, которой не было в первой таблице, интересна. На первый взгляд кажется полезной информация о времени, когда животное поступило в центр, о месте где его нашли, состоянии и типе (Type и Condition) на момент поступления, поле (кастрированный/стерилизованный на момент поступления или нет), возрасте. Объединим таблицы, взяв из данных о поступлении интересные нам и предварительно выкинув дубликаты и переименовав/преобразовав данные о дате." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "intakes['intake_date'] = intakes['intake_date'].apply(pd.to_datetime)\n", "intakes = intakes.sort_values(by='intake_date')\n", "intakes['intake_monthyear'] = intakes['intake_date'].apply(lambda x: x.year*100+x.month)\n", "intakes.drop_duplicates(['animal_id'], inplace=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data = outcomes.merge(intakes[['animal_id', 'intake_condition', 'intake_type', 'found_location', 'intake_sex',\\\n", " 'intake_monthyear', 'intake_date', 'intake_age']], how='inner', left_on='animal_id', right_on='animal_id')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data.info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Данные о точном времени поступления, рождения и пристройстве оставим, возможно они еще пригодятся для генерирования новых признаков. Создаем списки признаков по категориям для более удобной работы:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cat_features = ['name', 'color','breed','animal_type','outcome_sex', \\\n", " 'intake_condition', 'intake_type', 'found_location', 'intake_sex',]\n", "\n", "month_features = ['outcome_monthyear', 'intake_monthyear']\n", "\n", "time_features = ['outcome_date','date_of_birth', 'intake_date']\n", "\n", "age_features = ['outcome_age', 'intake_age']\n", "\n", "bin_features = ['is_name','is_uniq']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Первичный визуальный анализ признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Распределение целевой переменной" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12,6))\n", "\n", "ax = sns.countplot(data['outcome_type'])\n", "ax.set_xlabel('Outcome Type', fontsize=18)\n", "ax.set_ylabel('Count', fontsize=18)\n", "ax.set_yticklabels(ax.get_yticklabels(), rotation=0)\n", "plt.show;" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что распределение классов целевой переменной сильно несбалансированное." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Распределение временных признаков" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12,6))\n", "data['date_of_birth'].value_counts().sort_values().plot.line();" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12,6))\n", "data['outcome_date'].value_counts().resample('D').sum().plot.line();" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(12,6))\n", "data['intake_date'].value_counts().resample('D').sum().plot.line();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ясно видны годовые пики активности в деятельности центра." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Анализ категориальных признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сначала посмотрим на распределения бинарных и категориальных признаков (с числом категорий < 10) и целевой переменной:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_cat(feature, loc='best', yscale='linear'):\n", " \n", " plt.figure(figsize=(12,6)) \n", " plt.xlabel(feature, fontsize=12)\n", " ax = sns.countplot(data[feature], hue=data['outcome_type']) \n", " ax.set(yscale=yscale)\n", " ax.legend(loc=loc)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('intake_sex')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('outcome_sex')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Из этих двух предыдущих графиков видно, что большую часть поступающих животных стерилизуют (Это обычная практика в приютах/центрах для животных)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('intake_condition', loc=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видно, что больные и раненные животные чаще подвергаются эвтаназии, чем найденные в здоровом состоянии." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('intake_type')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "К сожалению, почти всех поступивших диких животных усыпляют." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('animal_type', loc=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Интересное наблюдение: кошек гораздо реже, чем собак, возвращают владельцам." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('is_uniq', loc=2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_cat('is_name')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Животных с именами гораздо чаще возвращают владельцу (интерпретируемо, так как если известно имя, то скорее всего на кошке или собаке есть медальон с именем и адресом хозяина, или просто известно чье это животное). Что интереснее - животных с именами чаще и пристраивают новым владельцам. (Вешайте медальоны с адресом на кошек и собак!)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь посмотрим на распределения топ-10 пород, цвета и места где нашли, в зависимости от целевой переменной:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "outcome_types = list(set(data['outcome_type'].values))\n", "\n", "def find_top10(feature):\n", " \n", " out_dict={} \n", " for i in outcome_types:\n", " out_dict[i] = list(data[data['outcome_type'] == i][feature].value_counts().head(10).keys())\n", " \n", " return out_dict\n", "\n", "\n", "def plot_top10(feature):\n", " \n", " for idx, outcome in enumerate(outcome_types):\n", " top_feature_list = find_top10(feature)[outcome]\n", " data_x = data[data[feature].apply(lambda x: x in top_feature_list)][data['outcome_type']==outcome][feature]\n", " order=data_x.value_counts().index\n", " \n", " plt.figure(figsize=(16,4))\n", " plt.xticks(rotation=75, fontsize=12)\n", " ax = 'ax{}'.format(idx)\n", " ax = sns.countplot(data_x, order=order)\n", " ax.set_title(outcome, fontsize=12)\n", " ax.set_xlabel(xlabel='')\n", " #ax.tick_params(rotation=75, labelsize=12)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_top10('breed')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В топе возвращаемых владельцу - породы собак, диких животных в основном выпускают или подвергают эвтаназии (видимо, пораненных), неоижданно в топе пристраиваемых и умерших порода кошки \"обычная домашняя\"." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_top10('color')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Каких-то особо интересных закономерностей нет, кроме того, что везде преобладают окрасы черный, черно-белый и коричневый." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_top10('found_location')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тоже особо интересного ничего нет, кроме того, что почти всех домашних животных находят в городе Austin." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_top10('outcome_age')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Интересные закономерности, пристраивают в основном щенков и котят, возвращают взрослых животных." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Закономерности, \"инсайты\", особенности данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Особенностью данных являются разнородные признаки с преобладанием категориальных, причем с большим количеством значений. Наблюдаются некоторые зависимости между признаками и целевой переменной, например, животных с именами забирают больше, найденных диких больше подвергают эвтаназии, и так далее. Однако, то, что кастрированные и стерилизованные животные составляют большинство при пристройстве, означает лишь что, что большинство животных попавших в приют, как известно, стерилизуют, так что не все зависимости полезны." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Выбор метрики" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "У нас задача многоклассовой классификации, причем с несбалансированными классами. Accuracy (просто доля верных ответов) сразу отпадает, так как нам будут важны результаты классификации по различным классам. Это удобно смотреть по таблице classification report, которая выводит результаты presicion (точность, насколько точно класс отделяется от других), recall (полнота, насколько хорошо, т.е \"полно\" мы находим этот класс) и f1 меру (среднее гармоническое между точностью и полнотой) по всем классам. Вот f1 меру и будем использовать (с микроусреднением по классам из-за несбалансированности целевой переменной) для измерения результатов моделей, периодически сверяясь с classification report." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Выбор модели" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Особенностью данных является разные категориальные признаки, соотвественно нам подойдут модели, которые умеют работать с категориальными признаками, и возможно, с большим количеством значений. Я думаю, нам подойдет случайный лес, возможно, линейные модели и градиентный бустинг. Выберем случайный лес за его простоту, качество и интерпретируемость." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Предобработка данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Первым делом, преобразуем данные о возрасте.\n", "\n", "Ясно видно, что возраст животных в 'outcome_age' и 'intake_age' дан приблизительно, да и к тому же в разных единицах измерения (2 weeks, 1 year и так далее), и является на данный момент категориальными и неуопрядоченным. (Модель не будет \"знать\" что месяц меньше года и т.д). Для более точного анализа будет лучше вычислить возраст напрямую (на момент пристройства животного), используя данные в столбце 'date_of_birth'." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['outcome_age'] = round((data['outcome_date'] - data['date_of_birth'])/np.timedelta64(1,'W'),2)\n", "data['intake_age'] = round((data['intake_date'] - data['date_of_birth'])/np.timedelta64(1,'W'),2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим, что получилось:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['outcome_age'].describe()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['intake_age'].describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Есть записи с отрицательным возрастом, видимо, в записях о дне рождения были ошибки, заменим такие записи нулем:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data[data['outcome_age']<0]['outcome_age'] = 0\n", "data[data['intake_age']<0]['intake_age'] = 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Выделим и преобразуем целевой признак" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "target = data['outcome_type']\n", "\n", "map_dir = {'Adoption':0, 'Died':1, 'Disposal':2, 'Euthanasia':3, 'Missing': 4, \\\n", " 'Relocate':5,'Return to Owner':6, 'Rto-Adopt':7, 'Transfer':8}\n", "\n", "map_rev = {0:'Adoption', 1:'Died', 2:'Disposal', 3:'Euthanasia', 4:'Missing', \\\n", " 5:'Relocate', 6:'Return to Owner', 7:'Rto-Adopt', 8:'Transfer'}\n", "\n", "y_ = target.map(map_dir)\n", "y = y_.values" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y_.value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Категориальные признаки преобразуем с помощью LabelEncoder, так как лес не любит слишком много признаков (а их получится много если будем использовать технику One Hot Encoder)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def lab_encoder(df, columns): \n", " for col in columns:\n", " label_encoder = LabelEncoder()\n", " df[col] = label_encoder.fit_transform(df[col])\n", " return df" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data_cat = data[cat_features].copy()\n", "data_le = lab_encoder(data_cat, cat_features)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Соединим преобразованные признаки с бинарными и с возрастом в неделях." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data_rf = pd.concat([data_le, data[age_features], data[bin_features], data[month_features]], axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Разделим выборки на обучающую и отложенную, так как классы не сбалансированы, будем использовать параметр stratify:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train_rf, X_holdout_rf, y_train_rf, y_holdout_rf = train_test_split(data_rf, y, test_size=0.3,\n", " random_state=17, stratify=y)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Обучение, кросс-валидация, подбор параметров" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В параметрах случайного леса укажем class_weight='balanced'. Для оценивания качества модели будем использовать f1_score, для этого создадим scorer из metrics.f1_score, укажем микроусреднение по классам. При разбиенни по фолдам в кроссвалидации будем учитывать дисбаланс классов с помощью StratifiedKFold." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rf = RandomForestClassifier(class_weight='balanced')\n", "\n", "skf = StratifiedKFold(n_splits=3, shuffle=True, random_state=17)\n", "f1_scorer = make_scorer(f1_score, average='micro')\n", "rf_params={'n_estimators':[100, 150, 300], 'min_samples_leaf':[2,3,5]}\n", "\n", "grid_rf = GridSearchCV(rf, rf_params, n_jobs=-1, cv=skf, verbose=1, scoring=f1_scorer)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "grid_rf.fit(X_train_rf, y_train_rf)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print ('Best score: ' , grid_rf.best_score_)\n", "print ('Best params: ' , grid_rf.best_params_)\n", "print ('Test std mean: ' , np.array(grid_rf.cv_results_['std_test_score']).mean())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на важность признаков с точки зрения леса:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "feat_importance = pd.DataFrame(X_train_rf.columns, columns = ['features'])\n", "feat_importance['value'] = grid_rf.best_estimator_.feature_importances_\n", "feat_importance.sort_values('value')[::-1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Создание новых признаков и описание этого процесса" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признак \"время пребывания в приюте\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выше мы видели, что временные признаки intake_month и outcome_month важны, попробуем скомбинировать их создав новый признак." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['time_in'] = round((data['outcome_date'] - data['intake_date'])/np.timedelta64(1,'D'),2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим (вдруг опять ошибки в данных):" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['time_in'].describe()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Заменим кривые значения нулями:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data[data['time_in']<0]['time_in'] = 0" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признак цвет + порода" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Предположение: так как цвет и порода неплохо оцениваются моделью, сделаем на основе этих двух признаков новый." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['color_breed'] = data['color'] + ' ' + data['breed']" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признак \"есть mix в названии породы\", признак \"помесь двух пород\"" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Интуитивное предположение: может быть, для пристройства важна чистопородность кошки или собаки. Эту информацию можно извлечь из колонки 'breed': Mix - не чистопородное животное, запись двух пород через слэш - помесь этих двух пород." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['is_mix'] = data['breed'].apply(lambda x: 1 if 'mix' in x.lower() else 0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data['crossbreed'] = data['breed'].apply(lambda x: 1 if '/' in x else 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поссмотрим, что получилось:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data.sample(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Обучим модель с новыми данными" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "columns = cat_features + ['color_breed']\n", "data_le_new = lab_encoder(data, columns)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "data_rf_new = pd.concat([data_le_new[columns], data['crossbreed'], data['is_mix'], data['time_in'], \\\n", " data['outcome_monthyear'], data['outcome_age'], data[bin_features]], axis=1)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train, X_holdout, y_train, y_holdout = train_test_split(data_rf_new, y, test_size=0.3,\n", " random_state=17, stratify=y)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rf = RandomForestClassifier(class_weight='balanced')\n", "\n", "skf = StratifiedKFold(n_splits=5, shuffle=True, random_state=17)\n", "f1_scorer = make_scorer(f1_score, average='micro')\n", "rf_params={'n_estimators':[100, 150, 300], 'min_samples_leaf':[2,3,5]}\n", "\n", "grid_rf = GridSearchCV(rf, rf_params, n_jobs=-1, cv=skf, verbose=1, scoring=f1_scorer)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "grid_rf.fit(X_train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print ('Best score: ' , grid_rf.best_score_)\n", "print ('Best params: ' , grid_rf.best_params_)\n", "print ('Test std mean: ' , np.array(grid_rf.cv_results_['std_test_score']).mean())" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "feat_importance = pd.DataFrame(X_train.columns, columns = ['features'])\n", "feat_importance['value'] = grid_rf.best_estimator_.feature_importances_\n", "feat_importance.sort_values('value')[::-1]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как мы видим, качество модели на кросс-валидации подросло! С новым признаком \"время, проведенное в приюте\", мы также угадали. Также сочетание 'цвет + порода' неплох (можно было бы попробовать использовать на этом признаке TfIdf). А вот дворняги это, помеси или чистопородные, не очень важно." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Построение кривых валидации и обучения " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_with_err(x, data, **kwargs):\n", " mu, std = data.mean(1), data.std(1)\n", " lines = plt.plot(x, mu, '-', **kwargs)\n", " plt.fill_between(x, mu - std, mu + std, edgecolor='none',\n", " facecolor=lines[0].get_color(), alpha=0.2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Построим кривые валидации, будем менять сложность модели изменяя параметры." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "f1_scorer = make_scorer(f1_score, average='micro')\n", "rf_val = RandomForestClassifier(class_weight='balanced', random_state=17)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на зависимость от числа деревьев." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n = np.linspace(50, 500, 5).astype(int)\n", "\n", "val_train, val_test = validation_curve(rf_val, X_train, y_train,\n", " 'n_estimators', n, cv=skf,\n", " scoring=f1_scorer, n_jobs=-1)\n", "\n", "plot_with_err(n, val_train, label='training scores')\n", "plot_with_err(n, val_test, label='validation scores')\n", "plt.xlabel('n_estimators'); plt.ylabel('f1_score')\n", "plt.legend();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на зависимость от числа объектов в листе." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "n = np.linspace(1, 10, 10).astype(int)\n", "\n", "val_train, val_test = validation_curve(rf_val, X_train, y_train,\n", " 'min_samples_leaf', n, cv=skf,\n", " scoring=f1_scorer, n_jobs=-1)\n", "\n", "plot_with_err(n, val_train, label='training scores')\n", "plot_with_err(n, val_test, label='validation scores')\n", "plt.xlabel('min_samples_leaf'); plt.ylabel('f1_score')\n", "plt.legend();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Усложнение модели не приводит к росту качества." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Построим обучающие кривые." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_learning_curve(min_samples_leaf=2, n_estimators=300):\n", " train_sizes = np.linspace(0.05, 1, 20)\n", " \n", " rf_learn = RandomForestClassifier(class_weight='balanced', min_samples_leaf=min_samples_leaf, n_estimators = n_estimators)\n", " N_train, val_train, val_test = learning_curve(rf_learn, X_train, y_train, train_sizes=train_sizes, cv=skf,\n", " scoring=f1_scorer, n_jobs=-1)\n", " plot_with_err(N_train, val_train, label='training scores')\n", " plot_with_err(N_train, val_test, label='validation scores')\n", " plt.xlabel('Training Set Size'); plt.ylabel('f1 score')\n", " plt.legend()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plot_learning_curve()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим рост на кросс-валидации при увеличении датасета, возможно новые данные помогут." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Прогноз для тестовой или отложенной выборки" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сделаем оценку на отложенной выборке, используя лучшую модель из предыдущего шага." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "best_rf = grid_rf.best_estimator_" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y_predict = best_rf.predict(X_holdout)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "report = classification_report(y_holdout, y_predict)\n", "print(report, '\\n', map_rev)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как мы видим, модель все-таки не очень хорошо различает малочисленные классы, но качество на отложенной выборке хорошее." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Оценка модели с описанием выбранной метрики" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итак, мы видим, что хоть модель и старалась оптимизировать f1 score, малочисленные классы (потерявшиеся животные, выпущенные в другом месте и возвращенные владельцу взявшему на адаптацию) она различает очень плохо. Вообще, с точки зрения применения, нам важнее всего предсказать, найдутся ли у данного животного хозяева, или возьмут ли его в новую семью. Возможно, удастся улучшить результат, использую данные не только о породе, но и о размере, пушистости, характере, и так далее, часть этих данных тяжело, но возможно, извлекается из данных о породе." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Выводы " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Данные для исследования не очень чистые, много пропусков, дубликатов и неверных значений. На основе вычищенных данных построена модель для многоклассовой классификации (пристройства, возврата, и т.д) животного из приюта. Конкретно пристройство и возврат владельцу она предсказывает неплохо, но хотелось бы улучшить качество на малочисленных классах, возможно использование более интересных признаков поможет." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.4" } }, "nbformat": 4, "nbformat_minor": 1 }