{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "fca21a6c",
   "metadata": {},
   "source": [
    "# III.2 Travaux pratiques\n",
    "\n",
    "L'objectif de cette séance de travaux pratiques est d'exposer la mise en œuvre des arbres de décision pour les problèmes de classification et de régression. Ce document reprend librement certains exemples montrés dans l'excellente documentation de scikit-learn.\n",
    "\n",
    "Références externes utiles :\n",
    "\n",
    "  * [Documentation NumPy](https://docs.scipy.org/doc/numpy/user/index.html)\n",
    "  * [Documentation SciPy](https://docs.scipy.org/doc/scipy/reference/)\n",
    "  * [Documentation MatPlotLib](http://matplotlib.org/)\n",
    "  * [Site scikit-learn](http://scikit-learn.org/stable/index.html)\n",
    "  * [Site langage python](https://www.python.org)\n",
    "\n",
    "## Arbres de décision\n",
    "\n",
    "Les arbres de décision sont des méthodes d'apprentissage non paramétriques utilisées pour des problèmes de classification et de régression. L'objectif est de créer un modèle qui prédit les valeurs de la variable cible, en se basant sur un ensemble de séquences de règles de décision déduites à partir des données d'apprentissage.\n",
    "L'arbre approxime donc la cible par une succession de règles `if-then-else`.\n",
    "Ce paradigme s'applique aussi bien à des données catégorielles qu'à des données numériques.\n",
    "Plus l'arbre généré est complexe, mieux le modèle « explique » les donnés d'apprentissage mais plus le risque de sur-apprentissage (*over-fitting*) est élevé.\n",
    "\n",
    "Les arbres de décision ont plusieurs **avantages** qui les rendent intéressants dans des contextes où il est utile de comprendre la séquence de décisions prise par le modèle :\n",
    "\n",
    "* Ils sont simples à comprendre et à visualiser.\n",
    "* Ils nécessitent peu de préparation des données (normalisation, etc.).\n",
    "* Le coût d'utilisation des arbres est logarithmique.\n",
    "* Ils sont capables d'utiliser des données catégorielles et numériques.\n",
    "* Ils sont capables de traiter des problèmes multi-classe.\n",
    "* Modèle en boîte blanche : le résultat est facile à conceptualiser et à visualiser.\n",
    "\n",
    "Ces modèles présentent néanmoins deux **désavantages** majeurs :\n",
    "\n",
    "* Sur-apprentissage : parfois les arbres générés sont trop complexes et généralisent mal. Choisir des bonnes valeurs pour les paramètres profondeur maximale (`max_depth`) et nombre minimal d'exemples par feuille (`min_samples_leaf`) permet d'éviter ce problème.\n",
    "* Il peut arriver que les arbres générés ne soient pas équilibrés (ce qui implique que le temps de parcours n'est plus logarithmique). Il est donc recommandé d'ajuster la base de données avant la construction, pour éviter qu'une classe domine largement les autres (en termes de nombre d'exemples d'apprentissage).\n",
    "\n",
    "## Arbres pour la classification\n",
    "\n",
    "Dans scikit-learn, la classe [sklearn.tree.DecisionTreeClassifier](http://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html) permet de réaliser une classification multi-classe à l'aide d'un arbre de décision.\n",
    "\n",
    "On commence par importer les bons modules et construire l'objet arbre :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "de560a18",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn import tree\n",
    "clf = tree.DecisionTreeClassifier()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "dbb8cfb6",
   "metadata": {},
   "source": [
    "Pour l'exemple, nous pouvons définir un jeu de données minimaliste (deux points, chacun dans une classe) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "cd7e802b",
   "metadata": {},
   "outputs": [],
   "source": [
    "X = [[0, 0], [1, 1]]\n",
    "y = [0, 1]"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2848cb88",
   "metadata": {},
   "source": [
    "L'arbre se construit comme d'habitude à l'aide de la méthode `.fit(X, y)` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "df34739d",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf = clf.fit(X, y)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c5c9fd27",
   "metadata": {},
   "source": [
    "La prédiction sur de nouveaux échantillons se fait de façon habituelle avec `.predict(X)` :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8052806c",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf.predict([[2., 2.]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1b9b6438",
   "metadata": {},
   "source": [
    "On peut aussi prédire la probabilité de chaque classe pour un échantillon (qui est calculée comme la fraction de données d'apprentissage dans chaque feuille) :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "4a620c13",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf.predict_proba([[2., 2.]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8b044463",
   "metadata": {},
   "source": [
    "## Classification des données Iris\n",
    "\n",
    "`DecisionTreeClassifier` est capable de gérer des problèmes de classification à plusieurs classes (par exemple, avec les étiquettes 0, 1, ... K-1). Dans cet exemple nous allons travailler avec la base de données [Iris](https://archive.ics.uci.edu/ml/datasets/Iris), facilement accessible dans `sklearn`. Cette base contient 150 instances d'iris (un type de plante, chaque observation décrit sa morphologie). L'objectif est de classer chaque instance en une des trois catégories : *Iris setosa*, *Iris virginica* ou *Iris versicolor*.\n",
    "\n",
    "Une des classes est linéairement séparable par rapport aux deux autres, mais les deux autres ne sont pas séparables une par rapport à l'autre.\n",
    "\n",
    "Les attributs du jeu de données sont :\n",
    "  * longueur de sépale,\n",
    "  * largeur de sépale,\n",
    "  * longueur de pétale,\n",
    "  * largeur de pétale,\n",
    "  * classe : Iris Setosa, Iris Versicolor ou Iris Virginica.\n",
    "\n",
    "Un échantillon : (4.9,3.6,1.4,0.1, 'Iris-setosa')\n",
    "\n",
    "Le jeu de données Iris étant très commun, scikit-learn propose une fonction native permettant de le charger en mémoire :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "69335312",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.datasets import load_iris\n",
    "\n",
    "iris = load_iris()\n",
    "X, y = iris.data, iris.target"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0d327553",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Calculer les statistiques (moyenne et écart-type) des quatre variables explicatives : longueur de sépale, largueur de sépale, longueur de pétale et largeur de pétale.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bb863e89",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Combien y a-t-il d'exemples de chaque classe ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a7079c32",
   "metadata": {},
   "source": [
    "Avant de construire le modèle, séparons le jeu de données en deux : 70% pour l'apprentissage, 30% pour le test."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8e341eba",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.model_selection import train_test_split\n",
    "X_train, X_test, y_train, y_test = train_test_split(X, y, train_size=0.7, random_state=0)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5f416e7f",
   "metadata": {},
   "source": [
    "Nous pouvons désormais construire un arbre de décision sur ces données :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "27d8ecf2",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn import tree\n",
    "clf = tree.DecisionTreeClassifier()\n",
    "clf.fit(X_train, y_train)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "44cdbe9f",
   "metadata": {},
   "source": [
    "Une fois l'apprentissage terminé, nous pouvons visualiser l'arbre, soit avec matplotlib en passant par la méthode `plot_tree`, soit avec l'outil `graphviz` (commande `dot`). Par exemple, avec matplotlib :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ebd98aa0",
   "metadata": {},
   "outputs": [],
   "source": [
    "tree.plot_tree(clf, filled=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "0871c7df",
   "metadata": {},
   "source": [
    "Alternativement, il est possible de faire un export en produisant un fichier `.dot` qui est le format par défaut de graphviz :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "04845afb",
   "metadata": {},
   "outputs": [],
   "source": [
    "# On exporte le graphe dans le fichier iris.dot\n",
    "with open(\"iris.dot\", 'w') as f:\n",
    "    f = tree.export_graphviz(clf, out_file=f, filled=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "1d76d8ed",
   "metadata": {},
   "source": [
    "Ensuite, en ligne de commande, il est possible de convertir ce fichier dans de nombreux formats, par exemple en PDF :\n",
    "\n",
    "<div class=\"admonition note\">\n",
    "\n",
    "**Note**\n",
    "\n",
    "À l'heure actuelle, cette commande ne fonctionne pas sur le JupyterHub du CNAM car `dot` n'est pas installé.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "449db7a0",
   "metadata": {},
   "outputs": [],
   "source": [
    "%%bash\n",
    "dot -Tpdf iris.dot -o iris.pdf"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ecbb8573",
   "metadata": {},
   "source": [
    "L'image générée doit ressembler à ceci :\n",
    "\n",
    "![](https://scikit-learn.org/stable/figures/iris.png)\n",
    "\n",
    "Une fois le modèle construit, il est possible de l'utiliser pour la prédiction sur de nouvelles données :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e8f62938",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf.predict(X_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6aa52a12",
   "metadata": {},
   "source": [
    "On peut de cette façon calculer le score en test :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b3623a1c",
   "metadata": {},
   "outputs": [],
   "source": [
    "clf.score(X_test, y_test)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "187ceb43",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Changez les valeurs de parametres `max_depth` et `min_samples_leaf`. Que constatez-vous ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "66fddaf7",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Le problème ici étant particulièrement simple, refaites une division apprentissage/test avec 5% des données en apprentissage et 95% test.\n",
    "\n",
    "Calculez le taux d'éléments mal classifiés sur l'ensemble de test.\n",
    "\n",
    "Faites varier (ou mieux, réalisez une recherche par grille avec `GridSearchCV`) les valeurs des paramètres `max_depth` et `min_samples_leaf` pour mesurer leur impact sur ce score.\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "bc551641",
   "metadata": {},
   "source": [
    "## Affichage de la surface de décision\n",
    "\n",
    "Pour une paire d'attributs, c'est-à-dire pour des observations en deux dimensions, nous pouvons visualiser la surface de décision en 2 dimensions. D'abord on discrétise le domaine bidimensionnel avec un pas constant et ensuite on évalue le modèle sur chaque point de la grille.\n",
    "\n",
    "Dans cet exemple, nous ne gardons que la longueur et la largeur des pétales."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f10394bf",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "# Paramètres\n",
    "n_classes = 3\n",
    "plot_colors = \"bry\" # blue-red-yellow\n",
    "plot_step = 0.02\n",
    "\n",
    "# Choisir les attributs longueur et largeur des pétales\n",
    "pair = [2, 3]\n",
    "\n",
    "# On ne garde seulement les deux attributs\n",
    "X = iris.data[:, pair]\n",
    "y = iris.target\n",
    "\n",
    "# Apprentissage de l'arbre\n",
    "clf = tree.DecisionTreeClassifier().fit(X, y)\n",
    "\n",
    "# Affichage de la surface de décision\n",
    "x_min, x_max = X[:, 0].min() - 1, X[:, 0].max() + 1\n",
    "y_min, y_max = X[:, 1].min() - 1, X[:, 1].max() + 1\n",
    "xx, yy = np.meshgrid(np.arange(x_min, x_max, plot_step), np.arange(y_min, y_max, plot_step))\n",
    "Z = clf.predict(np.c_[xx.ravel(), yy.ravel()])\n",
    "Z = Z.reshape(xx.shape)\n",
    "cs = plt.contourf(xx, yy, Z, cmap=plt.cm.Paired)\n",
    "plt.xlabel(iris.feature_names[pair[0]])\n",
    "plt.ylabel(iris.feature_names[pair[1]])\n",
    "plt.axis(\"tight\")\n",
    "\n",
    "# Affichage des points d'apprentissage\n",
    "for i, color in zip(range(n_classes), plot_colors):\n",
    "    idx = np.where(y == i)\n",
    "    plt.scatter(X[idx, 0], X[idx, 1], c=color, label=iris.target_names[i], cmap=plt.cm.Paired)\n",
    "plt.axis(\"tight\")\n",
    "plt.suptitle(\"Decision surface of a decision tree using paired features\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ed183f79",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Refaire l'affichage pour les autres paires d'attributs. Sur quelle paire la séparation entre les classes est la plus marquée ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "726d89f0",
   "metadata": {},
   "source": [
    "## Arbres de décision pour la régression\n",
    "\n",
    "Pour la régression avec les arbres de décision, scikit-learn offre la classe `DecisionTreeRegressor`. Comme pour la classification, la méthode `fit(...)` prend en entrée le paramètre X (attributs des observations). Attention : les `y` ne sont pas des étiquettes de classes mais des valeurs réelles."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d47a0301",
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn import tree\n",
    "\n",
    "X = [[0, 0], [2, 2]]\n",
    "y = [0.5, 2.5]\n",
    "clf = tree.DecisionTreeRegressor()\n",
    "clf = clf.fit(X, y)\n",
    "clf.predict([[1, 1]])"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c810b1cd",
   "metadata": {},
   "source": [
    "Dans l'exemple suivant nous allons construire un signal sinusoïdal affecté par un bruit blanc et nous allons apprendre un arbre de régression sur ces données d'apprentissage."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "8515b40b",
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "\n",
    "from sklearn.tree import DecisionTreeRegressor\n",
    "\n",
    "# Créer les données d'apprentissage\n",
    "np.random.seed(0)\n",
    "X = np.sort(5 * np.random.rand(80, 1), axis=0)\n",
    "y = np.sin(X).ravel()\n",
    "\n",
    "fig = plt.figure(figsize=(12, 4))\n",
    "fig.add_subplot(121)\n",
    "plt.plot(X, y)\n",
    "plt.title(\"Signal sinusoïdal pur\")\n",
    "\n",
    "# On ajoute un bruit aléatoire tous les 5 échantillons\n",
    "y[::5] += 3 * (0.5 - np.random.rand(16))\n",
    "fig.add_subplot(122)\n",
    "plt.plot(X, y)\n",
    "plt.title(\"Signal sinusoïdal bruité\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "df731808",
   "metadata": {},
   "source": [
    "L'objectif est de régresser ce signal `y` à partir des valeurs de `x`. Pour cela, nous utilisons un arbre de régression."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "65c895f8",
   "metadata": {},
   "outputs": [],
   "source": [
    "# Apprendre le modèle\n",
    "reg = DecisionTreeRegressor(max_depth=2)\n",
    "reg.fit(X, y)\n",
    "\n",
    "# Prédiction sur la même plage de valeurs\n",
    "X_test = np.arange(0.0, 5.0, 0.01)[:, np.newaxis]\n",
    "y_pred = reg.predict(X_test)\n",
    "\n",
    "# Affichage des résultats\n",
    "plt.figure()\n",
    "plt.scatter(X, y, c=\"darkorange\", label=\"Exemples d'apprentissage\")\n",
    "plt.plot(X_test, y_pred, color=\"cornflowerblue\", label=\"Prédiction\", linewidth=2)\n",
    "plt.xlabel(\"x\")\n",
    "plt.ylabel(\"y\")\n",
    "plt.title(\"Régression par un arbre de décision\")\n",
    "plt.legend()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e62516c9",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Changer la valeur du parametre `max_depth`. Que se passe-t-il si on prend une valeur trop grande ? Trop petite ? Changer le taux d'éléménts affectés par le bruit (le `y[::5]`). Quand tous les éléments sont affectés par le bruit, faut-il préférer une valeur élevée ou faible pour `max_depth` ?\n",
    "\n",
    "</div>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "603db6a2",
   "metadata": {},
   "source": [
    "<div class=\"admonition-question admonition\">\n",
    "\n",
    "**Question**\n",
    "\n",
    "Pour approfondir, chargez la base de données Diabetes du module `sklearn.datasets` et faire une partition aléatoire en partie apprentissage et partie test (70% apprentissage, 30% test). Construire un modèle d'arbre de regression sur cette base. Calculer l'erreur quadratique moyenne sur l'ensemble de test. Faire un *grid search* pour trouver la valeur du paramètre `max_depth` qui minimize cette erreur.\n",
    "\n",
    "</div>"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
