
Introduction¶
1. Contexte et problématique métier¶
Dans le domaine de la gestion environnementale, la cartographie précise des ressources forestières est un enjeu majeur. Les agences gouvernementales doivent identifier les types de forêts pour mener à bien des missions critiques, telles que la prévention des incendies ou la planification de l’exploitation du bois.
Cependant, la classification des essences d’arbres est complexe. Les frontières entre les différents types de végétation sont souvent floues et dépendent de nombreuses variables géographiques. Une erreur de classification peut avoir des conséquences lourdes, comme l’autorisation d’une coupe de bois dans une zone écologique sensible.
L’objectif de ce projet est de proposer une solution d’aide à la décision fiable. Il ne s’agit pas seulement de prédire le type de forêt, mais de quantifier l’incertitude de chaque prédiction pour sécuriser les décisions de gestion.
2. Présentation des données¶
Pour répondre à cette problématique, nous utilisons le jeu de données Forest Cover Type (disponible sur Kaggle). Ce jeu de données volumineux (plus de 500 000 échantillons) contient des mesures cartographiques telles que :
L’altitude et la pente.
L’exposition au soleil.
La distance aux points d’eau et aux routes.
La nature du sol.
La tâche consiste à réaliser une classification multi-classe pour prédire l’une des 7 catégories de couverture forestière (par exemple : “Spruce-Fir”, “Lodgepole Pine”, etc.). Ce jeu de données est particulièrement pertinent car il présente de nombreux cas ambigus où les classes se chevauchent, ce qui met à l’épreuve les algorithmes classiques.
3. Approche méthodologique : La prédiction conforme¶
Les modèles de classification traditionnels fournissent une réponse unique, parfois erronée, sans indiquer clairement leur niveau de doute.
Pour pallier ce manque, nous intégrons plusieurs méthodes issues de la prédiction conforme. Ces méthodes transforment la prédiction simple en un ensemble de prédictions avec une garantie statistique de fiabilité.
L’application concrète se déroule ainsi :
Zones de confiance élevée : Si l’algorithme ne prédit qu’une seule classe (ex : {Spruce-Fir}), la cartographie est validée automatiquement.
Zones d’incertitude : Si l’algorithme hésite, il renvoie plusieurs classes possibles (ex : {Spruce-Fir, Lodgepole Pine}). Ces zones sont alors signalées comme “à vérifier”.
Cette approche permet d’optimiser le travail des experts. Au lieu de vérifier toute la carte, ils peuvent concentrer leur analyse sur les zones identifiées comme ambiguës par le modèle.
4. Applicabilité de la prédiction conforme¶
Pour bénéficier des garanties théoriques de la prédiction conforme, il faut que les données respectent l’hypothèse d’échangeabilité. Dans le contexte de la cartographie forestière, cette hypothèse est raisonnable car les échantillons sont collectés de manière aléatoire à travers différentes régions géographiques. De plus les données sont réparties aléatoirement entre les ensembles d’entraînement, de calibration et de test.
Toutefois, il faut rester vigilant aux effets d’auto-corrélation spatiale qui pourraient biaiser les résultats si des échantillons proches géographiquement sont inclus dans différents ensembles. Dans notre cas, la grande taille du jeu de données et l’absence de données géographiques précises minimisent ce risque.
import polars as pl
import polars.selectors as cs
from kagglehub import KaggleDatasetAdapter, dataset_load
df = dataset_load(
adapter=KaggleDatasetAdapter.POLARS,
handle="uciml/forest-cover-type-dataset",
path="covtype.csv",
).collect()
df# Downcast binary columns to Boolean
binary_cols = cs.starts_with("Wilderness_Area") | cs.starts_with("Soil_Type")
df = df.with_columns(binary_cols.cast(pl.Boolean))Statistiques descriptives¶
Le jeu de données contient des variables numériques continues (ex: Elevation, Aspect, Slope, etc.) et des variables binaires (ex: Wilderness_Area1, Soil_Type1, etc.) mais aucune variable catégorielle autre que la variable cible Cover_Type.
df.glimpse()Rows: 581012
Columns: 55
$ Elevation <i64> 2596, 2590, 2804, 2785, 2595, 2579, 2606, 2605, 2617, 2612
$ Aspect <i64> 51, 56, 139, 155, 45, 132, 45, 49, 45, 59
$ Slope <i64> 3, 2, 9, 18, 2, 6, 7, 4, 9, 10
$ Horizontal_Distance_To_Hydrology <i64> 258, 212, 268, 242, 153, 300, 270, 234, 240, 247
$ Vertical_Distance_To_Hydrology <i64> 0, -6, 65, 118, -1, -15, 5, 7, 56, 11
$ Horizontal_Distance_To_Roadways <i64> 510, 390, 3180, 3090, 391, 67, 633, 573, 666, 636
$ Hillshade_9am <i64> 221, 220, 234, 238, 220, 230, 222, 222, 223, 228
$ Hillshade_Noon <i64> 232, 235, 238, 238, 234, 237, 225, 230, 221, 219
$ Hillshade_3pm <i64> 148, 151, 135, 122, 150, 140, 138, 144, 133, 124
$ Horizontal_Distance_To_Fire_Points <i64> 6279, 6225, 6121, 6211, 6172, 6031, 6256, 6228, 6244, 6230
$ Wilderness_Area1 <bool> True, True, True, True, True, True, True, True, True, True
$ Wilderness_Area2 <bool> False, False, False, False, False, False, False, False, False, False
$ Wilderness_Area3 <bool> False, False, False, False, False, False, False, False, False, False
$ Wilderness_Area4 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type1 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type2 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type3 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type4 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type5 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type6 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type7 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type8 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type9 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type10 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type11 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type12 <bool> False, False, True, False, False, False, False, False, False, False
$ Soil_Type13 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type14 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type15 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type16 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type17 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type18 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type19 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type20 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type21 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type22 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type23 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type24 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type25 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type26 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type27 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type28 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type29 <bool> True, True, False, False, True, True, True, True, True, True
$ Soil_Type30 <bool> False, False, False, True, False, False, False, False, False, False
$ Soil_Type31 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type32 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type33 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type34 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type35 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type36 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type37 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type38 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type39 <bool> False, False, False, False, False, False, False, False, False, False
$ Soil_Type40 <bool> False, False, False, False, False, False, False, False, False, False
$ Cover_Type <i64> 5, 5, 2, 2, 5, 2, 5, 5, 5, 5
df.describe()Distribution des classes de la variable cible¶
La variable cible a une classe majoritaire (class 2) qui représente environ 50% des observations. Les classes 4, 5, 6, 7 sont sous-représentés et il peut être difficile pour un modèle de bien les prédire.
df.group_by("Cover_Type").len(name="Count").with_columns(
Proportion=(pl.col("Count") / pl.sum("Count") * 100).round(1)
).sort("Count", descending=True)Reverse OneHotEncoding¶
Le jeu de données est fourni avec les colonnes Soil_Type (40 colonnes) et Wilderness_Area (4 colonnes) déjà encodées en One-Hot. Les conserver sous forme de 44 colonnes booléennes séparées est inefficace pour les modèles basés sur des arbres (qui gèrent mieux les divisions catégorielles que les divisions binaires sparses).
On procède donc à un reverse One-Hot Encoding pour regrouper ces colonnes en deux colonnes catégorielles Soil_Type (40 catégories) et Wilderness_Area (4 catégories).
# Check mutual exclusivity of one-hot encoded columns
df.select(pl.sum_horizontal(cs.boolean())).unique().item()2def reverse_ohe(boolean_columns: pl.Expr) -> pl.Expr:
"""Reverse one-hot encoding for a group of mutually exclusive boolean columns."""
return (
pl.concat_list(boolean_columns)
.list.arg_max()
.cast(pl.String)
.cast(pl.Categorical)
)
# Apply to Soil and Wilderness
df = df.with_columns(
reverse_ohe(cs.starts_with("Soil_Type")).alias("_Soil_Type"),
reverse_ohe(cs.starts_with("Wilderness_Area")).alias("_Wilderness_Area"),
).drop(cs.starts_with("Soil_Type") | cs.starts_with("Wilderness_Area"))
df.head()Sauvegarde des données nettoyées¶
df.write_parquet("../../data/forest_cover_clean.parquet")