{
 "cells": [
  {
   "cell_type": "markdown",
   "id": "7afa1a06",
   "metadata": {
    "id": "7afa1a06"
   },
   "source": [
    "**Projet UE8 : tester de nouvelles couches de données (hors rendu)**\n",
    "\n",
    "Notebook séparé pour ne pas toucher au rendu. Il relit les ventes nettoyées (`data/alpes_terrain.csv`, produites par le notebook principal) et compare le Random Forest réglé (200 arbres, 4 variables par embranchement) en validation croisée (10 plis), sans puis avec chaque nouvelle couche.\n",
    "\n",
    "Couches testées, une par une :\n",
    "1. Prix des ventes voisines : testé hors Colab le 09/10, gain négligeable (43,6 -> 42,9 €/m²).\n",
    "2. Distance aux remontées mécaniques (OpenStreetMap).\n",
    "3. Zonage du PLU (Géoportail de l'urbanisme, API Carto IGN).\n",
    "4. Parcelle cadastrale, bâti et ensoleillement : voir `Projet_UE8_couches_test_2.ipynb`."
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d91d232d",
   "metadata": {
    "id": "d91d232d"
   },
   "source": [
    "# Setup"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 1,
   "id": "fe7b4b16",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "fe7b4b16",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791536978616,
     "user_tz": -120,
     "elapsed": 16997,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "eb72c6d2-079b-40cd-cf28-630430e0c42a"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Mounted at /content/drive\n",
      "Dossier : /content/drive/MyDrive/Colab Notebooks/Projet Habitat 05\n"
     ]
    }
   ],
   "source": [
    "from google.colab import drive\n",
    "drive.mount('/content/drive')\n",
    "\n",
    "import os, time, json, requests\n",
    "import numpy as np, pandas as pd\n",
    "import geopandas as gpd\n",
    "from shapely.geometry import LineString\n",
    "from sklearn.ensemble import RandomForestRegressor\n",
    "from sklearn.model_selection import KFold\n",
    "\n",
    "PROJECT_ROOT_DIR = \"/content/drive/MyDrive/Colab Notebooks/Projet Habitat 05\"\n",
    "os.chdir(PROJECT_ROOT_DIR)\n",
    "assert os.path.exists(os.path.join(\"data\", \"alpes_terrain.csv\")), \"⚠️ data/alpes_terrain.csv introuvable : relancer le notebook principal\"\n",
    "print(\"Dossier :\", os.getcwd())"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "feedcf29",
   "metadata": {
    "id": "feedcf29"
   },
   "source": [
    "## Ventes et variables de base\n",
    "\n",
    "Mêmes variables que le modèle principal. `nb_ventes_commune` est recalculé à l'intérieur de chaque pli, sur l'entraînement seulement (pas de fuite)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 2,
   "id": "301b41bb",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 98
    },
    "id": "301b41bb",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791536999415,
     "user_tz": -120,
     "elapsed": 20786,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "5c7981d2-ab41-43ec-b734-d686313a5a32"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "4137 ventes géolocalisées\n"
     ]
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "           RMSE   MAE Erreur relative médiane Ventes à moins de 15 %  \\\n",
       "Référence  43.3  25.9                     14%                    52%   \n",
       "\n",
       "          RMSE stations  \n",
       "Référence          99.4  "
      ],
      "text/html": [
       "\n",
       "  <div id=\"df-f617bfe5-d878-4fc9-91d1-33a2425dcbe5\" class=\"colab-df-container\">\n",
       "    <div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>RMSE</th>\n",
       "      <th>MAE</th>\n",
       "      <th>Erreur relative médiane</th>\n",
       "      <th>Ventes à moins de 15 %</th>\n",
       "      <th>RMSE stations</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Référence</th>\n",
       "      <td>43.3</td>\n",
       "      <td>25.9</td>\n",
       "      <td>14%</td>\n",
       "      <td>52%</td>\n",
       "      <td>99.4</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>\n",
       "    <div class=\"colab-df-buttons\">\n",
       "\n",
       "  <div class=\"colab-df-container\">\n",
       "    <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-f617bfe5-d878-4fc9-91d1-33a2425dcbe5')\"\n",
       "            title=\"Convert this dataframe to an interactive table.\"\n",
       "            style=\"display:none;\">\n",
       "\n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
       "    <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
       "  </svg>\n",
       "    </button>\n",
       "\n",
       "  <style>\n",
       "    .colab-df-container {\n",
       "      display:flex;\n",
       "      gap: 12px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert {\n",
       "      background-color: #E8F0FE;\n",
       "      border: none;\n",
       "      border-radius: 50%;\n",
       "      cursor: pointer;\n",
       "      display: none;\n",
       "      fill: #1967D2;\n",
       "      height: 32px;\n",
       "      padding: 0 0 0 0;\n",
       "      width: 32px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert:hover {\n",
       "      background-color: #E2EBFA;\n",
       "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "      fill: #174EA6;\n",
       "    }\n",
       "\n",
       "    .colab-df-buttons div {\n",
       "      margin-bottom: 4px;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert {\n",
       "      background-color: #3B4455;\n",
       "      fill: #D2E3FC;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert:hover {\n",
       "      background-color: #434B5C;\n",
       "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "      fill: #FFFFFF;\n",
       "    }\n",
       "  </style>\n",
       "\n",
       "    <script>\n",
       "      const buttonEl =\n",
       "        document.querySelector('#df-f617bfe5-d878-4fc9-91d1-33a2425dcbe5 button.colab-df-convert');\n",
       "      buttonEl.style.display =\n",
       "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "      async function convertToInteractive(key) {\n",
       "        const element = document.querySelector('#df-f617bfe5-d878-4fc9-91d1-33a2425dcbe5');\n",
       "        const dataTable =\n",
       "          await google.colab.kernel.invokeFunction('convertToInteractive',\n",
       "                                                    [key], {});\n",
       "        if (!dataTable) return;\n",
       "\n",
       "        const docLinkHtml = 'Like what you see? Visit the ' +\n",
       "          '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
       "          + ' to learn more about interactive tables.';\n",
       "        element.innerHTML = '';\n",
       "        dataTable['output_type'] = 'display_data';\n",
       "        await google.colab.output.renderOutput(dataTable, element);\n",
       "        const docLink = document.createElement('div');\n",
       "        docLink.innerHTML = docLinkHtml;\n",
       "        element.appendChild(docLink);\n",
       "      }\n",
       "    </script>\n",
       "  </div>\n",
       "\n",
       "    </div>\n",
       "  </div>\n"
      ],
      "application/vnd.google.colaboratory.intrinsic+json": {
       "type": "dataframe",
       "summary": "{\n  \"name\": \"pd\",\n  \"rows\": 1,\n  \"fields\": [\n    {\n      \"column\": \"RMSE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 43.3,\n        \"max\": 43.3,\n        \"num_unique_values\": 1,\n        \"samples\": [\n          43.3\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"MAE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 25.9,\n        \"max\": 25.9,\n        \"num_unique_values\": 1,\n        \"samples\": [\n          25.9\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Erreur relative m\\u00e9diane\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 1,\n        \"samples\": [\n          \"14%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Ventes \\u00e0 moins de 15 %\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 1,\n        \"samples\": [\n          \"52%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"RMSE stations\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 99.4,\n        \"max\": 99.4,\n        \"num_unique_values\": 1,\n        \"samples\": [\n          99.4\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    }\n  ]\n}"
      }
     },
     "metadata": {},
     "execution_count": 2
    }
   ],
   "source": [
    "df = pd.read_csv(os.path.join(\"data\", \"alpes_terrain.csv\"),\n",
    "                 dtype={\"code_departement\": str, \"code_commune\": str}).dropna(subset=[\"longitude\"]).reset_index(drop=True)\n",
    "print(len(df), \"ventes géolocalisées\")\n",
    "\n",
    "def km(lon1, lat1, lon2, lat2):   # distance à vol d'oiseau (haversine)\n",
    "    lon1, lat1, lon2, lat2 = map(np.radians, [lon1, lat1, lon2, lat2])\n",
    "    a = np.sin((lat2 - lat1) / 2) ** 2 + np.cos(lat1) * np.cos(lat2) * np.sin((lon2 - lon1) / 2) ** 2\n",
    "    return 6371 * 2 * np.arcsin(np.sqrt(a))\n",
    "\n",
    "PREFECTURES = {\"04\": (6.2357, 44.0925), \"05\": (6.0794, 44.5594)}\n",
    "POLES = [(6.0616, 44.5797), (5.7896, 43.8293), (6.2495, 44.0954), (6.6536, 44.8995)]   # Gap, Manosque, Digne, Briançon\n",
    "df[\"dist_prefecture_km\"] = [km(lo, la, *PREFECTURES[d]) for lo, la, d in zip(df[\"longitude\"], df[\"latitude\"], df[\"code_departement\"])]\n",
    "df[\"dist_pole_km\"] = np.min([km(df[\"longitude\"], df[\"latitude\"], *p) for p in POLES], axis=0)\n",
    "df[\"log_population\"] = np.log(df[\"population_commune\"])\n",
    "df[\"log_surface\"] = np.log(df[\"surface\"])\n",
    "\n",
    "BASE = [\"annee\", \"longitude\", \"latitude\", \"surface\", \"part_AB\", \"population_commune\", \"altitude\", \"pente_pct\",\n",
    "        \"orientation_sud\", \"dist_cours_eau_m\", \"dist_prefecture_km\", \"dist_pole_km\", \"log_population\", \"log_surface\"]\n",
    "CATEGORIES = [\"code_departement\", \"type_mixite\", \"nom_epci\"]\n",
    "y = df[\"prix_m2\"].values\n",
    "STATIONS = [\"Montgenèvre\", \"Briançon\", \"Saint-Chaffrey\", \"La Salle-les-Alpes\", \"Le Monêtier-les-Bains\",\n",
    "            \"Vars\", \"Risoul\", \"Les Orres\", \"Orcières\", \"Dévoluy\"]\n",
    "en_station = df[\"nom_commune\"].isin(STATIONS).values\n",
    "\n",
    "def evaluer(numeriques=[], categories=[]):\n",
    "    \"\"\"Validation croisée 10 plis du Random Forest réglé, avec les colonnes ajoutées.\"\"\"\n",
    "    X = pd.concat([df[BASE + numeriques], pd.get_dummies(df[CATEGORIES + categories]).astype(int)], axis=1)\n",
    "    pred = np.zeros(len(y))\n",
    "    for tr, te in KFold(n_splits=10, shuffle=True, random_state=42).split(X):\n",
    "        Xtr, Xte = X.iloc[tr].copy(), X.iloc[te].copy()\n",
    "        nb = df.iloc[tr].groupby(\"code_commune\").size()\n",
    "        Xtr[\"nb_ventes_commune\"] = df[\"code_commune\"].iloc[tr].map(nb).values\n",
    "        Xte[\"nb_ventes_commune\"] = df[\"code_commune\"].iloc[te].map(nb).fillna(0).values\n",
    "        rf = RandomForestRegressor(n_estimators=200, max_features=4, random_state=42, n_jobs=-1).fit(Xtr, y[tr])\n",
    "        pred[te] = rf.predict(Xte)\n",
    "    err, rel = pred - y, np.abs(pred - y) / y\n",
    "    return {\"RMSE\": round(np.sqrt((err ** 2).mean()), 1), \"MAE\": round(np.abs(err).mean(), 1),\n",
    "            \"Erreur relative médiane\": f\"{np.median(rel):.0%}\", \"Ventes à moins de 15 %\": f\"{(rel < 0.15).mean():.0%}\",\n",
    "            \"RMSE stations\": round(np.sqrt((err[en_station] ** 2).mean()), 1)}\n",
    "\n",
    "resultats = {\"Référence\": evaluer()}\n",
    "pd.DataFrame(resultats).T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "5b010c3c",
   "metadata": {
    "id": "5b010c3c"
   },
   "source": [
    "## Couche 2 : distance aux remontées mécaniques (OpenStreetMap)\n",
    "\n",
    "Toutes les remontées (télésièges, télécabines, téléskis...) du 04 et du 05, en une seule requête à l'API Overpass, mises en cache dans `data/remontees_osm.geojson`.\n",
    "Deux variables : distance à la remontée la plus proche (en mètres) et nombre de remontées à moins de 5 km (taille du domaine skiable)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 4,
   "id": "63b95fea",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "63b95fea",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791537114603,
     "user_tz": -120,
     "elapsed": 2808,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "b0778c75-2291-4c68-e16c-2dd75b468e4c"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "Réponse de https://overpass-api.de/api/interpreter\n",
      "825 remontées mécaniques\n",
      "type\n",
      "platter         281\n",
      "chair_lift      224\n",
      "magic_carpet    117\n",
      "drag_lift       106\n",
      "gondola          43\n",
      "rope_tow         17\n",
      "mixed_lift       16\n",
      "cable_car        10\n",
      "t-bar             7\n",
      "j-bar             4\n",
      "Name: count, dtype: int64\n"
     ]
    }
   ],
   "source": [
    "REMONTEES_PATH = os.path.join(\"data\", \"remontees_osm.geojson\")\n",
    "OVERPASS = [\"https://overpass-api.de/api/interpreter\",\n",
    "            \"https://maps.mail.ru/osm/tools/overpass/api/interpreter\",\n",
    "            \"https://overpass.kumi.systems/api/interpreter\"]\n",
    "ENTETES = {\"User-Agent\": \"Projet-UE8-MBA-terrains-Alpes/1.0 (Google Colab)\", \"Accept\": \"application/json\"}\n",
    "TYPES = \"chair_lift|gondola|cable_car|mixed_lift|drag_lift|t-bar|j-bar|platter|rope_tow|magic_carpet\"\n",
    "BBOX = \"43.66,5.49,45.13,7.08\"   # sud, ouest, nord, est : 04 + 05\n",
    "\n",
    "if os.path.exists(REMONTEES_PATH):\n",
    "    remontees = gpd.read_file(REMONTEES_PATH)\n",
    "else:\n",
    "    requete = f'[out:json][timeout:180];way[\"aerialway\"~\"^({TYPES})$\"]({BBOX});out geom;'\n",
    "    data = None\n",
    "    for essai in range(2):                       # 2 tours sur les 3 serveurs\n",
    "        for url in OVERPASS:\n",
    "            try:\n",
    "                r = requests.post(url, data={\"data\": requete}, headers=ENTETES, timeout=200)\n",
    "                r.raise_for_status()\n",
    "                data = r.json()\n",
    "                print(\"Réponse de\", url)\n",
    "                break\n",
    "            except Exception as e:\n",
    "                print(f\"⚠️ Échec sur {url} : {e}\")\n",
    "        if data is not None:\n",
    "            break\n",
    "        print(\"Nouvel essai dans 60 s...\")\n",
    "        time.sleep(60)\n",
    "    if data is None:\n",
    "        raise RuntimeError(\"⚠️ Aucun serveur Overpass n'a répondu : réessayer dans quelques minutes\")\n",
    "    lignes = [{\"type\": w[\"tags\"].get(\"aerialway\"), \"nom\": w[\"tags\"].get(\"name\"),\n",
    "               \"geometry\": LineString([(p[\"lon\"], p[\"lat\"]) for p in w[\"geometry\"]])}\n",
    "              for w in data[\"elements\"] if len(w.get(\"geometry\", [])) >= 2]\n",
    "    if not lignes:\n",
    "        raise RuntimeError(\"⚠️ Réponse reçue mais aucune remontée trouvée : vérifier la zone (BBOX)\")\n",
    "    remontees = gpd.GeoDataFrame(lignes, crs=\"EPSG:4326\")\n",
    "    remontees.to_file(REMONTEES_PATH, driver=\"GeoJSON\")\n",
    "\n",
    "print(len(remontees), \"remontées mécaniques\")\n",
    "print(remontees[\"type\"].value_counts())"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 5,
   "id": "c44ffe1d",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 442
    },
    "id": "c44ffe1d",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791537151736,
     "user_tz": -120,
     "elapsed": 20775,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "69f111f7-20f0-48f1-c4b8-a4b0fdecea3d"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "       dist_remontee_m  nb_remontees_5km\n",
      "count           4137.0            4137.0\n",
      "mean           13741.0               4.0\n",
      "std            11705.0               9.0\n",
      "min               25.0               0.0\n",
      "25%             4213.0               0.0\n",
      "50%             8890.0               0.0\n",
      "75%            21175.0               5.0\n",
      "max            50736.0              59.0\n",
      "\n",
      "Prix médian selon la distance à la remontée la plus proche :\n",
      "                      ventes  prix_median\n",
      "dist_remontee_m                          \n",
      "(0.0, 500.0]             254        136.0\n",
      "(500.0, 1000.0]          172        154.0\n",
      "(1000.0, 2000.0]         182        109.0\n",
      "(2000.0, 5000.0]         572        111.0\n",
      "(5000.0, 10000.0]       1046        116.0\n",
      "(10000.0, 1000000.0]    1911        108.0\n"
     ]
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "                        RMSE   MAE Erreur relative médiane  \\\n",
       "Référence               43.3  25.9                     14%   \n",
       "+ remontées mécaniques  42.9  25.5                     14%   \n",
       "\n",
       "                       Ventes à moins de 15 % RMSE stations  \n",
       "Référence                                 52%          99.4  \n",
       "+ remontées mécaniques                    53%          99.5  "
      ],
      "text/html": [
       "\n",
       "  <div id=\"df-0b979ab9-b86c-4f33-9c10-22ee7b2e5a40\" class=\"colab-df-container\">\n",
       "    <div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>RMSE</th>\n",
       "      <th>MAE</th>\n",
       "      <th>Erreur relative médiane</th>\n",
       "      <th>Ventes à moins de 15 %</th>\n",
       "      <th>RMSE stations</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Référence</th>\n",
       "      <td>43.3</td>\n",
       "      <td>25.9</td>\n",
       "      <td>14%</td>\n",
       "      <td>52%</td>\n",
       "      <td>99.4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>+ remontées mécaniques</th>\n",
       "      <td>42.9</td>\n",
       "      <td>25.5</td>\n",
       "      <td>14%</td>\n",
       "      <td>53%</td>\n",
       "      <td>99.5</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>\n",
       "    <div class=\"colab-df-buttons\">\n",
       "\n",
       "  <div class=\"colab-df-container\">\n",
       "    <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-0b979ab9-b86c-4f33-9c10-22ee7b2e5a40')\"\n",
       "            title=\"Convert this dataframe to an interactive table.\"\n",
       "            style=\"display:none;\">\n",
       "\n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
       "    <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
       "  </svg>\n",
       "    </button>\n",
       "\n",
       "  <style>\n",
       "    .colab-df-container {\n",
       "      display:flex;\n",
       "      gap: 12px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert {\n",
       "      background-color: #E8F0FE;\n",
       "      border: none;\n",
       "      border-radius: 50%;\n",
       "      cursor: pointer;\n",
       "      display: none;\n",
       "      fill: #1967D2;\n",
       "      height: 32px;\n",
       "      padding: 0 0 0 0;\n",
       "      width: 32px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert:hover {\n",
       "      background-color: #E2EBFA;\n",
       "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "      fill: #174EA6;\n",
       "    }\n",
       "\n",
       "    .colab-df-buttons div {\n",
       "      margin-bottom: 4px;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert {\n",
       "      background-color: #3B4455;\n",
       "      fill: #D2E3FC;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert:hover {\n",
       "      background-color: #434B5C;\n",
       "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "      fill: #FFFFFF;\n",
       "    }\n",
       "  </style>\n",
       "\n",
       "    <script>\n",
       "      const buttonEl =\n",
       "        document.querySelector('#df-0b979ab9-b86c-4f33-9c10-22ee7b2e5a40 button.colab-df-convert');\n",
       "      buttonEl.style.display =\n",
       "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "      async function convertToInteractive(key) {\n",
       "        const element = document.querySelector('#df-0b979ab9-b86c-4f33-9c10-22ee7b2e5a40');\n",
       "        const dataTable =\n",
       "          await google.colab.kernel.invokeFunction('convertToInteractive',\n",
       "                                                    [key], {});\n",
       "        if (!dataTable) return;\n",
       "\n",
       "        const docLinkHtml = 'Like what you see? Visit the ' +\n",
       "          '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
       "          + ' to learn more about interactive tables.';\n",
       "        element.innerHTML = '';\n",
       "        dataTable['output_type'] = 'display_data';\n",
       "        await google.colab.output.renderOutput(dataTable, element);\n",
       "        const docLink = document.createElement('div');\n",
       "        docLink.innerHTML = docLinkHtml;\n",
       "        element.appendChild(docLink);\n",
       "      }\n",
       "    </script>\n",
       "  </div>\n",
       "\n",
       "    </div>\n",
       "  </div>\n"
      ],
      "application/vnd.google.colaboratory.intrinsic+json": {
       "type": "dataframe",
       "summary": "{\n  \"name\": \"pd\",\n  \"rows\": 2,\n  \"fields\": [\n    {\n      \"column\": \"RMSE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 42.9,\n        \"max\": 43.3,\n        \"num_unique_values\": 2,\n        \"samples\": [\n          42.9,\n          43.3\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"MAE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 25.5,\n        \"max\": 25.9,\n        \"num_unique_values\": 2,\n        \"samples\": [\n          25.5,\n          25.9\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Erreur relative m\\u00e9diane\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 1,\n        \"samples\": [\n          \"14%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Ventes \\u00e0 moins de 15 %\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 2,\n        \"samples\": [\n          \"53%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"RMSE stations\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 99.4,\n        \"max\": 99.5,\n        \"num_unique_values\": 2,\n        \"samples\": [\n          99.5\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    }\n  ]\n}"
      }
     },
     "metadata": {},
     "execution_count": 5
    }
   ],
   "source": [
    "# Distances en mètres : on passe en Lambert 93 (EPSG:2154), le système métrique officiel en France\n",
    "points = gpd.GeoDataFrame(df[[\"longitude\", \"latitude\"]], geometry=gpd.points_from_xy(df[\"longitude\"], df[\"latitude\"]),\n",
    "                          crs=\"EPSG:4326\").to_crs(\"EPSG:2154\")\n",
    "lignes_l93 = remontees.to_crs(\"EPSG:2154\")\n",
    "\n",
    "proche = gpd.sjoin_nearest(points, lignes_l93[[\"geometry\"]], distance_col=\"d\")\n",
    "df[\"dist_remontee_m\"] = proche.groupby(level=0)[\"d\"].min()\n",
    "tampon = points.copy(); tampon[\"geometry\"] = points.buffer(5000)\n",
    "df[\"nb_remontees_5km\"] = gpd.sjoin(tampon, lignes_l93[[\"geometry\"]], predicate=\"intersects\").groupby(level=0).size()\n",
    "df[\"nb_remontees_5km\"] = df[\"nb_remontees_5km\"].fillna(0)\n",
    "\n",
    "print(df[[\"dist_remontee_m\", \"nb_remontees_5km\"]].describe().round())\n",
    "print(\"\\nPrix médian selon la distance à la remontée la plus proche :\")\n",
    "print(df.groupby(pd.cut(df[\"dist_remontee_m\"], [0, 500, 1000, 2000, 5000, 10000, 1e6]), observed=True)[\"prix_m2\"]\n",
    "        .agg(ventes=\"count\", prix_median=\"median\").round())\n",
    "\n",
    "resultats[\"+ remontées mécaniques\"] = evaluer([\"dist_remontee_m\", \"nb_remontees_5km\"])\n",
    "pd.DataFrame(resultats).T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "d533fc2b",
   "metadata": {
    "id": "d533fc2b"
   },
   "source": [
    "## Couche 3 : zonage du PLU (Géoportail de l'urbanisme)\n",
    "\n",
    "Pour chaque vente, on demande à l'API Carto de l'IGN la zone d'urbanisme à cet endroit :\n",
    "- **PLU** : U (urbanisé), AU (à urbaniser), A (agricole), N (naturel) ;\n",
    "- **carte communale** : secteur constructible ou non ;\n",
    "- rien : la commune est au **règlement national d'urbanisme** (RNU).\n",
    "\n",
    "⚠️ C'est le zonage **actuel**, pas celui de la date de la vente. Environ 4 100 requêtes, mises en cache dans `data/plu_cache.csv` (compter 15 à 30 minutes au premier lancement ; si la cellule s'arrête, la relancer : elle reprend où elle en était)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 9,
   "id": "bff63b44",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/"
    },
    "id": "bff63b44",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791539026792,
     "user_tz": -120,
     "elapsed": 685,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "d26746af-7e7f-4454-8529-b5aa4dd3639a"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "1 points à interroger\n",
      "1/1 points, 0 échecs\n"
     ]
    }
   ],
   "source": [
    "PLU_PATH = os.path.join(\"data\", \"plu_cache.csv\")\n",
    "APICARTO = \"https://apicarto.ign.fr/api/gpu\"\n",
    "\n",
    "def zone_en(lon, lat, session):\n",
    "    geom = json.dumps({\"type\": \"Point\", \"coordinates\": [lon, lat]})\n",
    "    r = session.get(f\"{APICARTO}/zone-urba\", params={\"geom\": geom}, timeout=30)\n",
    "    r.raise_for_status()\n",
    "    f = r.json().get(\"features\", [])\n",
    "    if f:\n",
    "        return \"PLU_\" + str(f[0][\"properties\"].get(\"typezone\", \"?\"))\n",
    "    r = session.get(f\"{APICARTO}/secteur-cc\", params={\"geom\": geom}, timeout=30)\n",
    "    r.raise_for_status()\n",
    "    f = r.json().get(\"features\", [])\n",
    "    if f:\n",
    "        return \"CC_\" + str(f[0][\"properties\"].get(\"typesect\", \"?\"))\n",
    "    return \"RNU\"\n",
    "\n",
    "cache = pd.read_csv(PLU_PATH) if os.path.exists(PLU_PATH) else pd.DataFrame(columns=[\"lon_r\", \"lat_r\", \"zone\"])\n",
    "df[\"lon_r\"], df[\"lat_r\"] = df[\"longitude\"].round(5), df[\"latitude\"].round(5)\n",
    "deja = set(zip(cache[\"lon_r\"], cache[\"lat_r\"]))\n",
    "a_faire = df[[\"lon_r\", \"lat_r\"]].drop_duplicates()\n",
    "a_faire = a_faire[[(lo, la) not in deja for lo, la in zip(a_faire[\"lon_r\"], a_faire[\"lat_r\"])]]\n",
    "print(len(a_faire), \"points à interroger\")\n",
    "\n",
    "nouveaux, echecs = [], 0\n",
    "with requests.Session() as s:\n",
    "    for n, (lo, la) in enumerate(zip(a_faire[\"lon_r\"], a_faire[\"lat_r\"]), 1):\n",
    "        try:\n",
    "            nouveaux.append({\"lon_r\": lo, \"lat_r\": la, \"zone\": zone_en(lo, la, s)})\n",
    "        except Exception as e:\n",
    "            echecs += 1\n",
    "            if echecs <= 5:\n",
    "                print(f\"⚠️ Échec ({lo}, {la}) : {e}\")\n",
    "        if n % 200 == 0 or n == len(a_faire):   # sauvegarde régulière\n",
    "            cache = pd.concat([cache, pd.DataFrame(nouveaux)], ignore_index=True); nouveaux = []\n",
    "            cache.to_csv(PLU_PATH, index=False)\n",
    "            print(f\"{n}/{len(a_faire)} points, {echecs} échecs\")\n",
    "        time.sleep(0.05)\n",
    "\n",
    "if echecs:\n",
    "    print(f\"⚠️ {echecs} points sans réponse : relancer la cellule pour les reprendre\")"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 10,
   "id": "29138391",
   "metadata": {
    "colab": {
     "base_uri": "https://localhost:8080/",
     "height": 331
    },
    "id": "29138391",
    "executionInfo": {
     "status": "ok",
     "timestamp": 1791539070593,
     "user_tz": -120,
     "elapsed": 40462,
     "user": {
      "displayName": "Nathalie Wirth",
      "userId": "04968417715143734623"
     }
    },
    "outputId": "40ab76e2-3d32-4f59-cd94-cc7bef43ec38"
   },
   "outputs": [
    {
     "output_type": "stream",
     "name": "stdout",
     "text": [
      "          ventes  prix_median\n",
      "zone_plu                     \n",
      "CC_01         98         57.0\n",
      "CC_03         18         30.0\n",
      "PLU_A         93         70.0\n",
      "PLU_AU       527        124.0\n",
      "PLU_N         63         62.0\n",
      "PLU_U       2814        121.0\n",
      "RNU          524         78.0\n"
     ]
    },
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": [
       "                        RMSE   MAE Erreur relative médiane  \\\n",
       "Référence               43.3  25.9                     14%   \n",
       "+ remontées mécaniques  42.9  25.5                     14%   \n",
       "+ zonage PLU            43.3  25.9                     14%   \n",
       "+ remontées + PLU       43.3  25.7                     14%   \n",
       "\n",
       "                       Ventes à moins de 15 % RMSE stations  \n",
       "Référence                                 52%          99.4  \n",
       "+ remontées mécaniques                    53%          99.5  \n",
       "+ zonage PLU                              53%         100.5  \n",
       "+ remontées + PLU                         53%         101.0  "
      ],
      "text/html": [
       "\n",
       "  <div id=\"df-a702afd4-1c5a-4906-a2b4-63b7f8a75cb8\" class=\"colab-df-container\">\n",
       "    <div>\n",
       "<style scoped>\n",
       "    .dataframe tbody tr th:only-of-type {\n",
       "        vertical-align: middle;\n",
       "    }\n",
       "\n",
       "    .dataframe tbody tr th {\n",
       "        vertical-align: top;\n",
       "    }\n",
       "\n",
       "    .dataframe thead th {\n",
       "        text-align: right;\n",
       "    }\n",
       "</style>\n",
       "<table border=\"1\" class=\"dataframe\">\n",
       "  <thead>\n",
       "    <tr style=\"text-align: right;\">\n",
       "      <th></th>\n",
       "      <th>RMSE</th>\n",
       "      <th>MAE</th>\n",
       "      <th>Erreur relative médiane</th>\n",
       "      <th>Ventes à moins de 15 %</th>\n",
       "      <th>RMSE stations</th>\n",
       "    </tr>\n",
       "  </thead>\n",
       "  <tbody>\n",
       "    <tr>\n",
       "      <th>Référence</th>\n",
       "      <td>43.3</td>\n",
       "      <td>25.9</td>\n",
       "      <td>14%</td>\n",
       "      <td>52%</td>\n",
       "      <td>99.4</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>+ remontées mécaniques</th>\n",
       "      <td>42.9</td>\n",
       "      <td>25.5</td>\n",
       "      <td>14%</td>\n",
       "      <td>53%</td>\n",
       "      <td>99.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>+ zonage PLU</th>\n",
       "      <td>43.3</td>\n",
       "      <td>25.9</td>\n",
       "      <td>14%</td>\n",
       "      <td>53%</td>\n",
       "      <td>100.5</td>\n",
       "    </tr>\n",
       "    <tr>\n",
       "      <th>+ remontées + PLU</th>\n",
       "      <td>43.3</td>\n",
       "      <td>25.7</td>\n",
       "      <td>14%</td>\n",
       "      <td>53%</td>\n",
       "      <td>101.0</td>\n",
       "    </tr>\n",
       "  </tbody>\n",
       "</table>\n",
       "</div>\n",
       "    <div class=\"colab-df-buttons\">\n",
       "\n",
       "  <div class=\"colab-df-container\">\n",
       "    <button class=\"colab-df-convert\" onclick=\"convertToInteractive('df-a702afd4-1c5a-4906-a2b4-63b7f8a75cb8')\"\n",
       "            title=\"Convert this dataframe to an interactive table.\"\n",
       "            style=\"display:none;\">\n",
       "\n",
       "  <svg xmlns=\"http://www.w3.org/2000/svg\" height=\"24px\" viewBox=\"0 -960 960 960\">\n",
       "    <path d=\"M120-120v-720h720v720H120Zm60-500h600v-160H180v160Zm220 220h160v-160H400v160Zm0 220h160v-160H400v160ZM180-400h160v-160H180v160Zm440 0h160v-160H620v160ZM180-180h160v-160H180v160Zm440 0h160v-160H620v160Z\"/>\n",
       "  </svg>\n",
       "    </button>\n",
       "\n",
       "  <style>\n",
       "    .colab-df-container {\n",
       "      display:flex;\n",
       "      gap: 12px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert {\n",
       "      background-color: #E8F0FE;\n",
       "      border: none;\n",
       "      border-radius: 50%;\n",
       "      cursor: pointer;\n",
       "      display: none;\n",
       "      fill: #1967D2;\n",
       "      height: 32px;\n",
       "      padding: 0 0 0 0;\n",
       "      width: 32px;\n",
       "    }\n",
       "\n",
       "    .colab-df-convert:hover {\n",
       "      background-color: #E2EBFA;\n",
       "      box-shadow: 0px 1px 2px rgba(60, 64, 67, 0.3), 0px 1px 3px 1px rgba(60, 64, 67, 0.15);\n",
       "      fill: #174EA6;\n",
       "    }\n",
       "\n",
       "    .colab-df-buttons div {\n",
       "      margin-bottom: 4px;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert {\n",
       "      background-color: #3B4455;\n",
       "      fill: #D2E3FC;\n",
       "    }\n",
       "\n",
       "    [theme=dark] .colab-df-convert:hover {\n",
       "      background-color: #434B5C;\n",
       "      box-shadow: 0px 1px 3px 1px rgba(0, 0, 0, 0.15);\n",
       "      filter: drop-shadow(0px 1px 2px rgba(0, 0, 0, 0.3));\n",
       "      fill: #FFFFFF;\n",
       "    }\n",
       "  </style>\n",
       "\n",
       "    <script>\n",
       "      const buttonEl =\n",
       "        document.querySelector('#df-a702afd4-1c5a-4906-a2b4-63b7f8a75cb8 button.colab-df-convert');\n",
       "      buttonEl.style.display =\n",
       "        google.colab.kernel.accessAllowed ? 'block' : 'none';\n",
       "\n",
       "      async function convertToInteractive(key) {\n",
       "        const element = document.querySelector('#df-a702afd4-1c5a-4906-a2b4-63b7f8a75cb8');\n",
       "        const dataTable =\n",
       "          await google.colab.kernel.invokeFunction('convertToInteractive',\n",
       "                                                    [key], {});\n",
       "        if (!dataTable) return;\n",
       "\n",
       "        const docLinkHtml = 'Like what you see? Visit the ' +\n",
       "          '<a target=\"_blank\" href=https://colab.research.google.com/notebooks/data_table.ipynb>data table notebook</a>'\n",
       "          + ' to learn more about interactive tables.';\n",
       "        element.innerHTML = '';\n",
       "        dataTable['output_type'] = 'display_data';\n",
       "        await google.colab.output.renderOutput(dataTable, element);\n",
       "        const docLink = document.createElement('div');\n",
       "        docLink.innerHTML = docLinkHtml;\n",
       "        element.appendChild(docLink);\n",
       "      }\n",
       "    </script>\n",
       "  </div>\n",
       "\n",
       "    </div>\n",
       "  </div>\n"
      ],
      "application/vnd.google.colaboratory.intrinsic+json": {
       "type": "dataframe",
       "summary": "{\n  \"name\": \"pd\",\n  \"rows\": 4,\n  \"fields\": [\n    {\n      \"column\": \"RMSE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 42.9,\n        \"max\": 43.3,\n        \"num_unique_values\": 2,\n        \"samples\": [\n          42.9,\n          43.3\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"MAE\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 25.5,\n        \"max\": 25.9,\n        \"num_unique_values\": 3,\n        \"samples\": [\n          25.9,\n          25.5\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Erreur relative m\\u00e9diane\",\n      \"properties\": {\n        \"dtype\": \"category\",\n        \"num_unique_values\": 1,\n        \"samples\": [\n          \"14%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"Ventes \\u00e0 moins de 15 %\",\n      \"properties\": {\n        \"dtype\": \"string\",\n        \"num_unique_values\": 2,\n        \"samples\": [\n          \"53%\"\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    },\n    {\n      \"column\": \"RMSE stations\",\n      \"properties\": {\n        \"dtype\": \"date\",\n        \"min\": 99.4,\n        \"max\": 101.0,\n        \"num_unique_values\": 4,\n        \"samples\": [\n          99.5\n        ],\n        \"semantic_type\": \"\",\n        \"description\": \"\"\n      }\n    }\n  ]\n}"
      }
     },
     "metadata": {},
     "execution_count": 10
    }
   ],
   "source": [
    "df = df.drop(columns=[\"zone\"], errors=\"ignore\").merge(cache, on=[\"lon_r\", \"lat_r\"], how=\"left\")\n",
    "# Regroupement : U, AU (AUc, AUs...), A, N, carte communale constructible / non, RNU\n",
    "def famille(z):\n",
    "    if pd.isna(z): return \"inconnu\"\n",
    "    if z.startswith(\"PLU_AU\"): return \"PLU_AU\"\n",
    "    if z.startswith(\"PLU_U\"): return \"PLU_U\"\n",
    "    if z.startswith(\"PLU_A\"): return \"PLU_A\"\n",
    "    if z.startswith(\"PLU_N\"): return \"PLU_N\"\n",
    "    return z\n",
    "df[\"zone_plu\"] = df[\"zone\"].map(famille)\n",
    "print(df.groupby(\"zone_plu\")[\"prix_m2\"].agg(ventes=\"count\", prix_median=\"median\").round())\n",
    "\n",
    "resultats[\"+ zonage PLU\"] = evaluer(categories=[\"zone_plu\"])\n",
    "if \"dist_remontee_m\" in df.columns:\n",
    "    resultats[\"+ remontées + PLU\"] = evaluer([\"dist_remontee_m\", \"nb_remontees_5km\"], [\"zone_plu\"])\n",
    "pd.DataFrame(resultats).T"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "c85521fc",
   "metadata": {
    "id": "c85521fc"
   },
   "source": [
    "## Bilan\n",
    "\n",
    "Référence : RMSE 43,3 €/m² en validation croisée (10 plis), 99,4 €/m² dans les communes de stations.\n",
    "\n",
    "| Couche | RMSE (€/m²) | RMSE stations | Lecture |\n",
    "|---|---|---|---|\n",
    "| Prix des ventes voisines (testé à part) | 42,9 au mieux | | Le Random Forest fait déjà cette moyenne locale avec les coordonnées |\n",
    "| Remontées mécaniques (OpenStreetMap) | 42,9 | 99,5 | Environ +30 % à moins d'1 km d'une remontée, mais déjà capté par la position |\n",
    "| Zonage du PLU (Géoportail de l'urbanisme) | 43,3 | 100,5 | Zones U et AU vers 121-124 €/m², A et N vers 62-70 €/m² ; 81 % des ventes sont en U ou AU |\n",
    "\n",
    "Aucune couche de localisation n'améliore le modèle : il connaît déjà l'emplacement. L'erreur restante vient de ce que DVF ne dit pas du terrain lui-même. Suite : couches « parcelle » dans `Projet_UE8_couches_test_2.ipynb`."
   ]
  }
 ],
 "metadata": {
  "colab": {},
  "kernelspec": {
   "display_name": "Python 3",
   "name": "python3"
  },
  "language_info": {
   "name": "python"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}