{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "2321eccd",
   "metadata": {},
   "source": [
    "\n",
    "<a id='chap-tpapprentissagelargeechelle'></a>\n",
    "\n",
    "<a id='tpapprentissagelargeechellelabel'></a>"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ce57151e",
   "metadata": {},
   "source": [
    "# Travaux pratiques - Comparaison de classifieurs dans Spark ML\n",
    "\n",
    "Références externes utiles :\n",
    "\n",
    "> - [Documentation Spark ML](https://spark.apache.org/docs/latest/ml-guide.html)  \n",
    "- [Classification et régression dans Spark ML](https://spark.apache.org/docs/latest/ml-classification-regression.html)  \n",
    "- [Sélection de modèle dans Spark ML](https://spark.apache.org/docs/latest/ml-tuning.html)  \n",
    "- [Documentation API Spark en Python](https://spark.apache.org/docs/latest/api/python/index.html)  \n",
    "\n",
    "\n",
    "\n",
    "**L’objectif** de cette séance de TP est de comparer, sur un même jeu de données, quatre classifieurs disponibles dans Spark ML : la machine à vecteurs de support linéaire, la régression logistique, la forêt aléatoire et le perceptron multicouche (réseau de neurones). Pour chaque méthode nous construisons un *pipeline*, cherchons les valeurs optimales des hyperparamètres par validation croisée (avec *grid search*) et comparons les performances de généralisation sur un ensemble de test commun. La méthodologie est celle vue en cours ; il est utile de relire [la section sur les pipelines et la sélection de modèle](coursApprentissageLargeEchelle.html#apprentissage-supervise-avec-spark).\n",
    "\n",
    "Nous travaillons sur le jeu de données **Covertype** (couverture forestière), issu de l”[archive UCI](https://archive.ics.uci.edu/ml/datasets/Covertype). Il contient 581 012 observations décrites par 54 variables (10 quantitatives et 44 binaires issues du codage disjonctif de variables catégorielles), à classer en 7 types de couverture forestière."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "7e108db8",
   "metadata": {},
   "source": [
    "## Mise en place\n",
    "\n",
    "Pour préparer les répertoires et télécharger les données :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "9d125ef8",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import os, sys\n",
    "\n",
    "os.system(\"mkdir -p tpApprentissageLargeEchelle/data\")\n",
    "os.system(\"wget -q -nc https://www.csie.ntu.edu.tw/~cjlin/libsvmtools/datasets/multiclass/covtype.bz2 -P tpApprentissageLargeEchelle/data/\")\n",
    "os.system(\"bzip2 -dk tpApprentissageLargeEchelle/data/covtype.bz2\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2657e7b8",
   "metadata": {},
   "source": [
    "et après, pour créer la session Spark :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "2b58e2a2",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "os.environ['PYSPARK_PYTHON'] = sys.executable\n",
    "os.environ['PYSPARK_DRIVER_PYTHON'] = sys.executable\n",
    "\n",
    "from pyspark.sql import SparkSession\n",
    "spark = SparkSession.builder \\\n",
    "            .config(\"spark.driver.memory\", \"4g\") \\\n",
    "            .getOrCreate()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5609183a",
   "metadata": {},
   "source": [
    "## Lecture et préparation des données"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "69d31b1e",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Lecture des données en format libsvm\n",
    "donnees = spark.read.format(\"libsvm\") \\\n",
    "               .option(\"numFeatures\", \"54\") \\\n",
    "               .load(\"tpApprentissageLargeEchelle/data/covtype\")\n",
    "donnees.printSchema()\n",
    "donnees.show(3, truncate=False)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "3827e8de",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Distribution des classes\n",
    "donnees.groupBy(\"label\").count().orderBy(\"label\").show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d926ce87",
   "metadata": {},
   "source": [
    "## Question :\n",
    "\n",
    "Combien d’observations contient chaque classe ? La distribution est-elle équilibrée ? Qu’est-ce que cela implique pour l’évaluation des modèles ?\n",
    "\n",
    "Nous réduisons le nombre d’observations par échantillonnage stratifié pour maintenir des temps de calcul raisonnable et équilibrer la distribution des classes."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "5f0c8dc4",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.sql import functions as F\n",
    "\n",
    "# Taux d'échantillonnage par classe (environ 10 %)\n",
    "#fractions = {float(i): 0.10 for i in range(1, 8)}    # réduction,sans équilibrage\n",
    "#fractions = {1: 0.02, 2: 0.02, 3: 0.14, 4: 1, 5: 0.5, 6: 0.3, 7: 0.2}  # équilibrage, réduction\n",
    "fractions = {1: 0.01, 2: 0.01, 3: 0.07, 4: 0.9, 5: 0.25, 6: 0.15, 7: 0.1}  # équlibrage, plus forte réduction\n",
    "\n",
    "donnees_ech = donnees.stat.sampleBy(\"label\", fractions, seed=42).cache()\n",
    "\n",
    "print(f\"Taille de l'échantillon : {donnees_ech.count()} observations\")\n",
    "donnees_ech.groupBy(\"label\").count().orderBy(\"label\").show()"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31b8e716",
   "metadata": {},
   "source": [
    "## Note :\n",
    "\n",
    "Les étiquettes de classe dans Covertype sont entre 1 et 7, or Spark ML exige des étiquettes dans $ \\{0, \\ldots, K-1\\} $, il faut donc les décaler de 1 :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "afaa1c67",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "donnees_ech = donnees_ech.withColumn(\"label\", F.col(\"label\") - 1)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "e987a54c",
   "metadata": {},
   "source": [
    "Nous divisons ensuite le jeu en ensemble d’apprentissage (70 %) et ensemble de test (30 %). L’ensemble de test est mis de côté et ne sera utilisé qu’à la fin pour l’évaluation finale de chaque modèle."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "25ff62ab",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "partitions = donnees_ech.randomSplit([0.7, 0.3], seed=100)\n",
    "apprentissage = partitions[0].cache()\n",
    "test = partitions[1].cache()\n",
    "print(f\"Apprentissage : {apprentissage.count()} | Test : {test.count()}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "ea0c718d",
   "metadata": {},
   "source": [
    "## Standardisation des variables\n",
    "\n",
    "Les méthodes linéaires (SVM, régression logistique) sont sensibles à l’échelle des variables. Nous définissons un `StandardScaler` qui sera inclus dans tous les *pipelines* :"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "fa2d25d1",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.ml.feature import StandardScaler\n",
    "\n",
    "scaler = StandardScaler(inputCol=\"features\",\n",
    "                        outputCol=\"scaledFeatures\",\n",
    "                        withStd=True, withMean=True)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "83888aa6",
   "metadata": {},
   "source": [
    "## Question :\n",
    "\n",
    "Pourquoi applique-t-on la standardisation *à l’intérieur* du *pipeline* plutôt qu’avant de séparer apprentissage et test ? Quel problème introduirait une standardisation calculée sur la totalité des données ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2d9ba071",
   "metadata": {},
   "source": [
    "### Partie 1 : SVM linéaire"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0f661c49",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.ml import Pipeline\n",
    "from pyspark.ml.classification import LinearSVC, OneVsRest\n",
    "from pyspark.ml.tuning import CrossValidator, ParamGridBuilder\n",
    "from pyspark.ml.evaluation import MulticlassClassificationEvaluator\n",
    "import numpy as np"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "700c081e",
   "metadata": {},
   "source": [
    "Spark ML implémente `LinearSVC` pour la discrimination binaire uniquement. Pour le problème multiclasse à 7 classes, nous utilisons le schéma de décomposition **un contre tous** (*One-vs-Rest*, OvR) : 7 classifieurs binaires sont appris en parallèle, chacun distinguant une classe de toutes les autres. La classe retenue est celle dont le classifieur produit la valeur de décision la plus élevée."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "c1d631e2",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "svm = LinearSVC(featuresCol=\"scaledFeatures\", labelCol=\"label\",\n",
    "                maxIter=20)\n",
    "ovr_svm = OneVsRest(classifier=svm, labelCol=\"label\",\n",
    "                    featuresCol=\"scaledFeatures\",\n",
    "                    predictionCol=\"prediction\")\n",
    "\n",
    "pipeline_svm = Pipeline(stages=[scaler, ovr_svm])\n",
    "\n",
    "# Grille : valeurs de la constante de régularisation C\n",
    "# (regParam correspond à l'inverse de C pour LinearSVC dans Spark)\n",
    "grid_svm = ParamGridBuilder() \\\n",
    "               .addGrid(svm.regParam, [0.01, 0.1]) \\\n",
    "               .build()\n",
    "\n",
    "evaluateur = MulticlassClassificationEvaluator(\n",
    "                 labelCol=\"label\", predictionCol=\"prediction\",\n",
    "                 metricName=\"accuracy\")\n",
    "\n",
    "cv_svm = CrossValidator(estimator=pipeline_svm,\n",
    "                        estimatorParamMaps=grid_svm,\n",
    "                        evaluator=evaluateur,\n",
    "                        numFolds=5,\n",
    "                        seed=42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "31c2fd1a",
   "metadata": {},
   "source": [
    "### Question :\n",
    "\n",
    "Combien de classifieurs binaires sont appris au total pour cette étape de *grid search* avec validation croisée *5-fold* ?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "505e1d77",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "import time\n",
    "t0 = time.time()\n",
    "modele_svm = cv_svm.fit(apprentissage)\n",
    "t_svm = time.time() - t0\n",
    "print(f\"SVM - temps d'entraînement : {t_svm:.1f} s\")\n",
    "print(f\"Meilleur regParam : \"\n",
    "      f\"{modele_svm.getEstimatorParamMaps()[np.argmax(modele_svm.avgMetrics)][svm.regParam]}\")\n",
    "\n",
    "acc_svm = evaluateur.evaluate(modele_svm.transform(test))\n",
    "print(f\"SVM accuracy sur test : {acc_svm:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "78bbd391",
   "metadata": {},
   "source": [
    "### Partie 2 : Régression logistique\n",
    "\n",
    "La régression logistique multinomiale (*softmax*) est directement disponible pour les problèmes multiclasses dans Spark ML, sans schéma OvR."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "7223b564",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.ml.classification import LogisticRegression\n",
    "\n",
    "lr = LogisticRegression(featuresCol=\"scaledFeatures\", labelCol=\"label\",\n",
    "                        maxIter=50, family=\"multinomial\")\n",
    "\n",
    "pipeline_lr = Pipeline(stages=[scaler, lr])\n",
    "\n",
    "grid_lr = ParamGridBuilder() \\\n",
    "              .addGrid(lr.regParam,    [0.001, 0.01, 0.1]) \\\n",
    "              .addGrid(lr.elasticNetParam, [0.0, 0.5]) \\\n",
    "              .build()\n",
    "\n",
    "cv_lr = CrossValidator(estimator=pipeline_lr,\n",
    "                       estimatorParamMaps=grid_lr,\n",
    "                       evaluator=evaluateur,\n",
    "                       numFolds=5,\n",
    "                       seed=42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "6fb03c62",
   "metadata": {},
   "source": [
    "### Question :\n",
    "\n",
    "Le paramètre `elasticNetParam` de la régression logistique varie entre 0 et 1. Que représente-t-il ? A quoi correspondent les valeurs particulières 0,0 et 1,0 ?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "507b7a48",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "t0 = time.time()\n",
    "modele_lr = cv_lr.fit(apprentissage)\n",
    "t_lr = time.time() - t0\n",
    "print(f\"Régression logistique - temps d'entraînement : {t_lr:.1f} s\")\n",
    "\n",
    "best_lr = modele_lr.getEstimatorParamMaps()[np.argmax(modele_lr.avgMetrics)]\n",
    "print(f\"Meilleur regParam : {best_lr[lr.regParam]}, \"\n",
    "      f\"elasticNetParam : {best_lr[lr.elasticNetParam]}\")\n",
    "\n",
    "acc_lr = evaluateur.evaluate(modele_lr.transform(test))\n",
    "print(f\"Régression logistique accuracy sur test : {acc_lr:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "4ff01e0a",
   "metadata": {},
   "source": [
    "### Partie 3 : Forêt aléatoire\n",
    "\n",
    "La forêt aléatoire est une méthode d’ensemble qui agrège les prédictions d’un grand nombre d’arbres de décision appris sur des sous-échantillons des données et des sous-ensembles de variables. Les arbres de décision ne nécessitent pas de standardisation préalable."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "e5f18b7a",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.ml.classification import RandomForestClassifier\n",
    "\n",
    "rf = RandomForestClassifier(featuresCol=\"features\", labelCol=\"label\",\n",
    "                            numTrees=50, seed=42)\n",
    "\n",
    "# Pas de standardisation nécessaire pour la forêt aléatoire\n",
    "pipeline_rf = Pipeline(stages=[rf])\n",
    "\n",
    "grid_rf = ParamGridBuilder() \\\n",
    "              .addGrid(rf.maxDepth,  [5, 10]) \\\n",
    "              .addGrid(rf.numTrees,  [50, 100]) \\\n",
    "              .build()\n",
    "\n",
    "cv_rf = CrossValidator(estimator=pipeline_rf,\n",
    "                       estimatorParamMaps=grid_rf,\n",
    "                       evaluator=evaluateur,\n",
    "                       numFolds=5,\n",
    "                       seed=42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "61e440f9",
   "metadata": {},
   "source": [
    "### Question :\n",
    "\n",
    "Quel est l’impact des hyperparamètres `maxDepth` et `numTrees` sur la capacité de généralisation du modèle ?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "0098e0a6",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "t0 = time.time()\n",
    "modele_rf = cv_rf.fit(apprentissage)\n",
    "t_rf = time.time() - t0\n",
    "print(f\"Forêt aléatoire - temps d'entraînement : {t_rf:.1f} s\")\n",
    "\n",
    "best_rf = modele_rf.getEstimatorParamMaps()[np.argmax(modele_rf.avgMetrics)]\n",
    "print(f\"Meilleur maxDepth : {best_rf[rf.maxDepth]}, \"\n",
    "      f\"numTrees : {best_rf[rf.numTrees]}\")\n",
    "\n",
    "acc_rf = evaluateur.evaluate(modele_rf.transform(test))\n",
    "print(f\"Forêt aléatoire accuracy sur test : {acc_rf:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8f1a188f",
   "metadata": {},
   "source": [
    "### Partie 4 : Réseau de neurones (MLP)\n",
    "\n",
    "Spark ML propose un perceptron multicouche (*Multilayer Perceptron Classifier*, MLP). L’architecture du réseau est définie par le vecteur `layers` : le premier élément est la dimension d’entrée (54 variables), le dernier est le nombre de classes (7), les éléments intermédiaires sont les dimensions des couches cachées.\n",
    "\n",
    "Attention, le temps de calcul est beaucoup plus long ici, il est envisageable d’éviter cet apprentissage durant le TP."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "f3d32fcc",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "from pyspark.ml.classification import MultilayerPerceptronClassifier\n",
    "\n",
    "mlp = MultilayerPerceptronClassifier(\n",
    "          featuresCol=\"scaledFeatures\", labelCol=\"label\",\n",
    "          layers=[54, 64, 32, 7],\n",
    "          maxIter=100, seed=42)\n",
    "\n",
    "pipeline_mlp = Pipeline(stages=[scaler, mlp])\n",
    "\n",
    "grid_mlp = ParamGridBuilder() \\\n",
    "               .addGrid(mlp.layers, [[54, 64, 32, 7],\n",
    "                                     [54, 128, 64, 7]]) \\\n",
    "               .addGrid(mlp.stepSize, [0.01, 0.05]) \\\n",
    "               .build()\n",
    "\n",
    "cv_mlp = CrossValidator(estimator=pipeline_mlp,\n",
    "                        estimatorParamMaps=grid_mlp,\n",
    "                        evaluator=evaluateur,\n",
    "                        numFolds=5,\n",
    "                        seed=42)"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "768904f0",
   "metadata": {},
   "source": [
    "### Question :\n",
    "\n",
    "Calculez le nombre de paramètres (poids et biais) de l’architecture `[54, 64, 32, 7]`. Comparez à celui de la régression logistique multinomiale pour le même problème. Qu’en déduisez-vous sur le risque de sur-apprentissage et sur le volume de données nécessaire ?"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "d800b785",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "t0 = time.time()\n",
    "modele_mlp = cv_mlp.fit(apprentissage)\n",
    "t_mlp = time.time() - t0\n",
    "print(f\"MLP - temps d'entraînement : {t_mlp:.1f} s\")\n",
    "\n",
    "best_mlp = modele_mlp.getEstimatorParamMaps()[np.argmax(modele_mlp.avgMetrics)]\n",
    "print(f\"Meilleur layers : {best_mlp[mlp.layers]}, \"\n",
    "      f\"stepSize : {best_mlp[mlp.stepSize]}\")\n",
    "\n",
    "acc_mlp = evaluateur.evaluate(modele_mlp.transform(test))\n",
    "print(f\"MLP accuracy sur test : {acc_mlp:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "b99f04be",
   "metadata": {},
   "source": [
    "### Partie 5 : Comparaison et analyse"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "8c53adab",
   "metadata": {},
   "source": [
    "## Tableau récapitulatif"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "16c7d80c",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "resultats = {\n",
    "    \"SVM (OvR)\":             (acc_svm,  t_svm),\n",
    "    \"Régression logistique\": (acc_lr,   t_lr),\n",
    "    \"Forêt aléatoire\":       (acc_rf,   t_rf),\n",
    "    \"MLP\":                   (acc_mlp,  t_mlp),\n",
    "}\n",
    "\n",
    "print(f\"{'Méthode':<25} {'Accuracy (test)':>16} {'Temps entr. (s)':>16}\")\n",
    "print(\"-\" * 58)\n",
    "for nom, (acc, t) in sorted(resultats.items(), key=lambda x: -x[1][0]):\n",
    "    print(f\"{nom:<25} {acc:>16.4f} {t:>16.1f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "2fbffd1f",
   "metadata": {},
   "source": [
    "## Analyse par classe\n",
    "\n",
    "\n",
    "<dl style='margin: 20px 0;'>\n",
    "<dt>Les classes présentes sont :</dt>\n",
    "<dd>\n",
    "- Classe 0 : Spruce/Fir (épicea/sapin)  \n",
    "- Classe 1 : Lodgepole Pine (pin tordu)  \n",
    "- Classe 2 : Ponderosa Pine (pin ponderosa)  \n",
    "- Classe 3 : Cottonwood/Willow (peuplier/saule))  \n",
    "- Classe 4 : Aspen (peuplier faux-tremble))  \n",
    "- Classe 5 : Douglas-fir (Sapin Douglas)  \n",
    "- Classe 6 : Krummholz (arbres nains)  \n",
    "\n",
    "\n",
    "</dd>\n",
    "\n",
    "</dl>\n",
    "\n",
    "Un taux de bonne classification global masque des différences importantes selon les classes, surtout sur un jeu déséquilibré. Nous calculons ici la précision et le rappel par classe pour la meilleure méthode."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ca890430",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# À remplacer par le meilleur modèle selon les résultats obtenus\n",
    "meilleur_modele = modele_rf    # à adapter\n",
    "\n",
    "predictions = meilleur_modele.transform(test)\n",
    "\n",
    "# Évaluation par classe\n",
    "for metrique in [\"precisionByLabel\", \"recallByLabel\", \"fMeasureByLabel\"]:\n",
    "    eval_mc = MulticlassClassificationEvaluator(\n",
    "                  labelCol=\"label\", predictionCol=\"prediction\",\n",
    "                  metricName=metrique)\n",
    "    valeurs = eval_mc.evaluate(predictions)\n",
    "    print(f\"{metrique} : {valeurs:.4f}\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "b911063c",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Matrice de confusion\n",
    "from pyspark.mllib.evaluation import MulticlassMetrics\n",
    "\n",
    "preds_et_labels = predictions.select(\"prediction\", \"label\") \\\n",
    "                              .rdd.map(lambda r: (r.prediction, float(r.label)))\n",
    "metrics = MulticlassMetrics(preds_et_labels)\n",
    "print(\"Matrice de confusion :\")\n",
    "print(metrics.confusionMatrix().toArray().astype(int))"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "98fbc6f1",
   "metadata": {},
   "source": [
    "## Question :\n",
    "\n",
    "Quelles sont les classes les plus difficiles à prédire ? Ces difficultés sont-elles symétriques (deux classes se confondent mutuellement) ou asymétriques ? Quelle hypothèse cela suggère-t-il sur la structure des données pour ces classes ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "82e393b3",
   "metadata": {},
   "source": [
    "## Question (bilan) :\n",
    "\n",
    "Comparez les quatre méthodes selon trois critères : *accuracy* sur les données de test, temps d’entraînement et interprétabilité du modèle. Y a-t-il une méthode qui domine sur tous les critères ? Comment le choix dépend-il des contraintes applicatives (contrainte de temps réel, besoin d’explication des décisions, volume de données disponible) ?"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "fab6de55",
   "metadata": {},
   "source": [
    "## Importance des variables (forêt aléatoire)\n",
    "\n",
    "La forêt aléatoire fournit une mesure d’importance des variables qui est utile pour comprendre quelles variables contribuent le plus à la discrimination."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "id": "ff7dd437",
   "metadata": {
    "hide-output": false
   },
   "outputs": [],
   "source": [
    "# Récupération du modèle RandomForest depuis le meilleur pipeline\n",
    "rf_model = modele_rf.bestModel.stages[-1]\n",
    "importances = rf_model.featureImportances.toArray()\n",
    "\n",
    "# Affichage des 10 variables les plus importantes\n",
    "top10 = np.argsort(importances)[::-1][:10]\n",
    "print(\"Top 10 variables les plus importantes :\")\n",
    "for rang, idx in enumerate(top10, 1):\n",
    "    print(f\"  {rang:2d}. variable {idx:3d} : importance = {importances[idx]:.4f}\")"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "a1e1920d",
   "metadata": {},
   "source": [
    "## Question :\n",
    "\n",
    "Les variables les plus importantes sont-elles parmi les 10 variables quantitatives initiales (indices 0 à 9) ou parmi les variables binaires issues du codage disjonctif (indices 10 à 53) ? Qu’est-ce que cela suggère sur l’utilité respective des deux types de variables pour ce problème ?"
   ]
  }
 ],
 "metadata": {
  "date": 1791209401.1648316,
  "filename": "tpApprentissageLargeEchelle.rst",
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python"
  },
  "title": "Travaux pratiques - Comparaison de classifieurs dans Spark ML"
 },
 "nbformat": 4,
 "nbformat_minor": 5
}