FlorianSC/agritech-interface
0
1{2 "cells": [3 {4 "cell_type": "markdown",5 "id": "e049226f",6 "metadata": {},7 "source": [8 "# Système de recommandation agricole - Modélisation"9 ]10 },11 {12 "cell_type": "markdown",13 "id": "d11e1ec2",14 "metadata": {},15 "source": [16 "## Librairies nécessaires"17 ]18 },19 {20 "cell_type": "code",21 "execution_count": 1,22 "id": "19e6e556",23 "metadata": {},24 "outputs": [25 {26 "name": "stderr",27 "output_type": "stream",28 "text": [29 "/Users/florianschorer/Library/Caches/pypoetry/virtualenvs/systeme-recommandation-agricole-5e-JMIdX-py3.12/lib/python3.12/site-packages/tqdm/auto.py:21: TqdmWarning: IProgress not found. Please update jupyter and ipywidgets. See https://ipywidgets.readthedocs.io/en/stable/user_install.html\n",30 " from .autonotebook import tqdm as notebook_tqdm\n"31 ]32 }33 ],34 "source": [35 "# Import de base\n",36 "import pandas as pd\n",37 "import numpy as np\n",38 "import sys,os, joblib, shap\n",39 "import matplotlib.pyplot as plt\n",40 "import logging\n",41 "sys.path.append(os.path.abspath(\"..\"))\n",42 "\n",43 "# Import scikit learn\n",44 "from sklearn.model_selection import (train_test_split, KFold, cross_validate, GridSearchCV)\n",45 "from sklearn.compose import ColumnTransformer\n",46 "from sklearn.pipeline import Pipeline\n",47 "from sklearn.preprocessing import StandardScaler\n",48 "from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score, mean_absolute_percentage_error\n",49 "from sklearn.linear_model import LinearRegression\n",50 "from sklearn.ensemble import RandomForestRegressor\n",51 "from sklearn.dummy import DummyRegressor\n",52 "\n",53 "# Import des autres modèles testés\n",54 "import xgboost as xgb\n",55 "import lightgbm as lgb\n",56 "import warnings\n",57 "warnings.filterwarnings(\"ignore\", message=\"X does not have valid feature names\")\n",58 "warnings.filterwarnings(\"ignore\", message=\"Hint: Inferred schema contains integer\")\n",59 "warnings.filterwarnings(\"ignore\", message=\"Saving scikit-learn models\")\n",60 "\n",61 "# Import mlflow\n",62 "import mlflow\n",63 "mlflow.set_tracking_uri(\"file://\" + os.path.abspath(\"../mlruns\"))\n",64 "from mlflow.tracking import MlflowClient\n",65 "from mlflow.models.signature import infer_signature\n",66 "# Warning pickle/sklearn de MLflow\n",67 "logging.getLogger(\"mlflow.sklearn\").setLevel(logging.ERROR)\n",68 "# Import du projet\n",69 "from scripts.preprocessing_pipeline import (\n",70 " separation_X_y,\n",71 " preparation_pipeline,\n",72 " cross_validation,\n",73 " train_predict\n",74 ")\n",75 "from scripts.config import (csv_yield_conso, csv_yield_enriched)"76 ]77 },78 {79 "cell_type": "markdown",80 "id": "d6134633",81 "metadata": {},82 "source": [83 "# Comparaison modèle sans optimisation sur le fichier consolidé"84 ]85 },86 {87 "cell_type": "code",88 "execution_count": 20,89 "id": "5b8cc3b8",90 "metadata": {},91 "outputs": [],92 "source": [93 "def mlflow_tracking_model(model, model_name, tags, projet_description):\n",94 " # =====================\n",95 " # Configuration MLflow\n",96 " mlflow.set_tracking_uri(\"http://127.0.0.1:5000\")\n",97 " mlflow.set_experiment(\"Agritech_Answers\")\n",98 "\n",99 " reg_name = \"Yield_Forecaster_Global\"\n",100 " mlflow.sklearn.autolog(log_models=False, log_datasets=False, silent=True)\n",101 "\n",102 " # =======================\n",103 " # Chargement des données\n",104 " # Dataset préparé mais non encodé\n",105 " df = pd.read_csv(csv_yield_conso)\n",106 "\n",107 " with mlflow.start_run(run_name=model_name, tags={\n",108 " \"Training Info\": tags,\n",109 " \"Algorithm\": model.__class__.__name__,\n",110 " \"mlflow.note.content\": projet_description\n",111 " }) as run:\n",112 "\n",113 " X_train, X_test, y_train, y_test, categorical_cols, numeric_cols = separation_X_y(df)\n",114 "\n",115 " # Pipeline & Cross-Validation\n",116 " pipeline = preparation_pipeline(\n",117 " numeric_cols=numeric_cols,\n",118 " categorical_cols=categorical_cols,\n",119 " model=model\n",120 " )\n",121 " cv_results = cross_validation(pipeline=pipeline, X_train=X_train, y_train=y_train)\n",122 "\n",123 " # On divise par 10 pour avoir les informations en kg (actuellement en hg)\n",124 " cv_metrics = {\n",125 " \"cv_rmse_mean_kg\": np.sqrt(-cv_results[\"test_mse\"]).mean() / 10,\n",126 " \"cv_rmse_std_kg\": np.sqrt(-cv_results[\"test_mse\"]).std() / 10,\n",127 " \"cv_mae_mean_kg\": (-cv_results[\"test_mae\"]).mean() / 10,\n",128 " \"cv_mae_std_kg\": (-cv_results[\"test_mae\"]).std() / 10,\n",129 " \"cv_mape_mean\": (-cv_results[\"test_mape\"]).mean(),\n",130 " \"cv_mape_std\": (-cv_results[\"test_mape\"]).std(),\n",131 " \"cv_r2_mean\": cv_results[\"test_r2\"].mean(),\n",132 " \"cv_r2_std\": cv_results[\"test_r2\"].std()\n",133 " }\n",134 " mlflow.log_metrics(cv_metrics)\n",135 "\n",136 " # Entraînement final\n",137 " pipeline.fit(X_train, y_train)\n",138 "\n",139 " # Prédictions test\n",140 " y_pred = pipeline.predict(X_test)\n",141 "\n",142 " # Prix moyen au global tiré d'un fichier officiel - FAO\n",143 " prices_fao = {\n",144 " \"cassava\": 270, \"maize\": 260, \"plantains_and_others\": 480, \"potatoes\": 330,\n",145 " \"rice\": 360, \"sorghum\": 230, \"soybean\": 400, \"sweet_potatoes\": 420,\n",146 " \"wheat\": 200, \"yams\": 890, \"barley\": 220\n",147 " }\n",148 " # Construction d'un dataframe pour y extraire ligne à ligne par la suite\n",149 " results_df = pd.DataFrame({\n",150 " \"actual\": y_test,\n",151 " \"pred\": y_pred,\n",152 " \"abs_error\": np.abs(y_test - y_pred),\n",153 " }, index=X_test.index)\n",154 "\n",155 " # On récupère directement la culture brute\n",156 " results_df[\"crop\"] = (\n",157 " X_test.loc[results_df.index, \"item\"]\n",158 " .astype(str)\n",159 " .str.strip()\n",160 " .str.lower()\n",161 " )\n",162 " # Calcul du R2 par item\n",163 " r2_by_item = results_df.groupby(\"crop\").apply(\n",164 " lambda g: r2_score(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",165 " ).dropna()\n",166 " \n",167 " # Calcul du MAPE par item\n",168 " mape_by_item = results_df.groupby(\"crop\").apply(\n",169 " lambda g: mean_absolute_percentage_error(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",170 " ).dropna() \n",171 " # Enregistrement des métriques\n",172 " mlflow.log_metrics({f\"test_r2_{crop}\": float(r2)for crop, r2 in r2_by_item.items()})\n",173 " mlflow.log_metrics({f\"test_mape_{crop}\": float(mape)for crop, mape in mape_by_item.items()})\n",174 "\n",175 " # Calcul du coût économique\n",176 " def calculate_monetary_error(row):\n",177 " price = prices_fao.get(row[\"crop\"], 250)\n",178 " return (row[\"abs_error\"] / 10000) * price\n",179 "\n",180 " results_df[\"error_cost_usd_ha\"] = results_df.apply(calculate_monetary_error, axis=1)\n",181 " # Par crop\n",182 " economic_error_by_crop = (results_df.groupby(\"crop\")[\"error_cost_usd_ha\"].mean().sort_values())\n",183 "\n",184 " mlflow.log_metrics({f\"test_economic_error_usd_ha_{crop}\": float(cost)\n",185 " for crop, cost in economic_error_by_crop.items()\n",186 " })\n",187 " # Au total\n",188 " mean_economic_error = results_df[\"error_cost_usd_ha\"].mean()\n",189 "\n",190 " rmse_hg = np.sqrt(mean_squared_error(y_test, y_pred))\n",191 " mae_hg = mean_absolute_error(y_test, y_pred)\n",192 " test_metrics = {\n",193 " \"test_rmse_kg\": rmse_hg / 10,\n",194 " \"test_mae_kg\": mae_hg / 10,\n",195 " \"test_mape\": mean_absolute_percentage_error(y_test, y_pred),\n",196 " \"test_r2\": r2_score(y_test, y_pred),\n",197 " \"economic_error_usd_ha\": mean_economic_error\n",198 " }\n",199 " mlflow.log_metrics(test_metrics)\n",200 "\n",201 " signature = infer_signature(X_test, y_pred)\n",202 " model_info = mlflow.sklearn.log_model(\n",203 " sk_model=pipeline,\n",204 " name=\"model\",\n",205 " signature=signature,\n",206 " registered_model_name=reg_name\n",207 " )\n",208 "\n",209 " client = MlflowClient()\n",210 " mv = model_info.registered_model_version\n",211 "\n",212 " full_description = (\n",213 " f\"**Modèle :** {model_name}\\n\"\n",214 " f\"**Note :** {projet_description}\\n\\n\"\n",215 " f\"**Scores CV (Moyenne ± Écart-type) :**\\n\"\n",216 " f\"- CV R2: {cv_metrics['cv_r2_mean']:.4f} (± {cv_metrics['cv_r2_std']:.4f})\\n\"\n",217 " f\"- CV RMSE: {cv_metrics['cv_rmse_mean_kg']:.2f} (± {cv_metrics['cv_rmse_std_kg']:.2f})\\n\\n\"\n",218 " f\"**Scores Test :**\\n\"\n",219 " f\"- Test R2: {test_metrics['test_r2']:.4f}\\n\"\n",220 " f\"- Test economic_error_usd_ha: {test_metrics['economic_error_usd_ha']:.4f}\\n\"\n",221 " )\n",222 "\n",223 " client.update_model_version(name=reg_name, version=mv, description=full_description)\n",224 " client.set_model_version_tag(reg_name, mv, \"Algo\", model.__class__.__name__)\n",225 " client.set_model_version_tag(reg_name, mv, \"CV_R2\", round(cv_metrics[\"cv_r2_mean\"], 4))\n",226 " client.set_model_version_tag(reg_name, mv, \"CV_Std\", round(cv_metrics[\"cv_r2_std\"], 4))\n",227 " \n",228 "\n",229 " print(f\"\\nVersion {mv} enregistrée.\")\n",230 " print(\"\\n=== Résultats métriques ===\")\n",231 " print(f\"CV RMSE : {cv_metrics['cv_rmse_mean_kg']:.4f} (± {cv_metrics['cv_rmse_std_kg']:.2f})\")\n",232 " print(f\"CV MAPE : {cv_metrics['cv_mape_mean']:.4f} (± {cv_metrics['cv_mape_std']:.4f})\")\n",233 " print(f\"CV R2 : {cv_metrics['cv_r2_mean']:.4f} (± {cv_metrics['cv_r2_std']:.4f})\")\n",234 " print(f\"Test RMSE : {test_metrics['test_rmse_kg']:.4f}\")\n",235 " print(f\"Test R2 : {test_metrics['test_r2']:.4f}\")\n",236 " print(f\"Test MAPE : {test_metrics['test_mape']:.4f}\")\n",237 " print(f\"Test economic_error_usd_ha : {test_metrics['economic_error_usd_ha']:.4f}\\n\")"238 ]239 },240 {241 "cell_type": "code",242 "execution_count": 13,243 "id": "0705928c",244 "metadata": {},245 "outputs": [246 {247 "name": "stderr",248 "output_type": "stream",249 "text": [250 "2026-04-12 08:49:59,181 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",251 "2026-04-12 08:49:59,182 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",252 "Successfully registered model 'Yield_Forecaster_Global'.\n",253 "2026/04/12 08:50:03 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 1\n"254 ]255 },256 {257 "name": "stdout",258 "output_type": "stream",259 "text": [260 "\n",261 "Version 1 enregistrée.\n",262 "\n",263 "=== Résultats métriques ===\n",264 "CV RMSE : 7604.3730 (± 187.80)\n",265 "CV MAPE : 2.6730 (± 0.1885)\n",266 "CV R2 : -0.0002 (± 0.0002)\n",267 "Test RMSE : 7216.6343\n",268 "Test R2 : -0.0013\n",269 "Test MAPE : 2.4448\n",270 "Test economic_error_usd_ha : 1680.7060\n",271 "\n",272 "🏃 View run DummyRegressor - Baseline at: http://127.0.0.1:5000/#/experiments/5/runs/9c590a69132549ad912658af83cb30c4\n",273 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"274 ]275 },276 {277 "name": "stderr",278 "output_type": "stream",279 "text": [280 "Created version '1' of model 'Yield_Forecaster_Global'.\n"281 ]282 }283 ],284 "source": [285 "# DummyRegressor\n",286 "model = DummyRegressor()\n",287 "model_name = \"DummyRegressor - Baseline\"\n",288 "tags = \"DummyRegressor - Baseline\"\n",289 "projet_description = \"Test d'un modèle DummyRegressor pour avoir une base sur laquelle comparer\"\n",290 "mlflow_tracking_model(model, model_name, tags, projet_description)"291 ]292 },293 {294 "cell_type": "code",295 "execution_count": 14,296 "id": "ebe96f7f",297 "metadata": {},298 "outputs": [299 {300 "name": "stderr",301 "output_type": "stream",302 "text": [303 "2026-04-12 08:50:39,180 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",304 "2026-04-12 08:50:39,181 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",305 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",306 "2026/04/12 08:50:44 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 2\n"307 ]308 },309 {310 "name": "stdout",311 "output_type": "stream",312 "text": [313 "\n",314 "Version 2 enregistrée.\n",315 "\n",316 "=== Résultats métriques ===\n",317 "CV RMSE : 4840.4290 (± 55.16)\n",318 "CV MAPE : 0.9546 (± 0.0388)\n",319 "CV R2 : 0.5943 (± 0.0143)\n",320 "Test RMSE : 4805.9227\n",321 "Test R2 : 0.5559\n",322 "Test MAPE : 0.9859\n",323 "Test economic_error_usd_ha : 1033.3629\n",324 "\n",325 "🏃 View run LinearRegression - Baseline at: http://127.0.0.1:5000/#/experiments/5/runs/871688843f8347a08019ae537790c430\n",326 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"327 ]328 },329 {330 "name": "stderr",331 "output_type": "stream",332 "text": [333 "Created version '2' of model 'Yield_Forecaster_Global'.\n"334 ]335 }336 ],337 "source": [338 "# Modèle de régression linéaire\n",339 "model = LinearRegression()\n",340 "model_name = \"LinearRegression - Baseline\"\n",341 "tags = \"LinearRegression - Baseline\"\n",342 "projet_description = \"Test d'un modèle LinearRegression sans optimisation\"\n",343 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"344 ]345 },346 {347 "cell_type": "code",348 "execution_count": 15,349 "id": "6f7dd30f",350 "metadata": {},351 "outputs": [352 {353 "name": "stderr",354 "output_type": "stream",355 "text": [356 "2026-04-12 08:50:53,242 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",357 "2026-04-12 08:50:53,242 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",358 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",359 "2026/04/12 08:53:12 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 3\n",360 "Created version '3' of model 'Yield_Forecaster_Global'.\n"361 ]362 },363 {364 "name": "stdout",365 "output_type": "stream",366 "text": [367 "\n",368 "Version 3 enregistrée.\n",369 "\n",370 "=== Résultats métriques ===\n",371 "CV RMSE : 1893.0226 (± 45.34)\n",372 "CV MAPE : 0.2091 (± 0.0098)\n",373 "CV R2 : 0.9378 (± 0.0051)\n",374 "Test RMSE : 1729.6763\n",375 "Test R2 : 0.9425\n",376 "Test MAPE : 0.1739\n",377 "Test economic_error_usd_ha : 273.6834\n",378 "\n",379 "🏃 View run RandomForest - Baseline at: http://127.0.0.1:5000/#/experiments/5/runs/1c3f33cbdb7a486995cbb441297fd81e\n",380 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"381 ]382 }383 ],384 "source": [385 "# Modèle de RandomForest\n",386 "model = RandomForestRegressor(random_state=42)\n",387 "model_name = \"RandomForest - Baseline\"\n",388 "tags = \"RandomForest - Baseline\"\n",389 "projet_description = \"Test d'un modèle Random Forest sans optimisation\"\n",390 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"391 ]392 },393 {394 "cell_type": "code",395 "execution_count": 16,396 "id": "c486a9a3",397 "metadata": {},398 "outputs": [399 {400 "name": "stderr",401 "output_type": "stream",402 "text": [403 "2026-04-12 08:53:58,395 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",404 "2026-04-12 08:53:58,396 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",405 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",406 "2026/04/12 08:54:04 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 4\n"407 ]408 },409 {410 "name": "stdout",411 "output_type": "stream",412 "text": [413 "\n",414 "Version 4 enregistrée.\n",415 "\n",416 "=== Résultats métriques ===\n",417 "CV RMSE : 2152.0037 (± 71.11)\n",418 "CV MAPE : 0.3938 (± 0.0186)\n",419 "CV R2 : 0.9195 (± 0.0082)\n",420 "Test RMSE : 2040.8158\n",421 "Test R2 : 0.9199\n",422 "Test MAPE : 0.3603\n",423 "Test economic_error_usd_ha : 406.6547\n",424 "\n",425 "🏃 View run XGBRegressor - Baseline at: http://127.0.0.1:5000/#/experiments/5/runs/4d8e7f0cc79d4d7cbbe9685a5a1ccd3a\n",426 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"427 ]428 },429 {430 "name": "stderr",431 "output_type": "stream",432 "text": [433 "Created version '4' of model 'Yield_Forecaster_Global'.\n"434 ]435 }436 ],437 "source": [438 "# Modèle de XGBoost\n",439 "model = xgb.XGBRegressor(random_state=42)\n",440 "model_name = \"XGBRegressor - Baseline\"\n",441 "tags = \"XGBRegressor - Baseline\"\n",442 "projet_description = \"Test d'un modèle XGBRegressor sans optimisation\"\n",443 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"444 ]445 },446 {447 "cell_type": "code",448 "execution_count": 17,449 "id": "26fbf12c",450 "metadata": {},451 "outputs": [452 {453 "name": "stderr",454 "output_type": "stream",455 "text": [456 "2026-04-12 08:54:10,602 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",457 "2026-04-12 08:54:10,602 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",458 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",459 "2026/04/12 08:54:19 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 5\n"460 ]461 },462 {463 "name": "stdout",464 "output_type": "stream",465 "text": [466 "\n",467 "Version 5 enregistrée.\n",468 "\n",469 "=== Résultats métriques ===\n",470 "CV RMSE : 2689.7368 (± 50.00)\n",471 "CV MAPE : 0.5493 (± 0.0383)\n",472 "CV R2 : 0.8745 (± 0.0095)\n",473 "Test RMSE : 2679.5389\n",474 "Test R2 : 0.8620\n",475 "Test MAPE : 0.5107\n",476 "Test economic_error_usd_ha : 551.5749\n",477 "\n",478 "🏃 View run LGBMRegressor - Baseline at: http://127.0.0.1:5000/#/experiments/5/runs/b29dcd3b524e4853a8ae6db900a8327e\n",479 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"480 ]481 },482 {483 "name": "stderr",484 "output_type": "stream",485 "text": [486 "Created version '5' of model 'Yield_Forecaster_Global'.\n"487 ]488 }489 ],490 "source": [491 "# Modèle de LightGBM\n",492 "model = lgb.LGBMRegressor(random_state=42, verbose=-1)\n",493 "model_name = \"LGBMRegressor - Baseline\"\n",494 "tags = \"LGBMRegressor - Baseline\"\n",495 "projet_description = \"Test d'un modèle LGBMRegressor sans optimisation\"\n",496 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"497 ]498 },499 {500 "cell_type": "markdown",501 "id": "9180d9a8",502 "metadata": {},503 "source": [504 "- Sur ces premiers tests, le modèle qui s'en sort le mieux sur l'ensemble des métriques (performance et métier) c'est celui de Random Forest.\n",505 "- Les résultats de XGBoost et LightGBM sont exploitables pour essayer d'améliorer leur performance.\n",506 "- Par contre le modèle de régression linéaire est trop loin en terme de performance. Au vu de la distribution de notre varibale cible, cela n'est pas étonnant."507 ]508 },509 {510 "cell_type": "markdown",511 "id": "fd6c92b7",512 "metadata": {},513 "source": [514 "# Comparaison modèle sans optimisation sur le fichier enrichi"515 ]516 },517 {518 "cell_type": "code",519 "execution_count": 2,520 "id": "3304ea12",521 "metadata": {},522 "outputs": [],523 "source": [524 "def mlflow_tracking_model(model, model_name, tags, projet_description):\n",525 " # =====================\n",526 " # Configuration MLflow\n",527 " mlflow.set_tracking_uri(\"http://127.0.0.1:5000\")\n",528 " mlflow.set_experiment(\"Agritech_Answers\")\n",529 "\n",530 " reg_name = \"Yield_Forecaster_Global\"\n",531 " mlflow.sklearn.autolog(log_models=False, log_datasets=False, silent=True)\n",532 "\n",533 " # =======================\n",534 " # Chargement des données\n",535 " # Dataset préparé mais non encodé\n",536 " df = pd.read_csv(csv_yield_enriched)\n",537 "\n",538 " with mlflow.start_run(run_name=model_name, tags={\n",539 " \"Training Info\": tags,\n",540 " \"Algorithm\": model.__class__.__name__,\n",541 " \"mlflow.note.content\": projet_description\n",542 " }) as run:\n",543 "\n",544 " X_train, X_test, y_train, y_test, categorical_cols, numeric_cols = separation_X_y(df)\n",545 "\n",546 " # Pipeline & Cross-Validation\n",547 " pipeline = preparation_pipeline(\n",548 " numeric_cols=numeric_cols,\n",549 " categorical_cols=categorical_cols,\n",550 " model=model\n",551 " )\n",552 " cv_results = cross_validation(pipeline=pipeline, X_train=X_train, y_train=y_train)\n",553 "\n",554 " # On divise par 10 pour avoir les informations en kg (actuellement en hg)\n",555 " cv_metrics = {\n",556 " \"cv_rmse_mean_kg\": np.sqrt(-cv_results[\"test_mse\"]).mean() / 10,\n",557 " \"cv_rmse_std_kg\": np.sqrt(-cv_results[\"test_mse\"]).std() / 10,\n",558 " \"cv_mae_mean_kg\": (-cv_results[\"test_mae\"]).mean() / 10,\n",559 " \"cv_mae_std_kg\": (-cv_results[\"test_mae\"]).std() / 10,\n",560 " \"cv_mape_mean\": (-cv_results[\"test_mape\"]).mean(),\n",561 " \"cv_mape_std\": (-cv_results[\"test_mape\"]).std(),\n",562 " \"cv_r2_mean\": cv_results[\"test_r2\"].mean(),\n",563 " \"cv_r2_std\": cv_results[\"test_r2\"].std()\n",564 " }\n",565 " mlflow.log_metrics(cv_metrics)\n",566 "\n",567 " # Entraînement final\n",568 " pipeline.fit(X_train, y_train)\n",569 "\n",570 " # Prédictions test\n",571 " y_pred = pipeline.predict(X_test)\n",572 "\n",573 " # Prix moyen au global tiré d'un fichier officiel - FAO\n",574 " prices_fao = {\n",575 " \"cassava\": 270, \"maize\": 260, \"plantains_and_others\": 480, \"potatoes\": 330,\n",576 " \"rice\": 360, \"sorghum\": 230, \"soybean\": 400, \"sweet_potatoes\": 420,\n",577 " \"wheat\": 200, \"yams\": 890, \"barley\": 220\n",578 " }\n",579 " # Construction d'un dataframe pour y extraire ligne à ligne par la suite\n",580 " results_df = pd.DataFrame({\n",581 " \"actual\": y_test,\n",582 " \"pred\": y_pred,\n",583 " \"abs_error\": np.abs(y_test - y_pred),\n",584 " }, index=X_test.index)\n",585 "\n",586 " # On récupère directement la culture brute\n",587 " results_df[\"crop\"] = (\n",588 " X_test.loc[results_df.index, \"item\"]\n",589 " .astype(str)\n",590 " .str.strip()\n",591 " .str.lower()\n",592 " )\n",593 " # Calcul du R2 par item\n",594 " r2_by_item = results_df.groupby(\"crop\").apply(\n",595 " lambda g: r2_score(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",596 " ).dropna()\n",597 " \n",598 " # Calcul du MAPE par item\n",599 " mape_by_item = results_df.groupby(\"crop\").apply(\n",600 " lambda g: mean_absolute_percentage_error(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",601 " ).dropna() \n",602 " # Enregistrement des métriques\n",603 " mlflow.log_metrics({f\"test_r2_{crop}\": float(r2)for crop, r2 in r2_by_item.items()})\n",604 " mlflow.log_metrics({f\"test_mape_{crop}\": float(mape)for crop, mape in mape_by_item.items()})\n",605 "\n",606 " # Calcul du coût économique\n",607 " def calculate_monetary_error(row):\n",608 " price = prices_fao.get(row[\"crop\"], 250)\n",609 " return (row[\"abs_error\"] / 10000) * price\n",610 "\n",611 " results_df[\"error_cost_usd_ha\"] = results_df.apply(calculate_monetary_error, axis=1)\n",612 " # Par crop\n",613 " economic_error_by_crop = (results_df.groupby(\"crop\")[\"error_cost_usd_ha\"].mean().sort_values())\n",614 "\n",615 " mlflow.log_metrics({f\"test_economic_error_usd_ha_{crop}\": float(cost)\n",616 " for crop, cost in economic_error_by_crop.items()\n",617 " })\n",618 " # Au total\n",619 " mean_economic_error = results_df[\"error_cost_usd_ha\"].mean()\n",620 "\n",621 " rmse_hg = np.sqrt(mean_squared_error(y_test, y_pred))\n",622 " mae_hg = mean_absolute_error(y_test, y_pred)\n",623 " test_metrics = {\n",624 " \"test_rmse_kg\": rmse_hg / 10,\n",625 " \"test_mae_kg\": mae_hg / 10,\n",626 " \"test_mape\": mean_absolute_percentage_error(y_test, y_pred),\n",627 " \"test_r2\": r2_score(y_test, y_pred),\n",628 " \"economic_error_usd_ha\": mean_economic_error\n",629 " }\n",630 " mlflow.log_metrics(test_metrics)\n",631 "\n",632 " signature = infer_signature(X_test, y_pred)\n",633 " model_info = mlflow.sklearn.log_model(\n",634 " sk_model=pipeline,\n",635 " name=\"model\",\n",636 " signature=signature,\n",637 " registered_model_name=reg_name\n",638 " )\n",639 "\n",640 " client = MlflowClient()\n",641 " mv = model_info.registered_model_version\n",642 "\n",643 " full_description = (\n",644 " f\"**Modèle :** {model_name}\\n\"\n",645 " f\"**Note :** {projet_description}\\n\\n\"\n",646 " f\"**Scores CV (Moyenne ± Écart-type) :**\\n\"\n",647 " f\"- CV R2: {cv_metrics['cv_r2_mean']:.4f} (± {cv_metrics['cv_r2_std']:.4f})\\n\"\n",648 " f\"- CV RMSE: {cv_metrics['cv_rmse_mean_kg']:.2f} (± {cv_metrics['cv_rmse_std_kg']:.2f})\\n\\n\"\n",649 " f\"**Scores Test :**\\n\"\n",650 " f\"- Test R2: {test_metrics['test_r2']:.4f}\\n\"\n",651 " f\"- Test economic_error_usd_ha: {test_metrics['economic_error_usd_ha']:.4f}\\n\"\n",652 " )\n",653 "\n",654 " client.update_model_version(name=reg_name, version=mv, description=full_description)\n",655 " client.set_model_version_tag(reg_name, mv, \"Algo\", model.__class__.__name__)\n",656 " client.set_model_version_tag(reg_name, mv, \"CV_R2\", round(cv_metrics[\"cv_r2_mean\"], 4))\n",657 " client.set_model_version_tag(reg_name, mv, \"CV_Std\", round(cv_metrics[\"cv_r2_std\"], 4))\n",658 " \n",659 "\n",660 " print(f\"\\nVersion {mv} enregistrée.\")\n",661 " print(\"\\n=== Résultats métriques ===\")\n",662 " print(f\"CV RMSE : {cv_metrics['cv_rmse_mean_kg']:.4f} (± {cv_metrics['cv_rmse_std_kg']:.2f})\")\n",663 " print(f\"CV MAPE : {cv_metrics['cv_mape_mean']:.4f} (± {cv_metrics['cv_mape_std']:.4f})\")\n",664 " print(f\"CV R2 : {cv_metrics['cv_r2_mean']:.4f} (± {cv_metrics['cv_r2_std']:.4f})\")\n",665 " print(f\"Test RMSE : {test_metrics['test_rmse_kg']:.4f}\")\n",666 " print(f\"Test R2 : {test_metrics['test_r2']:.4f}\")\n",667 " print(f\"Test MAPE : {test_metrics['test_mape']:.4f}\")\n",668 " print(f\"Test economic_error_usd_ha : {test_metrics['economic_error_usd_ha']:.4f}\\n\")"669 ]670 },671 {672 "cell_type": "code",673 "execution_count": 3,674 "id": "007ff704",675 "metadata": {},676 "outputs": [677 {678 "name": "stderr",679 "output_type": "stream",680 "text": [681 "2026-04-12 09:01:09,645 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",682 "2026-04-12 09:01:09,646 - INFO - Colonnes catégorielles : ['region', 'item', 'fertilizer_used', 'irrigation_used', 'weather_condition', 'soil_type', 'is_drought']\n",683 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",684 "2026/04/12 09:01:16 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 6\n"685 ]686 },687 {688 "name": "stdout",689 "output_type": "stream",690 "text": [691 "\n",692 "Version 6 enregistrée.\n",693 "\n",694 "=== Résultats métriques ===\n",695 "CV RMSE : 7604.3730 (± 187.80)\n",696 "CV MAPE : 2.6730 (± 0.1885)\n",697 "CV R2 : -0.0002 (± 0.0002)\n",698 "Test RMSE : 7216.6343\n",699 "Test R2 : -0.0013\n",700 "Test MAPE : 2.4448\n",701 "Test economic_error_usd_ha : 1680.7060\n",702 "\n",703 "🏃 View run DummyRegressor - Baseline - fichier enrichi at: http://127.0.0.1:5000/#/experiments/5/runs/f15a2b7071754947abb27685d1282e0e\n",704 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"705 ]706 },707 {708 "name": "stderr",709 "output_type": "stream",710 "text": [711 "Created version '6' of model 'Yield_Forecaster_Global'.\n"712 ]713 }714 ],715 "source": [716 "# DummyRegressor\n",717 "model = DummyRegressor()\n",718 "model_name = \"DummyRegressor - Baseline - fichier enrichi\"\n",719 "tags = \"DummyRegressor - Baseline - fichier enrichi\"\n",720 "projet_description = \"Test d'un modèle DummyRegressor pour avoir une base sur laquelle comparer\"\n",721 "mlflow_tracking_model(model, model_name, tags, projet_description)"722 ]723 },724 {725 "cell_type": "code",726 "execution_count": 4,727 "id": "f5b2bbd0",728 "metadata": {},729 "outputs": [730 {731 "name": "stderr",732 "output_type": "stream",733 "text": [734 "2026-04-12 09:01:24,808 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",735 "2026-04-12 09:01:24,808 - INFO - Colonnes catégorielles : ['region', 'item', 'fertilizer_used', 'irrigation_used', 'weather_condition', 'soil_type', 'is_drought']\n",736 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",737 "2026/04/12 09:01:30 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 7\n"738 ]739 },740 {741 "name": "stdout",742 "output_type": "stream",743 "text": [744 "\n",745 "Version 7 enregistrée.\n",746 "\n",747 "=== Résultats métriques ===\n",748 "CV RMSE : 4841.7110 (± 54.37)\n",749 "CV MAPE : 0.9555 (± 0.0386)\n",750 "CV R2 : 0.5941 (± 0.0143)\n",751 "Test RMSE : 4807.8225\n",752 "Test R2 : 0.5556\n",753 "Test MAPE : 0.9849\n",754 "Test economic_error_usd_ha : 1034.2202\n",755 "\n",756 "🏃 View run LinearRegression - Baseline - fichier enrichi at: http://127.0.0.1:5000/#/experiments/5/runs/5704e47afdd842e58333e141750f025d\n",757 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"758 ]759 },760 {761 "name": "stderr",762 "output_type": "stream",763 "text": [764 "Created version '7' of model 'Yield_Forecaster_Global'.\n"765 ]766 }767 ],768 "source": [769 "# Modèle de régression linéaire\n",770 "model = LinearRegression()\n",771 "model_name = \"LinearRegression - Baseline - fichier enrichi\"\n",772 "tags = \"LinearRegression - Baseline - fichier enrichi\"\n",773 "projet_description = \"Test d'un modèle LinearRegression sans optimisation\"\n",774 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"775 ]776 },777 {778 "cell_type": "code",779 "execution_count": 5,780 "id": "5f4160e5",781 "metadata": {},782 "outputs": [783 {784 "name": "stderr",785 "output_type": "stream",786 "text": [787 "2026-04-12 09:01:37,118 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",788 "2026-04-12 09:01:37,119 - INFO - Colonnes catégorielles : ['region', 'item', 'fertilizer_used', 'irrigation_used', 'weather_condition', 'soil_type', 'is_drought']\n",789 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",790 "2026/04/12 09:05:12 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 8\n"791 ]792 },793 {794 "name": "stdout",795 "output_type": "stream",796 "text": [797 "\n",798 "Version 8 enregistrée.\n",799 "\n",800 "=== Résultats métriques ===\n",801 "CV RMSE : 1926.1649 (± 66.57)\n",802 "CV MAPE : 0.2276 (± 0.0102)\n",803 "CV R2 : 0.9355 (± 0.0065)\n",804 "Test RMSE : 1748.9721\n",805 "Test R2 : 0.9412\n",806 "Test MAPE : 0.1982\n",807 "Test economic_error_usd_ha : 291.8239\n",808 "\n",809 "🏃 View run RandomForest - Baseline - fichier enrichi at: http://127.0.0.1:5000/#/experiments/5/runs/a071b5bdea9345c9b04d4ad003464fbc\n",810 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"811 ]812 },813 {814 "name": "stderr",815 "output_type": "stream",816 "text": [817 "Created version '8' of model 'Yield_Forecaster_Global'.\n"818 ]819 }820 ],821 "source": [822 "# Modèle de RandomForest\n",823 "model = RandomForestRegressor(random_state=42)\n",824 "model_name = \"RandomForest - Baseline - fichier enrichi\"\n",825 "tags = \"RandomForest - Baseline\"\n",826 "projet_description = \"Test d'un modèle Random Forest sans optimisation\"\n",827 "mlflow_tracking_model(model, model_name, tags, projet_description)\n"828 ]829 },830 {831 "cell_type": "code",832 "execution_count": 6,833 "id": "493e55f7",834 "metadata": {},835 "outputs": [836 {837 "name": "stderr",838 "output_type": "stream",839 "text": [840 "2026-04-12 09:06:35,255 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",841 "2026-04-12 09:06:35,255 - INFO - Colonnes catégorielles : ['region', 'item', 'fertilizer_used', 'irrigation_used', 'weather_condition', 'soil_type', 'is_drought']\n",842 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",843 "2026/04/12 09:06:39 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 9\n"844 ]845 },846 {847 "name": "stdout",848 "output_type": "stream",849 "text": [850 "\n",851 "Version 9 enregistrée.\n",852 "\n",853 "=== Résultats métriques ===\n",854 "CV RMSE : 2179.0641 (± 44.71)\n",855 "CV MAPE : 0.3995 (± 0.0220)\n",856 "CV R2 : 0.9175 (± 0.0072)\n",857 "Test RMSE : 2017.5085\n",858 "Test R2 : 0.9217\n",859 "Test MAPE : 0.3757\n",860 "Test economic_error_usd_ha : 413.5231\n",861 "\n",862 "🏃 View run XGBRegressor - Baseline - fichier enrichi at: http://127.0.0.1:5000/#/experiments/5/runs/d89fdce99f9b43ae87ad023c2c8ba077\n",863 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"864 ]865 },866 {867 "name": "stderr",868 "output_type": "stream",869 "text": [870 "Created version '9' of model 'Yield_Forecaster_Global'.\n"871 ]872 }873 ],874 "source": [875 "# Modèle de XGBoost\n",876 "model = xgb.XGBRegressor(random_state=42)\n",877 "model_name = \"XGBRegressor - Baseline - fichier enrichi\"\n",878 "tags = \"XGBRegressor - Baseline - fichier enrichi\"\n",879 "projet_description = \"Test d'un modèle XGBRegressor sans optimisation\"\n",880 "mlflow_tracking_model(model, model_name, tags, projet_description)"881 ]882 },883 {884 "cell_type": "code",885 "execution_count": 7,886 "id": "0c5606eb",887 "metadata": {},888 "outputs": [889 {890 "name": "stderr",891 "output_type": "stream",892 "text": [893 "2026-04-12 09:06:43,463 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",894 "2026-04-12 09:06:43,464 - INFO - Colonnes catégorielles : ['region', 'item', 'fertilizer_used', 'irrigation_used', 'weather_condition', 'soil_type', 'is_drought']\n",895 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",896 "2026/04/12 09:06:52 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 10\n"897 ]898 },899 {900 "name": "stdout",901 "output_type": "stream",902 "text": [903 "\n",904 "Version 10 enregistrée.\n",905 "\n",906 "=== Résultats métriques ===\n",907 "CV RMSE : 2709.1046 (± 52.11)\n",908 "CV MAPE : 0.5443 (± 0.0350)\n",909 "CV R2 : 0.8726 (± 0.0101)\n",910 "Test RMSE : 2677.7113\n",911 "Test R2 : 0.8621\n",912 "Test MAPE : 0.5128\n",913 "Test economic_error_usd_ha : 553.9541\n",914 "\n",915 "🏃 View run LGBMRegressor - Baseline - fichier enrichi at: http://127.0.0.1:5000/#/experiments/5/runs/a6066be51e0c4f49b5d7a6478e5b9514\n",916 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n"917 ]918 },919 {920 "name": "stderr",921 "output_type": "stream",922 "text": [923 "Created version '10' of model 'Yield_Forecaster_Global'.\n"924 ]925 }926 ],927 "source": [928 "# Modèle de LightGBM\n",929 "model = lgb.LGBMRegressor(random_state=42, verbose=-1)\n",930 "model_name = \"LGBMRegressor - Baseline - fichier enrichi\"\n",931 "tags = \"LGBMRegressor - Baselzine - fichier enrichi\"\n",932 "projet_description = \"Test d'un modèle LGBMRegressor sans optimisation\"\n",933 "mlflow_tracking_model(model, model_name, tags, projet_description)"934 ]935 },936 {937 "cell_type": "markdown",938 "id": "5b1bfb2d",939 "metadata": {},940 "source": [941 "# Optimisation des hyperparamètres sur RandomForest"942 ]943 },944 {945 "cell_type": "markdown",946 "id": "62c70c95",947 "metadata": {},948 "source": [949 "A l'aide de gridsearchCV, on va essayer ici de trouver les meilleurs hyperparamètres pour chacun des modèles"950 ]951 },952 {953 "cell_type": "code",954 "execution_count": 2,955 "id": "f49d391c",956 "metadata": {},957 "outputs": [],958 "source": [959 "def mlflow_tracking_gridsearch(model, model_name, param_grid, tags, projet_description):\n",960 " # =====================\n",961 " # Configuration MLflow\n",962 " mlflow.set_tracking_uri(\"http://127.0.0.1:5000\")\n",963 " mlflow.set_experiment(\"Agritech_Answers\")\n",964 "\n",965 " reg_name = \"Yield_Forecaster_Global\"\n",966 " mlflow.sklearn.autolog(log_models=False, log_datasets=False, silent=True, max_tuning_runs=0)\n",967 "\n",968 " # =======================\n",969 " # Chargement des données\n",970 " df = pd.read_csv(csv_yield_conso)\n",971 "\n",972 " with mlflow.start_run(run_name=model_name, tags={\n",973 " \"Training Info\": tags,\n",974 " \"Algorithm\": model.__class__.__name__,\n",975 " \"mlflow.note.content\": projet_description\n",976 " }) as run:\n",977 "\n",978 " mlflow.set_tag(\"target_raw_unit\", \"hg/ha\")\n",979 " mlflow.set_tag(\"logged_metric_unit\", \"kg/ha\")\n",980 " mlflow.set_tag(\"economic_metric_unit\", \"usd/ha\")\n",981 "\n",982 " X_train, X_test, y_train, y_test, categorical_cols, numeric_cols = separation_X_y(df)\n",983 "\n",984 " # Pipeline & GridSearch\n",985 " pipeline = preparation_pipeline(\n",986 " numeric_cols=numeric_cols,\n",987 " categorical_cols=categorical_cols,\n",988 " model=model\n",989 " )\n",990 "\n",991 " cv = KFold(n_splits=5, shuffle=True, random_state=42)\n",992 "\n",993 " grid_search = GridSearchCV(\n",994 " estimator=pipeline,\n",995 " param_grid=param_grid,\n",996 " scoring=\"r2\",\n",997 " n_jobs=-1,\n",998 " cv=cv,\n",999 " refit=True,\n",1000 " error_score=\"raise\"\n",1001 " )\n",1002 "\n",1003 " grid_search.fit(X_train, y_train)\n",1004 "\n",1005 " best_index = grid_search.best_index_\n",1006 "\n",1007 " cv_metrics = {\n",1008 " \"best_cv_r2_mean\": grid_search.best_score_,\n",1009 " \"best_cv_r2_std\": grid_search.cv_results_[\"std_test_score\"][best_index]\n",1010 " }\n",1011 "\n",1012 " mlflow.log_params(grid_search.best_params_)\n",1013 " mlflow.log_metrics(cv_metrics)\n",1014 "\n",1015 " # Entraînement final déjà fait par refit=True\n",1016 " best_pipeline = grid_search.best_estimator_\n",1017 "\n",1018 " # Prédictions test\n",1019 " y_pred = best_pipeline.predict(X_test)\n",1020 "\n",1021 " # Prix moyen au global tiré d'un fichier officiel - FAO\n",1022 " prices_fao = {\n",1023 " \"cassava\": 270, \"maize\": 260, \"plantains_and_others\": 480, \"potatoes\": 330,\n",1024 " \"rice\": 360, \"sorghum\": 230, \"soybean\": 400, \"sweet_potatoes\": 420,\n",1025 " \"wheat\": 200, \"yams\": 890, \"barley\": 220\n",1026 " }\n",1027 " # Construction d'un dataframe pour y extraire ligne à ligne par la suite\n",1028 " results_df = pd.DataFrame({\n",1029 " \"actual\": y_test,\n",1030 " \"pred\": y_pred,\n",1031 " \"abs_error\": np.abs(y_test - y_pred),\n",1032 " }, index=X_test.index)\n",1033 "\n",1034 " # On récupère directement la culture brute\n",1035 " results_df[\"crop\"] = (\n",1036 " X_test.loc[results_df.index, \"item\"]\n",1037 " .astype(str)\n",1038 " .str.strip()\n",1039 " .str.lower()\n",1040 " )\n",1041 " # Calcul du R2 par item\n",1042 " r2_by_item = results_df.groupby(\"crop\").apply(\n",1043 " lambda g: r2_score(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",1044 " ).dropna()\n",1045 " \n",1046 " # Calcul du MAPE par item\n",1047 " mape_by_item = results_df.groupby(\"crop\").apply(\n",1048 " lambda g: mean_absolute_percentage_error(g[\"actual\"], g[\"pred\"]) if len(g) > 1 else np.nan, include_groups=False\n",1049 " ).dropna() \n",1050 " # Enregistrement des métriques\n",1051 " mlflow.log_metrics({f\"test_r2_{crop}\": float(r2)for crop, r2 in r2_by_item.items()})\n",1052 " mlflow.log_metrics({f\"test_mape_{crop}\": float(mape)for crop, mape in mape_by_item.items()})\n",1053 "\n",1054 " # Calcul du coût économique\n",1055 " def calculate_monetary_error(row):\n",1056 " price = prices_fao.get(row[\"crop\"], 250)\n",1057 " return (row[\"abs_error\"] / 10000) * price\n",1058 "\n",1059 " results_df[\"error_cost_usd_ha\"] = results_df.apply(calculate_monetary_error, axis=1)\n",1060 " # Par crop\n",1061 " economic_error_by_crop = (results_df.groupby(\"crop\")[\"error_cost_usd_ha\"].mean().sort_values())\n",1062 "\n",1063 " mlflow.log_metrics({f\"test_economic_error_usd_ha_{crop}\": float(cost)\n",1064 " for crop, cost in economic_error_by_crop.items()\n",1065 " })\n",1066 " # Au total\n",1067 " mean_economic_error = results_df[\"error_cost_usd_ha\"].mean()\n",1068 "\n",1069 " rmse_hg = np.sqrt(mean_squared_error(y_test, y_pred))\n",1070 " mae_hg = mean_absolute_error(y_test, y_pred)\n",1071 " test_metrics = {\n",1072 " \"test_rmse_kg\": rmse_hg / 10,\n",1073 " \"test_mae_kg\": mae_hg / 10,\n",1074 " \"test_mape\": mean_absolute_percentage_error(y_test, y_pred),\n",1075 " \"test_r2\": r2_score(y_test, y_pred),\n",1076 " \"economic_error_usd_ha\": mean_economic_error\n",1077 " }\n",1078 " mlflow.log_metrics(test_metrics)\n",1079 "\n",1080 " signature = infer_signature(X_test, y_pred)\n",1081 "\n",1082 " model_info = mlflow.sklearn.log_model(\n",1083 " sk_model=best_pipeline,\n",1084 " name=\"model\",\n",1085 " signature=signature,\n",1086 " registered_model_name=reg_name\n",1087 " )\n",1088 "\n",1089 " client = MlflowClient()\n",1090 " mv = model_info.registered_model_version\n",1091 "\n",1092 " full_description = (\n",1093 " f\"**Modèle Optimisé :** {model_name}\\n\"\n",1094 " f\"**Note :** {projet_description}\\n\\n\"\n",1095 " f\"**Scores CV :**\\n\"\n",1096 " f\"- Best CV R2: {cv_metrics['best_cv_r2_mean']:.4f} \"\n",1097 " f\"(± {cv_metrics['best_cv_r2_std']:.4f})\\n\\n\"\n",1098 " f\"**Scores Test :**\\n\"\n",1099 " f\"- Test R2: {test_metrics['test_r2']:.4f}\\n\"\n",1100 " f\"- Test RMSE (kg/ha): {test_metrics['test_rmse_kg']:.2f}\\n\"\n",1101 " f\"- Test MAE (kg/ha): {test_metrics['test_mae_kg']:.2f}\\n\"\n",1102 " f\"- Test MAPE: {test_metrics['test_mape']:.2%}\\n\"\n",1103 " f\"- Test economic_error_usd_ha: {test_metrics['economic_error_usd_ha']:.4f}\\n\"\n",1104 " )\n",1105 "\n",1106 " client.update_model_version(name=reg_name, version=mv, description=full_description)\n",1107 " client.set_model_version_tag(reg_name, mv, \"Algo\", model.__class__.__name__)\n",1108 " client.set_model_version_tag(reg_name, mv, \"Best_CV_R2\", round(cv_metrics[\"best_cv_r2_mean\"], 4))\n",1109 " client.set_model_version_tag(reg_name, mv, \"Best_CV_Std\", round(cv_metrics[\"best_cv_r2_std\"], 4))\n",1110 " client.set_model_version_tag(reg_name, mv, \"Test_RMSE_kg_ha\", round(test_metrics[\"test_rmse_kg\"], 2))\n",1111 " client.set_model_version_tag(reg_name, mv, \"Test_MAPE\", round(test_metrics[\"test_mape\"],2))\n",1112 "\n",1113 " print(f\"\\nVersion {mv} enregistrée.\")\n",1114 " print(\"\\n=== Résultats métriques ===\")\n",1115 " print(f\"Best CV R2 : {cv_metrics['best_cv_r2_mean']:.4f} (± {cv_metrics['best_cv_r2_std']:.4f})\")\n",1116 " print(f\"Test RMSE (kg/ha) : {test_metrics['test_rmse_kg']:.4f}\")\n",1117 " print(f\"Test R2 : {test_metrics['test_r2']:.4f}\")\n",1118 " print(f\"Test MAPE : {test_metrics['test_mape']:.2%}\")\n",1119 " print(f\"Test economic_error_usd_ha : {test_metrics['economic_error_usd_ha']:.4f}\\n\")\n",1120 "\n",1121 " return {\n",1122 " \"best_pipeline\": best_pipeline,\n",1123 " \"best_params\": grid_search.best_params_,\n",1124 " \"test_metrics\": test_metrics,\n",1125 " \"X_test\": X_test,\n",1126 " \"y_test\": y_test\n",1127 " }"1128 ]1129 },1130 {1131 "cell_type": "code",1132 "execution_count": null,1133 "id": "596e1c31",1134 "metadata": {},1135 "outputs": [1136 {1137 "name": "stderr",1138 "output_type": "stream",1139 "text": [1140 "2026-04-12 09:54:25,072 - INFO - Colonnes numériques : ['avg_temp', 'rainfall_mm', 'pesticides_tonnes', 'input_imbalance', 'thermal_stress', 'years_from_now']\n",1141 "2026-04-12 09:54:25,073 - INFO - Colonnes catégorielles : ['region', 'item', 'is_drought']\n",1142 "Registered model 'Yield_Forecaster_Global' already exists. Creating a new version of this model...\n",1143 "2026/04/12 09:57:30 INFO mlflow.store.model_registry.abstract_store: Waiting up to 300 seconds for model version to finish creation. Model name: Yield_Forecaster_Global, version 11\n",1144 "Created version '11' of model 'Yield_Forecaster_Global'.\n"1145 ]1146 },1147 {1148 "name": "stdout",1149 "output_type": "stream",1150 "text": [1151 "\n",1152 "Version 11 enregistrée.\n",1153 "\n",1154 "=== Résultats métriques ===\n",1155 "Best CV R2 : 0.9397 (± 0.0055)\n",1156 "Test RMSE (kg/ha) : 1712.4748\n",1157 "Test R2 : 0.9436\n",1158 "Test MAPE : 17.75%\n",1159 "Test economic_error_usd_ha : 274.4312\n",1160 "\n",1161 "🏃 View run RandomForest - GridSearchCV at: http://127.0.0.1:5000/#/experiments/5/runs/5d7e084d2b0b43bba6d7d2a9802d8a11\n",1162 "🧪 View experiment at: http://127.0.0.1:5000/#/experiments/5\n",1163 "Pipeline sauvegardé : ../model/randomforest_best_pipeline.joblib\n",1164 "Best params : {'model__max_depth': 25, 'model__max_features': 0.7, 'model__min_samples_split': 2, 'model__n_estimators': 200}\n"1165 ]1166 }1167 ],1168 "source": [1169 " # Modèle de RandomForest\n",1170 "model = RandomForestRegressor(random_state=42, n_jobs=1)\n",1171 "model_name = \"RandomForest - GridSearchCV\"\n",1172 "param_grid = {\n",1173 " 'model__n_estimators': [200],\n",1174 " 'model__max_depth': [25],\n",1175 " 'model__min_samples_split': [2],\n",1176 " 'model__max_features': [0.7]\n",1177 "}\n",1178 "tags = \"RandomForest - GridSearchCV\"\n",1179 "projet_description = \"L'objectif est de trouver les meilleurs paramètres possibles pour ce modèle\"\n",1180 "results = mlflow_tracking_gridsearch(model, model_name,param_grid, tags, projet_description)\n",1181 "\n",1182 "best_pipeline = results[\"best_pipeline\"]\n",1183 "\n",1184 "# Sauvegarde explicite du meilleur pipeline pour SHAP plus tard\n",1185 "joblib.dump(best_pipeline, \"../model/randomforest_best_pipeline.joblib\")\n",1186 "\n",1187 "print(\"Pipeline sauvegardé : ../model/randomforest_best_pipeline.joblib\")\n",1188 "print(\"Best params :\", results[\"best_params\"])"1189 ]1190 },1191 {1192 "cell_type": "code",1193 "execution_count": 6,1194 "id": "a789374d",1195 "metadata": {},1196 "outputs": [1197 {1198 "name": "stderr",1199 "output_type": "stream",1200 "text": [