{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "\n", "## Открытый курс по машинному обучению. Сессия № 3\n", "\n", "###
Автор материала: Безрученко Павел" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Индивидуальный проект по анализу данных
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**План исследования**\n", " - Описание набора данных и признаков\n", " - Первичный анализ признаков\n", " - Первичный визуальный анализ признаков\n", " - Закономерности, \"инсайты\", особенности данных\n", " - Предобработка данных\n", " - Кросс-валидация, подбор параметров\n", " - Построение кривых валидации и обучения \n", " - Прогноз для тестовой или отложенной выборки\n", " - Оценка модели с описанием выбранной метрики\n", " - Выводы\n", " \n", " Более детальное описание [тут](https://goo.gl/cJbw7V)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ссылка на данные: https://www.kaggle.com/uciml/breast-cancer-wisconsin-data/data\n", "\n", "Описание признаков: https://archive.ics.uci.edu/ml/machine-learning-databases/breast-cancer-wisconsin/wdbc.names" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import pandas as pd\n", "import numpy as np\n", "import matplotlib\n", "import matplotlib.pyplot as plt\n", "import seaborn as sns\n", "\n", "from tqdm import tqdm\n", "from sklearn.model_selection import train_test_split, cross_val_score, learning_curve, StratifiedKFold, GridSearchCV\n", "from sklearn.metrics import accuracy_score, precision_score\n", "from sklearn.ensemble import RandomForestClassifier\n", "from sklearn.manifold import TSNE\n", "from sklearn.preprocessing import StandardScaler\n", "\n", "pd.set_option('display.height', 1000)\n", "pd.set_option('display.max_rows', 500)\n", "pd.set_option('display.max_columns', 500)\n", "pd.set_option('display.width', 1000)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = pd.read_csv('data/data.csv', sep=',')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 1. Описание набора данных и признаков" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Признаки были получены из оцифрованных изображений (FNA) молочной железы. Они описывают\n", "характеристики ячеек клеток, присутствующих в изображении.\n", "\n", "Наша задача бинарно классифицировать пациентов по типу опухоли - доброкачественная или злокачественная.\n", "Целевой признак \"diagnosis\", значения которого (\"M\" и \"B\") соответсвуют диагнозу (\"Malignant\" или \"Benign\").\n", "\n", "Количество сэмплов: 569\n", "\n", "Количество фич: 32 (ID, диагноз и 30 переменных в формате real, обозначающих характеристики опухоли)\n", "\n", "В данных пропусков нет. \n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.info()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для каждой опухоли вычисляются десять вещественных признаков:\n", "\n", "+ **радиус**\n", "+ **текстура** (стандартное отклонение значений шкалы серого)\n", "+ **периметр**\n", "+ **площадь**\n", "+ **гладкость**\n", "+ **компактность** (периметр ^ 2 / площадь - 1,0)\n", "+ **вогнутость**\n", "+ **вогнутые точки** (количество вогнутых участков контура)\n", "+ **симметрия**\n", "+ **фрактальная размерность**\n", " \n", "Mean, SE (standard error) и Worst этих признаков были рассчитаны для каждого изображения,\n", "в результате чего мы имеем 30 фич. \n", "\n", "Например, поле 3 представляет собой средний радиус, поле\n", "13 - Радиус SE, поле 23 является Worst Радиусом." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 2. Первичный анализ признаков" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на распределение целевого признака" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Malignant:', len(df[df['diagnosis'] == 'M']))\n", "print('Benign:', len(df[df['diagnosis'] == 'B']))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим датасет на наличие пропущенных значений" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.isnull().sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Отлично, все значения присутствуют, кроме полностью нулевого признака, который мы позже удалим" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Основная статистическая информация:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.describe()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.get_ftype_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В датасете только один категориальный признак, который является целевым, все остальные числа с плавающей точкой" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 3. Предобработка данных " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Представим переменную diagnosis в бинарном виде" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.diagnosis = df.diagnosis.replace({'M': 1, 'B': 0})\n", "df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Удалим столбец с пустыми значениями и идетификаторы пациентов" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.drop(columns=['Unnamed: 32', 'id'], inplace=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 4. Первичный визуальный анализ признаков" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Более наглядное распределение целевой переменной:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.countplot(x=df['diagnosis'], palette=\"Set3\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Проверим наши данные на наличие выбросов и посмотрим на разницу значений между переменными:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=[30, 30])\n", "ax = sns.boxplot(data=df, palette=\"Set3\")\n", "ax.set_xticklabels(ax.get_xticklabels(), rotation=45, ha=\"right\")\n", "ax.set_xticklabels(ax.get_xticklabels(), size='xx-large')\n", "plt.tight_layout()\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на средний радиус в контексте целевой переменной" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.boxplot(x='radius_mean', y=df['diagnosis'].replace({1: 'Malignant', 0: 'Benign'}), data=df, palette=\"Set3\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что в основном средний радиус опухоли значительно больше у людей со злокачественной опухолью.\n", "\n", "Нужно проверить, есть ли отличия по другим признакам.\n", "\n", "Построим pairplot для mean фич:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ax = sns.pairplot(data=df, hue='diagnosis', \n", " vars=['radius_mean', 'texture_mean', 'perimeter_mean', 'area_mean',\\\n", " 'smoothness_mean', 'compactness_mean', 'concavity_mean', \\\n", " 'concave points_mean', 'symmetry_mean', 'fractal_dimension_mean'])\n", "plt.figure(figsize=[30,30])\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для standart error:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ax = sns.pairplot(data=df, hue='diagnosis', \n", " vars=['radius_se', 'texture_se', 'perimeter_se', 'area_se',\\\n", " 'smoothness_se', 'compactness_se', 'concavity_se', \\\n", " 'concave points_se', 'symmetry_se', 'fractal_dimension_se'])\n", "plt.figure(figsize=[30,30])\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для worst признаков:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ax = sns.pairplot(data=df, hue='diagnosis', \n", " vars=['radius_worst', 'texture_worst', 'perimeter_worst', 'area_worst',\\\n", " 'smoothness_worst', 'compactness_worst', 'concavity_worst', \\\n", " 'concave points_worst', 'symmetry_worst', 'fractal_dimension_worst'])\n", "plt.figure(figsize=[30,30])\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Попробуем теперь уменьшить размерность наших данных и разбить их на кластеры" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scaler = StandardScaler()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "df_scaled = scaler.fit_transform(df.drop(columns=['diagnosis']))\n", "tsne = TSNE(random_state=22)\n", "tsne_representation_full = tsne.fit_transform(df_scaled)\n", "\n", "plt.scatter(tsne_representation_full[:, 0], tsne_representation_full[:, 1], \n", " c=df['diagnosis'].map({0: 'blue', 1: 'orange'}));\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на матрицу корреляций:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=[30, 30])\n", "ax = sns.heatmap(df.corr(), fmt = \".1f\", cmap='YlGnBu', cbar = True, annot=True)\n", "ax.set_xticklabels(ax.get_xticklabels(), size='xx-large')\n", "ax.set_yticklabels(ax.get_yticklabels(), size='xx-large')\n", "sns.set(font_scale=1.4)\n", "plt.show();" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 5. Закономерности, \"инсайты\", особенности данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Судя по boxplot'ам в данных присутствуют выбросы, пара признаков значительно отличаются от большинства по масштабу." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "У _mean и _worst признаков на большинстве графиков видно четкое разбиение по целевому признаку.\n", "Снижение размерности данных с помощью TSN-e также показало два четких кластера с несущественным количеством аномалий (которые могут быть выбросами или ошибками в данных). \n", "\n", "Подобный результат должен означать высокую точность предсказаний у модели.\n", "\n", "На матрице корреляций оказалось много значений, стремящихся к единице, в основном потому, что некоторые признаки вычисялются друг из друга или имеют прямую зависимость между собой (например: radius_mean и radius_worst)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 6. Кросс-валидация, подбор параметров" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В качестве модели для классификации опухолей будем использовать случайный лес, т.к. данная модель нечувствительна к выбросам и может выдавать высокую точность без масштабирования и детальной настройки" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y = df.diagnosis\n", "X = df.drop(columns=['diagnosis'])\n", "X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.3, random_state=21)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfc = RandomForestClassifier(n_jobs=-1, random_state=21)\n", "rfc_params = {'max_depth': range(2, 10),\n", " 'n_estimators': [2, 20, 100],\n", " 'criterion': ['entropy', 'gini'],\n", " 'max_features': ['sqrt', None], \n", " 'min_samples_split' : range(2, 6),\n", " 'max_leaf_nodes' : [100, None]}" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Найдем наилучшее сочетание гипер-параметров в модели" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "rfc_search = GridSearchCV(rfc, param_grid=rfc_params, n_jobs=-1)\n", "rfc_search.fit(X_train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfc_search.best_params_" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 7. Построение кривых валидации и обучения " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def plot_learning_curve(estimator, title, X, y, ylim=None, cv=None,\n", " n_jobs=1, train_sizes=np.linspace(.1, 1.0, 5)):\n", " '''\n", " http://scikit-learn.org/stable/auto_examples/model_selection/plot_learning_curve.html\n", " '''\n", " \n", " plt.figure()\n", " plt.title(title)\n", " if ylim is not None:\n", " plt.ylim(*ylim)\n", " plt.xlabel(\"Training examples\")\n", " plt.ylabel(\"Score\")\n", " train_sizes, train_scores, test_scores = learning_curve(\n", " estimator, X, y, cv=cv, n_jobs=n_jobs, train_sizes=train_sizes, scoring='roc_auc')\n", " train_scores_mean = np.mean(train_scores, axis=1)\n", " train_scores_std = np.std(train_scores, axis=1)\n", " test_scores_mean = np.mean(test_scores, axis=1)\n", " test_scores_std = np.std(test_scores, axis=1)\n", " plt.grid()\n", "\n", " plt.fill_between(train_sizes, train_scores_mean - train_scores_std,\n", " train_scores_mean + train_scores_std, alpha=0.1,\n", " color=\"r\")\n", " plt.fill_between(train_sizes, test_scores_mean - test_scores_std,\n", " test_scores_mean + test_scores_std, alpha=0.1, color=\"g\")\n", " plt.plot(train_sizes, train_scores_mean, 'o-', color=\"r\",\n", " label=\"Training score\")\n", " plt.plot(train_sizes, test_scores_mean, 'o-', color=\"g\",\n", " label=\"Cross-validation score\")\n", "\n", " plt.legend(loc=\"best\")\n", " return plt" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfc_best = RandomForestClassifier(max_depth=4, max_features='sqrt', max_leaf_nodes=100, \\\n", " n_estimators=20, min_samples_split=2, random_state=21)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfc_best.fit(X_train, y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=(8, 6))\n", "plot_learning_curve(rfc_best, 'Random Forest', X_train, y_train, cv=3, n_jobs=-1);\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на важность признаков при обучении" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('Feature ranking:')\n", "for f in range(X_train.shape[1]):\n", " print('%d. feature %s (%f)' % (f + 1, X_train.columns[f],\n", " rfc_best.feature_importances_[f]))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Самыми важными признаками оказались radius_worst и perimeter_worst, забавно" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 8. Прогноз для тестовой или отложенной выборки" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ради интереса попробуем обучить модель и сделать прогноз для двух выборок - для полной и для выборки только из двух признаков (radius_worst и perimeter_worst)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "rfc_main_features = RandomForestClassifier(max_depth=4, max_features='sqrt', max_leaf_nodes=100, \\\n", " n_estimators=20, min_samples_split=2, random_state=21)\n", "rfc_main_features.fit(X_train[['radius_worst', 'perimeter_worst']], y_train)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictions_all = rfc_best.predict(X_test)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "predictions_main_features = rfc_main_features.predict(X_test[['radius_worst', 'perimeter_worst']])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 9. Оценка модели с описанием выбранной метрики" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Распределение данных по целевому признаку неравномерно (37%), поэтому в качестве метрики будем использовать precision" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "round(precision_score(y_test, predictions_all), 3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для выборки со всеми признаками мы получили достаточно высокую точность (около 96%-99% в зависимости от данных)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "round(precision_score(y_test, predictions_main_features), 3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Обучение на всего лишь двух признаках (radius_worst и perimeter_worst) выдает метрику precision около 91%-93%" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Похоже, что для высокого качества классификации опухолей достаточно иметь представление хотя бы об их размере" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 10. Выводы " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "RandomForestClassifier обученный на тренировочной выборке выдает 96%-99% по метрике precision.\n", "\n", "Наибольшее значение для классификации имеют только 2-3 признака, причиной этому может быть либо природа данных, либо их искусственность.\n", "Возможно, выборка слишком узкая, а в реальной жизни доброкачественная и злокачественная опухоли могут иметь одинаковый размер, но отличаться по другим признакам.\n", "\n", "Также не ясно, как наша модель поведет себя на большом количестве данных, т.к. у нас в выборке меньше 600 сэмплов. Скорее всего модель потребуется переобучить, вследсвие чего метрика может снизиться.\n", "\n", "Однако, по графику с валидационной кривой видно, что по мере увеличения выборки растет и точность.\n", "\n", "Вывод: для использования в реальных условиях необходимо обеспечить модель необходимым количеством информации, после чего можно будет понять целесообразно ее использовать или нет." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "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 }