
GoodAir (Phase 2) : Prédiction de la qualité de l'air (Cas d'usage IA)
Jul 2026Développement d'un modèle de Machine Learning pour prédire l'indice de qualité de l'air (AQI) à 6 heures, à partir des données du pipeline GoodAir et de données météo complémentaires (Open-Meteo).
Contexte
Ce projet s’inscrit dans le cadre de ma formation en ingénierie des données, menée avec mes collègues. Il est structuré en plusieurs phases : la construction d’un pipeline data, puis l’exploitation des données issues de ce pipeline. Cette année, nous avons travaillé sur ces deux premières phases : la construction du pipeline data (lien vers la documentation du pipeline) et l’exploitation des données, elle-même divisée en deux volets: un cas d’usage IA et un cas d’usage Analyste. Cette documentation couvre la partie sur laquelle nous avons travaillé : le cas d’usage IA.
L’entreprise fictive TotalGreen, qui opère dans le secteur des énergies renouvelables, a décidé de développer son pôle R&D en créant GoodAir : un laboratoire de recherche pour étudier la qualité de l’air et de l’eau en France.
L’objectif de GoodAir est de réaliser des analyses plus poussées, par exemple des prédictions sur les prochaines canicules ou une modélisation de la saisonnalité des données liées à la qualité de l’air. Il nous revient donc d’anticiper ces besoins dans la réalisation de nos livrables : c’est l’objectif de cette partie IA.
Cahier des charges
Avant toute mise en place d’une stratégie, il a fallu analyser le cahier des charges afin d’en extraire les contraintes imposées par le jury devant lequel nous devons présenter ce projet.
Le jury nous a fixé des contraintes, utilisées comme benchmark pour valider le projet. Je précise qu’en Data Science ou en Machine Learning, le résultat obtenu peut être un faux positif ou un faux négatif (je détaillerai ce point plus loin) : il faut donc être capable de justifier pourquoi un résultat est atteint, ou pourquoi il ne l’est pas et comment y remédier, pour être parfaitement cohérent.
Les contraintes du jury sont les suivantes :
- Score minimal : un R² (coefficient de détermination, détaillé plus loin) strictement supérieur à 0.5.
- Données complémentaires obligatoires : en plus des données déjà disponibles, nous devions utiliser des données complémentaires.
- Conformité RGPD : utilisation exclusive de sources de données issues d’un pays européen respectant naturellement le RGPD.
Ces contraintes ne sont pas qu’un simple benchmark : elles orientent aussi la sélection des algorithmes potentiels, la méthode d’apprentissage et les méthodes à utiliser pour créer ou sélectionner le modèle le plus adapté et ce, avant même d’avoir défini le cas d’usage ou sélectionné la donnée complémentaire.
Analyse de notre source de données existante
Avant de définir le cas d’usage ou la donnée complémentaire, nous avons commencé par analyser notre source de données existante, afin d’en connaître les forces et les contraintes.
Ce projet a été lancé début mars, et le pipeline a été mis en place et lancé précisément le 29 mars 2026 ; il est toujours en cours à ce jour. Le développement de la partie IA, en revanche, a été réalisé début juillet : notre modèle a donc été entraîné entièrement sur les données allant de fin mars à début juillet.
Premier constat : le manque de données. Nous sommes sur un volume qui n’est pas très important. Mathématiquement, depuis mars 2026, avec 11 villes et un run horaire :
~3 mois × 30 jours × 24h × 11 villes ≈ 23 000 lignes
Points forts : nos données sont fiables, car le pipeline a été développé selon les bonnes pratiques, avec des contraintes d’unicité et de véracité bien mises en place. Elles respectent également le RGPD. Pour en savoir plus sur les principes de développement du pipeline, je vous invite à consulter la documentation dédiée (lien vers la documentation du pipeline).
Points faibles (contraintes à prendre en compte pour la partie IA) :
- Infrastructure locale : dans cette première phase, le projet n’est pas hébergé sur un VPS, mais tourne entièrement sur un ordinateur local. Le projet GoodAir est un projet académique qui s’appuie sur une infrastructure hébergée localement (ordinateur personnel) pour ce premier MVP. Bien que la collecte de données ait été globalement très régulière et performante sur ces 3 mois, un tel matériel ne peut raisonnablement garantir une disponibilité absolue 24/7 (déplacements pour aller au campus, week-ends d’absence, mises en veille). Par conséquent, notre historique contient quelques trous temporels. Autrement dit, certaines heures sont manquantes dans notre historique, ce qui est un biais à prendre en compte pour la modélisation.
- Biais de saisonnalité : nos données ne couvrent exclusivement que le printemps et le début de l’été. C’est un biais que nous devrons combler au fil du temps, en mettant en place des stratégies de réentraînement du modèle (automatique ou manuel), dans la logique d’un MVP.
- NULL légitimes : certaines villes n’ont pas de capteur pour certains polluants. Lorsqu’un capteur physique n’est pas présent, les données correspondantes sont naturellement absentes. Ces NULL ne sont donc pas des problèmes de qualité de données, mais des NULL légitimes et justifiés.
Cadrage technique
Suite à l’analyse du cahier des charges et de nos données actuelles, nous avons répondu aux questions suivantes pour cadrer le projet :
Machine Learning ou Deep Learning ?
Réponse : Machine Learning classique (imposé techniquement). Justification (volume de données) : nous disposons de volumes modérés (quelques milliers d’enregistrements par ville). Le Deep Learning (réseaux de neurones) nécessite des millions de données pour être performant, il est donc exclu.
Apprentissage supervisé ou non supervisé ?
Réponse : Supervisé. Justification : nous cherchons à prédire une variable cible spécifique, que nous connaissons déjà dans notre historique (ex : le taux d’AqiGlobal).
Ai-je besoin d’expliquer facilement les prédictions ?
Réponse : Oui, exigence d’explicabilité forte. Justification : les utilisateurs finaux sont des chercheurs GoodAir. Ils n’ont pas seulement besoin d’une prédiction, ils doivent comprendre pourquoi (ex : “Le modèle prédit un pic car le vent est tombé et nous sommes un jour ouvré”).
Quel niveau de performance est réellement nécessaire ?
Réponse : un score de prédiction strictement supérieur à 0.5.
Ce cadrage n’a pas été fait d’un seul coup en amont : c’est le résultat d’un processus itératif, avec des erreurs corrigées au fur et à mesure. La méthodologie à laquelle nous sommes arrivés reste simple, surtout pour un projet de ce type. Avant tout développement ou toute sélection d’algorithme, il faut définir un cas d’usage ; mais avant de définir un cas d’usage, il faut comprendre la nature de la matière première, qui est la donnée ; et avant même d’analyser la donnée, il faut comprendre les contraintes et les attendus. C’est avec cette vision que nous avons avancé, en répondant à des questions spécifiques pour définir un périmètre clair dès le départ.
Phase 1 : Définition d’un cas d’usage
Cas d’usage ML : Prédiction de l’AQI Global
1. La question métier
“Quel sera le niveau global de qualité de l’air dans les prochaines heures pour chaque ville surveillée ?”
Les chercheurs de GoodAir veulent anticiper les pics de pollution, pas juste les constater. Une courbe de prévision AQI sur 6h leur permet de déclencher des alertes préventives et de planifier les activités en extérieur.
2. Variable cible Y
AqiGlobal, indice de qualité de l’air global, valeur continue, fourni par AQICN (data.aqi). OpenWeatherMap ne fournit aucun indice de qualité de l’air dans son plan gratuit.
Règle métier : si AQI prédit > 100 -> “Qualité de l’air dégradée prévue”.
Ce choix de cas d’usage ne s’est pas fait de façon anodine : il s’est basé sur le niveau de faisabilité, la nature de nos données et leurs contraintes. Je précise qu’au début, nous visions une prédiction sur 24 heures. Mais en raison des contraintes de nos données, et d’une découverte faite lors de la modélisation (détaillée plus bas), nous avons dû réduire ce cas d’usage à une prédiction sur 6h. C’est l’un des détails qui a rendu notre modèle le plus efficace possible.
Ce n’est pas non plus le seul cas d’usage envisagé : nous en avons eu plusieurs, et avons sélectionné le plus adapté en fonction de la faisabilité. À la suite de la définition de ce cas d’usage, nous nous sommes posé plusieurs questions pour rester alignés avec les contraintes du jury :
- Quelle donnée complémentaire pourrait donner vie à cette prédiction ?
- Est-il possible de prédire l’AQI ? Nous avons démontré que oui.
- Quel est le type de données complémentaires dont nous avons besoin ?
Nous devions obtenir une source de données complémentaire offrant un historique compatible avec la granularité de nos données actuelles, et permettant aussi d’obtenir des données futures pour nourrir nos prédictions. C’était l’étape la plus importante de la sélection de la source complémentaire.
La donnée complémentaire : Open-Meteo
Source gratuite, sans clé API, proposant une architecture hybride idéale (même endpoint, mêmes champs entre l’entraînement et la production).
Phase entraînement -> CSV historique
https://archive-api.open-meteo.com/v1/archive?latitude=48.85&longitude=2.35&start_date=2026-03-29&end_date=2026-04-01&hourly=wind_direction_10m,cloud_cover,precipitation&format=csv
Phase production -> même API
https://api.open-meteo.com/v1/forecast?latitude=48.85&longitude=2.35&hourly=wind_direction_10m,cloud_cover,precipitation,uv_index
Les noms de colonnes sont identiques : le code de feature engineering s’applique sans modification entre entraînement et production.
Audit de la source complémentaire
Comme pour toute nouvelle source de données, nous nous sommes posé les questions fondamentales suivantes afin de valider Open-Meteo comme donnée complémentaire :
- Quelles sont les caractéristiques les plus essentielles de cette source ?
Réponse : un JSON structuré avec deux objets :
hourly_units(les unités de chaque variable) ethourly(les tableaux de valeurs parallèles indexés partime). Le format est simple et cohérent, sans imbrication complexe comparé à OpenWeatherMap et AQICN. - Comment les données sont-elles stockées dans le système source ? Réponse : le modèle calcule les prévisions à la demande. L’API Forecast retourne les N prochaines heures depuis l’instant de l’appel. L’API Archive retourne l’historique reconstitué par le modèle pour n’importe quelle période passée.
- Quel est le niveau de consistance et la fréquence des erreurs ?
Réponse : très élevé. Le modèle calcule toujours une valeur : il n’y a pas de valeur NULL liée à un capteur en panne. Les erreurs sont exclusivement des erreurs réseau ou de surcharge serveur (503). Dans notre pipeline, ces erreurs sont gérées par le mécanisme de retry Airflow (
retries=2). - Si le schéma change (Schema Drift), comment gérons-nous cela ?
Réponse : pour le moment, on sauvegarde le JSON complet en Bronze sans filtrage. Le code de
feature_engineering.pyagit comme un contrat de données (data contract) strict sur les clés attendues. - À quelle fréquence les données sont-elles extraites ?
Réponse : une fois par heure, en même temps qu’OpenWeatherMap et AQICN, dans la même task
extract_bronze.
En savoir plus sur l’audit complet de la source
Présélection des algorithmes
Nous sommes sur de l’apprentissage supervisé et nous faisons du Machine Learning. Les algorithmes potentiels présélectionnés sont : ARIMA, SARIMA, Facebook Prophet, la régression linéaire, K-Nearest Neighbors Regressor (KNN), Random Forest et XGBoost.
Ce choix n’est pas anodin : nous sommes uniquement sur de la régression, pas de la classification - c’est logique, puisque le R² (coefficient de détermination) est utilisé pour la prédiction d’un chiffre continu, et non pour une classification.
Concernant ARIMA, SARIMA et Facebook Prophet : ce sont des algorithmes conçus spécifiquement pour la prédiction de séries temporelles, et nos données en sont. Donc pour le moment, ce sont d’excellents candidats.
- Linear Regression : Baseline obligatoire. Teste si la relation entre les features météo (Temperature, cloud_cover, precipitation, uv_index) et l’AQI est linéaire. Résultat attendu : R² moyen, car les relations sont souvent non linéaires.
- Polynomial Regression : Extension de la régression linéaire avec des termes quadratiques ou cubiques. Utile si l’AQI réagit de façon non linéaire à la température (courbe en U : froid extrême = pollution, chaleur extrême = ozone). Risque de surapprentissage si le degré est trop élevé.
- Random Forest Regressor : Très adapté ici, les features sont hétérogènes (vent, précipitations, heure, ville). Random Forest gère naturellement les interactions entre features sans normalisation. Il fournit aussi une
feature_importancequi permet de montrer au jury quelle variable météo explique le plus l’AQI. - Gradient Boosting (XGBoost) : La saisonnalité de l’AQI (été = ozone, hiver = particules) est exactement le type de pattern non linéaire que le Gradient Boosting capture bien.
- K-Nearest Neighbors Regressor (KNN) : Prédit l’AQI d’une heure donnée en cherchant les N heures historiques les plus similaires en termes de conditions météo. Intuitif à expliquer (“cet AQI ressemble aux jours où il faisait chaud, humide et sans vent”). Peu performant sur des datasets très larges.
- ARIMA / SARIMA / Prophet : Algorithmes de séries temporelles. ARIMA est simple mais limité aux patterns linéaires (c’est à dire, les relations entre les variables sont supposées linéaires pour qu’il puisse fonctionner). SARIMA ajoute la saisonnalité, mais reste limité aux patterns linéaires. Prophet est plus flexible, mais nécessite un volume de données plus important pour être performant.
Nous n’avons pas voulu prendre trop d’algorithmes, en partie pour une question de gestion du temps : il fallait limiter le nombre de modèles à entraîner pour rester efficace. Je précise aussi que ce projet est un MVP amené à itérer : d’autres modèles pourront être testés plus tard. Nous avons donc fait le choix de rester sur un périmètre restreint plutôt que de tout tester en même temps.
Choix final : Random Forest et XGBoost
En nous basant sur notre cas d’usage, nos contraintes techniques et métier, et la nature de nos données, nous avons spécifiquement retenu Random Forest et XGBoost.
Pourquoi ces deux modèles ? Ce sont, parmi les algorithmes présélectionnés, ceux qui donnent les meilleurs résultats compte tenu du volume de données dont nous disposons. Ils offrent aussi un bon visuel sur les variables les plus importantes, c’est-à-dire celles qui contribuent le plus à l’apprentissage du modèle.
Un point de vigilance sur XGBoost : étant une approche plus fine que les forêts d’arbres classiques, il peut facilement mener à du surapprentissage, notamment parce que nous n’avons pas énormément de données. XGBoost peut être plus performant que Random Forest, mais il faut rester vigilant sur ce risque.
C’est aussi pour ces raisons que nous avons écarté la régression linéaire, les algorithmes de séries temporelles (ARIMA, SARIMA, Prophet) et les autres approches.
La réalité de la collecte IoT (impact sur le choix des features)
Le projet GoodAir est un projet académique et s’appuie sur une infrastructure hébergée localement (ordinateur personnel) pour un premier MVP. Bien que la collecte de données ait été globalement très régulière et performante sur ces 3 mois, un tel matériel ne peut garantir une disponibilité de 24/7 (déplacements pour aller au campus, week-ends d’absence, mises en veille). Par conséquent, notre historique comporte quelques trous temporels (discontinuités).
Nous avons donc écarté les modèles dépendant fortement de la continuité temporelle (comme les régressions avec des lags classiques, voir le tableau ci-dessous), au profit de modèles robustes comme Random Forest ou XGBoost, qui gèrent mieux les données manquantes et les non-linéarités sans nécessiter de combler artificiellement les trous.
| Feature | Problème | Solution |
|---|---|---|
AQI_lag_1 | Si AQI(t-1) est manquant (trou à t-1), la ligne est supprimée → perte de données. | À éviter (sauf à accepter de perdre encore plus de lignes). |
AQI_lag_24 | Même problème, aggravé (24h de trous = suppression de 24 lignes). | À éviter. |
rolling_mean_24h | Nécessite 24h de données consécutives → beaucoup de NaN avec nos trous. | À éviter. |
temp_diff_24h | Différence entre Temperature(t) et Temperature(t-24) → NaN si t-24 est manquant. | À éviter. |
Nous avons pris la décision de ne générer aucune donnée synthétique pour combler ces trous, afin de préserver l’intégrité de la variable cible AQI.
Lors de la fusion des données (jointure) : nous utilisons un INNER JOIN sur les clés (NomVille, DateHeure) entre notre historique GoodAir (incomplet) et l’archive Open-Meteo (complète à 100%). Conséquence : les heures où le pipeline GoodAir était éteint sont naturellement exclues du dataset final. Le volume de données est légèrement réduit, mais reste composé à 100% de vérité terrain (Ground Truth).
Sources de données
fact_mesures.csv(tableGold.FactMesuresde notre data warehouse SQL Server), export manuel via la requête suivante :
SELECT
l.NomVille,
t.DateHeure,
t.Heure,
t.Mois,
f.Temperature,
f.Humidite,
f.Pression,
f.VitesseVent,
f.AqiGlobal,
f.PM25,
f.PM10,
f.NO2,
f.O3,
f.MeteoStatus,
f.AirStatus
FROM Gold.FactMesures f
INNER JOIN Gold.DimLieux l ON f.IDLieu = l.IDLieu
INNER JOIN Gold.DimTemps t ON f.IDTemps = t.IDTemps
ORDER BY t.DateHeure, l.NomVille;
open_meteo_combined.csv(API Archive : téléchargement manuel via l’URL, pour les 11 villes cibles de notre data warehouse) :
# Paris
https://archive-api.open-meteo.com/v1/archive?latitude=48.8534&longitude=2.3488&start_date=2026-03-29&end_date=2026-07-03&hourly=wind_direction_10m,cloud_cover,precipitation&format=csv
# Lyon
https://archive-api.open-meteo.com/v1/archive?latitude=45.7500&longitude=4.5833&start_date=2026-03-29&end_date=2026-07-03&hourly=wind_direction_10m,cloud_cover,precipitation&format=csv
# Lille
...
Architecture : module intégré ou projet externe ?
Avant même de commencer le développement du modèle, nous nous sommes posé une question d’architecture : est-ce que nous devions faire de cette partie IA un projet externe, ou un module du projet actuel ?
Après réflexion, nous avons adapté la partie IA à notre infrastructure actuelle plutôt que de l’externaliser, ce qui était totalement faisable et évitait de complexifier l’ensemble. L’objectif était de faciliter la tâche à la fois côté Data Engineering et côté Data Science, notre modèle devait tourner en même temps que notre pipeline : deux infrastructures séparées auraient compliqué les choses.
Mettre à jour notre structure de projet existante s’est avéré plus simple que prévu, car cette structure avait été conçue dès le départ pour être extensible et facile à améliorer. Cela confirme qu’adopter de bonnes pratiques et de bonnes bases dès le début facilite ensuite l’amélioration et l’adaptation du projet face à de nouvelles problématiques. Nous avons donc décidé d’intégrer plusieurs modules dans notre projet (voir le lien pour plus de détails).
Étapes du cycle de développement du modèle
- Analyse exploratoire (EDA) se déroule en deux temps : d’abord sur chaque source séparément (notebooks 01 et 02), puis sur le dataset combiné (notebook 03). L’objectif est de décrire, visualiser, analyser et comprendre nos données avant tout modèle.
- Préparation des données en fonction des réponses fournies par l’EDA, on prépare les données pour que le modèle puisse facilement les exploiter (transformations, feature engineering).
- Entraînement du modèle se déroule en deux temps : d’abord sur un dataset d’entraînement, puis sur un dataset de validation pour évaluer la performance du modèle.
- Évaluation des résultats des modèles pour comparer les performances de Random Forest et XGBoost, et sélectionner le meilleur modèle pour la production.
- Mise en production en intégrant le modèle directement dans notre pipeline, avec des tâches dédiées pour que tout se fasse automatiquement.
- Amélioration du monitoring du pipeline, en intégrant des alertes.
Analyse exploratoire (EDA) - Synthèse GoodAir
- 10 544 mesures propres sur 3 mois (mars-juillet 2026) pour 11 villes.
- Biais de saisonnalité assumé : printemps et début d’été uniquement.
- Trous temporels confirmés (heures de nuit sous-représentées) ->
TimeSeriesSplitobligatoire. - Les deux APIs sont très fiables (>95% de disponibilité) ; Strasbourg est la ville à surveiller.
Variables avec NULL à surveiller :
O3: 16.43% de NULL globalement, Franconville (100%) et Lyon (88.84%) sans capteur ozone.PM25: 8.16% de NULL, Lyon sans capteur PM25 (88.84%).- Ces NULL sont légitimes (capteurs absents).
Outliers détectés :
AqiGlobal: pic extrême à 434 sur Paris, Rennes et Bordeaux au-dessus de 150 - winsorisation à 99% prévue dans le notebook de préparation des données. Le pic à 434 correspond à un événement de pollution ponctuel à ~10°C (pas une canicule).PM25: valeurs jusqu’à 191, même raisonnement, traitées comme outliers.
Features prometteuses pour le modèle :
PM25(corrélation 0.76 avec AqiGlobal) etPM10(0.59)Temperature(0.27) variable utile, varie beaucoup sur la période.Humidite(-0.19) etO3(0.14) faible mais à conserver.VitesseVent(0.08) etPression(0.06) faible, mais les variables complémentaires d’Open-Meteo (direction du vent, couverture nuageuse) pourraient renforcer ce signal.
Lien du notebook d’analyse exploratoire (EDA) : EDA_GoodAir.ipynb
Analyse exploratoire (EDA) - Synthèse Open-Meteo
- Dataset est complet : 25 608 lignes, sans aucun trou temporel ni valeur manquante — un avantage majeur par rapport à GoodAir.
- Toutes les villes ont exactement 2 328 mesures
- La couverture nuageuse est bimodale (0% ou 100%), la précipitation est quasi toujours à 0 mm, la direction du vent varie fortement selon les villes.
- Les trois variables sont quasi indépendantes entre elles : pas de multicolinéarité à craindre.
Variables avec NULL à surveiller : aucune, les trois variables sont complètes à 100%, aucune imputation nécessaire.
Outliers détectés :
precipitation: des valeurs jusqu’à 13.9 mm, statistiquement des outliers mais physiquement légitimes (épisodes pluvieux intenses). Pas de winsorisation prévue, car la binarisation (0/1) neutralisera leur impact.cloud_coveretwind_direction_10m: aucun outlier, les valeurs couvrent leur plage physique naturelle (0 à 100% et 0° à 360°).
Features prometteuses pour le modèle :
cloud_cover: forte variabilité entre les villes et signal physique clair sur la formation des polluants.wind_direction_10m: profils très différents selon les villes, information complémentaire àVitesseVentdéjà présente dans GoodAir. Encodage sin/cos sur 360° obligatoire.precipitation: sera binarisée (0/1), car sa distribution continue apporte peu de signal ; son effet “lavage de l’air” est un prédicteur physique reconnu.
Lien du notebook d’analyse exploratoire (EDA) : EDA_OpenMeteo.ipynb
Fusion des deux sources - INNER JOIN
Choix de jointure : INNER JOIN entre GoodAir (incomplet) et Open-Meteo (complet à 100%) sur les clés (NomVille, DateHeure). Les heures où le pipeline GoodAir était éteint sont naturellement exclues du dataset final. Le volume sera légèrement réduit, mais le dataset restera composé à 100% de vérité terrain (Ground Truth). Nous avons délibérément choisi de ne générer aucune donnée synthétique pour combler ces trous, afin de préserver l’intégrité scientifique du projet.
Exemple de ce qui se produit lors de la fusion :
GoodAir contient :
2026-03-29 00:00 Paris AQI=35
2026-03-29 03:00 Paris AQI=40
# 01:00 et 02:00 n'existent pas du tout
Open-Meteo contient :
2026-03-29 00:00 Paris wind=270
2026-03-29 01:00 Paris wind=265
2026-03-29 02:00 Paris wind=260
2026-03-29 03:00 Paris wind=255
Observations - Fusion
Aucune ligne n’a été perdue lors de la fusion. Deux explications possibles :
Open-Meteo Archive couvre exactement la même période que GoodAir (2026-03-29 → 2026-07-03) et est complet à 100% : chaque heure de GoodAir a donc son équivalent Open-Meteo (c’est exactement ce que nous avons fait lors du téléchargement des données Open-Meteo : nous avons choisi la même période que GoodAir). Mais dans l’auatre sens, open-Meteo contient des heures que GoodAir n’a pas collectées (ex : 01:00 et 02:00 dans l’exemple ci-dessus). Ces heures sont naturellement exclues lors de la fusion, car elles ne sont pas présentes dans GoodAir. Donc aucune perte de lignes côté GoodAir, mais une perte côté Open-Meteo (ce qui est normal et attendu).
Le
dt.floor('h')a bien aligné les timestamps. Sans cela, on aurait probablement eu beaucoup de lignes exclues à cause des millisecondes, par exemple :- Heure de GoodAir :
2026-03-29 00:00:00.000 - Heure d’Open-Meteo :
2026-03-29 00:00:00
En appliquant
dt.floor('h')sur les deux colonnes, on obtient dans les deux cas2026-03-29 00:00:00.- Heure de GoodAir :
Conclusion : GoodAir a 10 544 heures collectées sur la période. Pour chacune de ces 10 544 heures, Open-Meteo avait la donnée correspondante. Donc 0 perte lors de la fusion : la fusion INNER JOIN s’est faite sans perte de lignes, avec 10 544 lignes à 100% de Ground Truth.
- Les deux sources sont complémentaires et sans contradiction : Open-Meteo n’introduit aucune multicolinéarité problématique avec GoodAir.
- Seules 4 villes sur 11 dépassent le seuil AQI > 100 : les alertes sont des événements rares (0.2% du dataset). La précision des alertes sera une métrique critique, en plus du R².
- Aucune relation linéaire forte entre les variables météo et AqiGlobal, ce qui confirme le choix de Random Forest et XGBoost.
Variables avec NULL à surveiller :
O3: 16.43% de NULL, Lyon (88.84%) et Franconville (100%) sans capteur ozone.PM25: 8.16% de NULL, Lyon (88.84%) sans capteur PM25.
Outliers détectés :
AqiGlobal: pic extrême à 434 sur Paris, valeurs au-dessus de 150 sur Rennes et Bordeaux. Winsorisation à 99% prévue.PM10: maximum à 434, qui coïncide avec le pic AqiGlobal (même événement de pollution).PM25: maximum à 191, même traitement de winsorisation.- Variables Open-Meteo : aucun outlier problématique, leurs plages de valeurs sont physiquement correctes.
Sélection des features
Features retenues pour le modèle :
Temperature,Humidite,Pression,VitesseVent(GoodAir, météo standard)AqiGlobal(variable cible Y)wind_direction_10m,cloud_cover,precipitation(Open-Meteo, complémentaires)Heure,Mois(encodage sin/cos)IsWeekend(feature dérivée)NomVille(One-Hot Encoding)
Features écartées et justification :
PM25,PM10,NO2etO3: ces variables sont les composantes directes de l’indice de qualité de l’air. Les utiliser comme features reviendrait à donner la réponse au modèle. De plus, elles ne sont pas disponibles pour les heures futures, et sont absentes pour certaines villes (Lyon, Franconville). Elles sont corrélées avec AqiGlobal dans l’historique, mais ne peuvent pas servir à prédire l’AQI futur.MeteoStatusetAirStatus: colonnes d’audit pipeline, sans valeur prédictive pour l’AQI futur.DateHeure: remplacée parHeure,MoisetIsWeekend.- Features de lag (
AQI_lag_1,rolling_mean_24h) : écartées à cause des trous temporels, qui généreraient trop de valeursNaN.
Note importante sur le data leakage (fuite de données)
Le vrai risque de data leakage identifié ici : PM25, PM10, NO2, O3 sont des indices AQI partiels fournis par AQICN dans le même appel que AqiGlobal. En production, quand on veut prédire AqiGlobal, ces valeurs sont disponibles au même instant : il n’y a donc pas de leakage temporel à proprement parler.
Mais il y a un autre risque : AqiGlobal est calculé par AQICN à partir de PM25, PM10, NO2, O3. Les inclure comme features revient à donner au modèle les composantes du résultat qu’il doit prédire. Le R² serait alors artificiellement très élevé, mais le modèle ne prédirait rien d’utile en production si ces valeurs venaient à manquer.
La vraie question est : en production, PM25, PM10, NO2, O3 seront-elles toujours disponibles au moment de prédire AqiGlobal ? Réponse : non. Lyon n’a jamais PM25 ni O3. Si on les inclut comme features obligatoires, le modèle ne peut pas prédire pour Lyon. Décision finale : on les exclut. La justification correcte n’est donc pas seulement “data leakage”, mais aussi “indisponibilité en production pour certaines villes”.
Lien du notebook sur l’EDA combinée : EDA_Combined.ipynb
Préparation des données
Durant cette étape, nous avons appliqué plusieurs transformations pour rendre les données exploitables par le modèle. Ces transformations sont détaillées ci-dessous.
One-Hot Encoding sur les villes. Le modèle ne comprend que des chiffres. Si on encode Paris=1, Lyon=2, Bordeaux=3, le modèle croira que Lyon vaut deux fois Paris et que Bordeaux en vaut trois fois plus ce qui n’a aucun sens géographiquement. Le One-Hot transforme chaque ville en une colonne binaire (0 ou 1) : le modèle traite ainsi chaque ville comme une entité indépendante, sans relation mathématique artificielle entre elles. C’est justifié, car chaque ville a un profil de pollution unique, lié à sa géographie, son industrie et sa densité de trafic.
Binarisation des précipitations. 75% des heures ont exactement 0 mm de pluie. La différence entre 0.1 mm et 5 mm de pluie n’est pas significative pour la pollution : ce qui compte, c’est de savoir s’il pleut ou non. La pluie lave les polluants de l’air, c’est un effet binaire. En gardant la valeur continue, on donnerait trop d’importance à la quantité de pluie, alors que seule sa présence ou son absence importe. Cela simplifie le signal et réduit le bruit.
Encodage cyclique sin/cos. Sans cet encodage, le modèle penserait que minuit (heure 0) et 23h sont très éloignés, alors qu’ils ne sont séparés que d’une heure. De même, 359° de vent (presque Nord) et 1° (presque Nord aussi) seraient vus comme opposés. Le sin/cos place ces variables sur un cercle où les valeurs extrêmes se rejoignent. C’est indispensable pour toute variable qui “tourne”, comme l’heure, les mois ou la direction du vent.
Est-ce que cela a amélioré le modèle ? Oui, indirectement. Ces transformations n’améliorent pas directement le R², mais elles permettent au modèle d’apprendre des patterns corrects. Sans elles, le modèle aurait appris de fausses relations : par exemple, sans le One-Hot, il aurait cru que Lyon (=2) et Paris (=4) ont une relation mathématique, ce qui aurait introduit du bruit dans les prédictions. Un modèle qui apprend sur des données bien préparées généralise mieux, même si le R² sur les données d’entraînement semble similaire.
La winsorisation Nous avons appliqué pour limité l’imppact des données abberantes sur le modèle. Deux raisons justtifient cette décison:
- Les outliers sont rares et ne représentent pas le comportement normal de l’AQI. Les inclure fausserait le modèle.
- Les outliers sont des événements ponctuels, souvent liés à des conditions météo extrêmes et plus de ça nous disposons uniquement de 03 mois de données.
Si les l’impact des outliers est trop important, le modèle pourrait apprendre à prédire ces événements rares plutôt que le comportement normal de l’AQI. La winsorisation limite les valeurs extrêmes à un certain seuil (Dans notre projet, on applique une winsorisation à 1% et 99% : le pic à 434 est ramené à 76.), réduisant leur influence sur l’apprentissage du modèle. Nous avons choisi de ne pas les supprimer, car ils sont physiquement légitimes et peuvent se reproduire. La winsorisation est un compromis : on garde les données, mais on limite leur impact car les données n’ont pas été collectées sur une période suffisamment longue pour que le modèle apprenne à les gérer correctement.
Observations - Sélection des features
25 features pour 10 526 lignes : le dataset est bien dimensionné pour Random Forest et XGBoost. Le ratio lignes/features est de 421, largement suffisant pour éviter l’overfitting.
Les 14 features météo et temporelles capturent les conditions physiques qui influencent la qualité de l’air. Les 11 colonnes One-Hot NomVille permettent au modèle d’apprendre des comportements spécifiques à chaque ville. AQI_mean_6h apporte la seule dimension temporelle robuste aux trous.
Aucune feature avec NULL obligatoire dans la liste finale. Le dropna(subset=[TARGET]) n’a supprimé aucune ligne supplémentaire au-delà des 18 déjà retirées à l’étape précédente.
Split temporel des données - TimeSeriesSplit
from sklearn.model_selection import TimeSeriesSplit
df_final = df_final.sort_values("DateHeure")
X = df_final[FEATURES]
y = df_final[TARGET]
tscv = TimeSeriesSplit(n_splits=3)
print("Validation des splits temporels")
for i, (train_idx, test_idx) in enumerate(tscv.split(X), 1):
train_dates = df_final["DateHeure"].iloc[train_idx]
test_dates = df_final["DateHeure"].iloc[test_idx]
print(f"Split {i} -> Train : {train_dates.min().date()} à {train_dates.max().date()} ({len(train_idx)} lignes)")
print(f"Test : {test_dates.min().date()} à {test_dates.max().date()} ({len(test_idx)} lignes)")
Résultats :
Split 1 → Train : 2026-03-29 à 2026-04-19 (2633 lignes)
Test : 2026-04-19 à 2026-05-13 (2631 lignes)
Split 2 → Train : 2026-03-29 à 2026-05-13 (5264 lignes)
Test : 2026-05-13 à 2026-06-08 (2631 lignes)
Split 3 → Train : 2026-03-29 à 2026-06-08 (7895 lignes)
Test : 2026-06-08 à 2026-07-03 (2631 lignes)
Observations - Split temporel
Les 3 splits sont parfaitement construits et respectent la chronologie : chaque split train se termine exactement là où le split test commence, sans chevauchement ni fuite de données. Les splits test ont tous exactement 2 631 lignes, ce qui donne une évaluation cohérente et comparable entre les trois splits. Le train grossit progressivement de 2 633 à 7 895 lignes, ce qui permet d’observer si le modèle s’améliore en voyant plus de données.
- Split 1 : le modèle n’a vu que mars et début avril. Il est testé sur fin avril et début mai. C’est le split le plus difficile, car le dataset d’entraînement est le plus petit.
- Split 2 : le modèle a vu mars, avril et début mai. Il est testé sur fin mai et début juin. C’est le split intermédiaire.
- Split 3 : le modèle a vu mars à début juin. Il est testé sur fin juin et début juillet. C’est le split le plus réaliste, car il correspond à ce qui se passera en production.
Le R² du Split 3 sera le plus représentatif de la performance réelle du modèle : c’est celui qu’on retiendra pour comparer Random Forest et XGBoost.
Pourquoi pas un split classique (train_test_split aléatoire) ?
Dataset : [Jan, Fév, Mar, Avr, Mai, Jun]
Après split aléatoire :
Train : [Jan, Mar, Mai, Jun] ← mélangé
Test : [Fév, Avr] ← mélangé
Le problème : le modèle apprend sur juin pour prédire février.
En production c'est impossible : on ne peut pas voyager dans le passé.
Le TimeSeriesSplit (ce qu'on fait) :
Dataset trié chronologiquement : [Mar → Avr → Mai → Jun → Jul]
Split 1 : Train [Mar → Avr] Test [Mai]
Split 2 : Train [Mar → Mai] Test [Jun]
Split 3 : Train [Mar → Jun] Test [Jul]
Le modèle apprend toujours sur le passé pour prédire le futur exactement comme en production.
Récapitulatif des décisions de préparation des données
| Feature brute | Transformation | Feature finale | Justification |
|---|---|---|---|
| Temperature | Aucune | Temperature | Distribution proche normale |
| Humidite | Aucune | Humidite | Bornée 0-100, acceptable |
| Pression | Aucune | Pression | Distribution stable |
| VitesseVent | Aucune | VitesseVent | Distribution acceptable |
| Heure | Sin/Cos /24 | Heure_sin, Heure_cos | Variable cyclique 0-23 |
| Mois | Sin/Cos /12 | Mois_sin, Mois_cos | Variable cyclique 1-12 |
| IsWeekend | Aucune (déjà bool) | IsWeekend | 0/1 déjà exploitable |
| wind_direction_10m | Sin/Cos /360 | wind_dir_sin, wind_dir_cos | Variable circulaire 0-360° |
| cloud_cover | Aucune | cloud_cover | Distribution acceptable |
| precipitation | Binarisation | precipitation_bin | 75% à 0, valeur continue peu informative |
| NomVille | One-Hot Encoding | ville_Paris… | Pas de relation ordinale entre villes |
| AqiGlobal (Y) | Winsorisation 1%/99% | AqiGlobal | Pics légitimes bridés |
| AQI_mean_6h | Rolling 6h sur index datetime | AQI_mean_6h | Feature temporelle robuste aux trous |
| PM25, NO2, O3, PM10 | Exclus | / | Data leakage (composantes de Y) |
| MeteoStatus, AirStatus | Exclus | / | Colonnes d’audit pipeline |
| AQI_lag_1, rolling_mean_24h | Exclus | / | Trous temporels → trop de NaN |
Lien du notebook de préparation des données pour en savoir plus sur les différentes transformations que nous avons appliquées : DataPrep_GoodAir.ipynb
Entraînement et évaluation
Observations - Random Forest
R² moyen : 0.8487, largement au-dessus du seuil exigé (0.5 par le jury). Le modèle explique ~85% de la variance de l’AQI.
Progression des splits :
- Split 1 (train = mars-avril) : 0.8185
- Split 2 (train = mars-mai) : 0.8564
- Split 3 (train = mars-juin) : 0.8711
Interprétation : le R² augmente avec la taille du train, ce qui indique une amélioration lorsque le modèle voit plus de données, et suggère l’absence d’overfitting.
Écart-type : 0.0221, très faible. les scores sont cohérents d’un split à l’autre, le modèle est stable.
Observations - XGBoost
R² moyen de 0.8111, au-dessus du seuil jury de 0.5, mais inférieur à Random Forest (0.8487). La progression entre les splits est visible (≈0.75 → 0.83 → 0.85), ce qui confirme que XGBoost bénéficie de plus de données, mais il démarre moins bien que Random Forest sur le premier split (2 633 lignes d’entraînement seulement).
L’écart-type de 0.0434 est deux fois plus élevé que celui de Random Forest (0.0221), ce qui traduit une moins grande stabilité entre les splits. XGBoost est plus sensible à la quantité de données disponibles, ce qui est cohérent avec son fonctionnement séquentiel par boosting.
Les deux modèles sont valides pour le jury, mais Random Forest est retenu comme modèle final, car il offre un meilleur R² moyen et une plus grande stabilité sur notre historique de 3 mois.
Pourquoi Random Forest plutôt que XGBoost ?
Mathématiquement, et sur des datasets volumineux, XGBoost est généralement supérieur à Random Forest. c’est une réalité reconnue dans la communauté Data Science. Cependant, sur notre projet, les résultats ont été les suivants :
| Modèle | R² moyen | Stabilité (écart-type) |
|---|---|---|
| Random Forest | 0.8487 | 0.0221 |
| XGBoost | 0.8111 | 0.0434 |
Random Forest obtient un meilleur R au carré et une meilleure stabilité sur nos données.
L’explication est simple : XGBoost apprend de manière séquentielle en corrigeant progressivement ses erreurs. Cette approche nécessite beaucoup de données pour exprimer tout son potentiel. Sur notre premier split, qui contient seulement 2 633 lignes d’entraînement, XGBoost obtient un R au carré de 0.75 contre 0.82 pour Random Forest : il manque de données pour bien apprendre.
Random Forest, quant à lui, construit plusieurs centaines d’arbres de décision indépendants et fait la moyenne de leurs prédictions. Cette approche par consensus est naturellement plus robuste sur de petits datasets avec des trous temporels et c’est exactement notre situation.
En résumé, sur un historique de 3 mois avec des discontinuités, Random Forest est plus adapté que XGBoost. Avec 2 ans de données continues, XGBoost aurait probablement été plus performant.
Feature Importance
importance = (
pd.Series(best_model.feature_importances_, index=FEATURES)
.sort_values(ascending=True)
.tail(10)
)
plt.figure(figsize=(10, 6))
importance.plot(kind="barh", color="steelblue", edgecolor="white")
plt.title("Top 10 features les plus importantes")
plt.xlabel("Importance relative")
plt.tight_layout()
plt.savefig("../rapports_ml/feature_importance.png")
plt.show()
print("Top 10 features :")
print(importance[::-1].round(4))
Résultat :
AQI_mean_6h 0.7170
ville_Lille 0.1049
ville_Lyon 0.0658
Temperature 0.0220
Humidite 0.0138
wind_dir_sin 0.0135
Pression 0.0114
VitesseVent 0.0089
wind_dir_cos 0.0082
Heure_sin 0.0072
Top 10 des features les plus importantes du modèle
Observations :
AQI_mean_6hdomine massivement à 71.7%. La moyenne glissante de l’AQI sur les 6 dernières heures est de loin la feature la plus prédictive. C’est logique : la qualité de l’air évolue lentement - si l’AQI était à 40 il y a 3 heures, il a peu de chances d’être à 200 maintenant. Le passé récent est le meilleur prédicteur du présent.ville_Lilleà 10.5% etville_Lyonà 6.6%. Ces deux villes ont des comportements AQI très distinctifs : Lyon avec son AQI systématiquement bas (peu de capteurs) et Lille avec ses patterns spécifiques se distinguent suffisamment du reste pour que le modèle leur accorde une importance élevée.Temperatureà 2.2% - troisième variable météo la plus importante, cohérent avec la corrélation de 0.27 observée précédemment.- Variables Open-Meteo :
wind_dir_sin(1.35%) etwind_dir_cos(0.82%) - la direction du vent apporte un signal utile même si modeste. C’est la contribution la plus visible des données complémentaires Open-Meteo.cloud_coveretprecipitation_binn’apparaissent pas dans le top 10, mais contribuent au reste.
Conclusion : AQI_mean_6h est la feature pivot du modèle. C’est une feature temporelle construite à partir de nos propres données, pas une donnée externe. Ce qui confirme que l’historique récent de l’AQI est l’information la plus précieuse pour prédire l’AQI futur. Les variables météo apportent un signal complémentaire, mais secondaire.
Lien du notebook d’entraînement et d’évaluation : Entraînement et Évaluation
Capacité de prédiction du modèle et mise en production
Ce que le modèle fait : le modèle Random Forest prédit l’indice de qualité de l’air (AqiGlobal) à partir des conditions météo actuelles et de l’historique récent de l’AQI.
Horizon de prédiction : 6 heures. La feature la plus importante du modèle est AQI_mean_6h, la moyenne de l’AQI sur les 6 dernières heures disponibles en base. Cette feature ancre le modèle dans le passé récent.
En production, à chaque run horaire Airflow, par exemple à 15h :
AQI_mean_6h= moyenne AQI de 9h à 15h (données réelles dansGold.FactMesures).- Météo H+1 à H+6 = prévisions Open-Meteo Forecast (même appel dans
extract_apis.py). - Le modèle prédit l’AQI pour 16h, 17h, 18h, 19h, 20h et 21h.
- Les 6 prédictions sont écrites dans
Gold.AlertesPredites. - Si AQI prédit > 100 -> Alerte =
'ALERTE', sinon Alerte ='OK'.
Pourquoi pas 24h ? Une prédiction à H+24 nécessiterait AQI_lag_1, AQI_lag_24 et rolling_mean_24h. Ces valeurs n’existent pas encore : on ne peut pas les calculer sans avoir déjà prédit toutes les heures intermédiaires. L’horizon de 6h est le maximum cohérent avec notre architecture et notre feature la plus importante.
Pourquoi 6h est préférable au temps réel ? Une prédiction en temps réel (H+0) ne fait que confirmer ce qui se passe déjà. Une prédiction à H+6 donne aux chercheurs de GoodAir le temps d’agir avant qu’un pic de pollution ne survienne : alerter les populations, planifier des activités en extérieur, préparer des recommandations sanitaires.
Ce qui change dans extract_apis.py en production : l’appel Open-Meteo passe de l’heure courante aux 6 prochaines heures, via le paramètre forecast_hours=6 de l’API Forecast. Le reste du pipeline (transform, load, predict) ne change pas.
# Dans extract_apis.py appel Open-Meteo Forecast pour 6h d'anticipation
params = {
"latitude": latitude,
"longitude": longitude,
"hourly": ",".join(variables),
"forecast_hours": 10, # 10 au lieu de 6 pour absorber le décalage
"timezone": "Europe/Paris",
}
Limites assumées :
- L’horizon de 6h dépend de la qualité des prévisions météo Open-Meteo.
AQI_mean_6hutilise des données réelles passées : si le pipeline a eu des trous dans les 6 dernières heures, la feature sera moins précise.- Le modèle est entraîné sur le printemps/été 2026 : le Continuous Training trimestriel améliorera progressivement la couverture saisonnière.
Intégration en production
Rappel : Notre architecture suit le pattern Médaillon en 3 couches. Cela permet de séparer les données brutes (Bronze), les données nettoyées et enrichies (Silver) et les données finales prêtes à l’analyse (Gold). Stratégie de Stockage
Le modèle entraîné (aqi_model.pkl) est intégré dans le pipeline Airflow existant via deux nouveaux fichiers déjà prévus dans l’architecture :
src/ml/feature_engineering.py- lit le Parquet Silver du créneau courant, appelle l’API Open-Meteo pour les 4 variables complémentaires, applique exactement les mêmes transformations que dans le notebook de préparation des données (sin/cos, log, rolling 6h, One-Hot), et sauvegarde le dataset de features danssilver/features-ml/.src/ml/predict.py- chargeaqi_model.pkl, lit les features depuissilver/features-ml/, génère les prédictionsAqiGlobalpour chaque ville, applique la règle métier (AQI prédit > 100 -> ALERTE), et écrit les résultats dansGold.AlertesPredites.
Nouvelles sous-zones Silver créées pour la phase ML :
silver/features-ml/- features prêtes à l’emploi pour le modèle de prédiction, générées parfeature_engineering.pyà chaque run horaire.- Format : Parquet
- Partitionnement :
features-ml/year=/month=/day=/hour=/features.parquet - Contenu : les 25 features transformées (encodages sin/cos, One-Hot,
AQI_mean_6h) pour les 6 prochains créneaux horaires de chaque ville, soit 66 lignes maximum par run. - Durée de vie : 48h, écrasé à chaque run via le partitionnement horaire.
silver/rejet-ml/- capture les données qui n’ont pas pu être traitées par le feature engineering, pour analyse ultérieure et amélioration du modèle.- Format : JSON
- Partitionnement :
rejet-ml/year=/month=/day=/hour=/{ville}.json - Contenu : la ville concernée, la raison du rejet (Silver manquant, Open-Meteo indisponible, aucun créneau futur disponible) et les données disponibles au moment du rejet.
- Usage : permet d’identifier les villes et les heures problématiques, pour améliorer la robustesse du pipeline et enrichir le dataset d’entraînement futur.
Alerting Airflow. Objectif : notifier par email en cas de problème sur le pipeline. Deux types d’alertes :
- Alerte technique une task Airflow échoue (API down, SQL Server injoignable, modèle corrompu).
- Alerte métier AQI prédit > 100 pour une ou plusieurs villes.
Ce qui pourra être amélioré
- Continuous Training : réentraîner le modèle régulièrement pour améliorer progressivement la couverture saisonnière, actuellement limitée au printemps/été 2026.
- Migration vers une infrastructure hébergée (VPS) : pour garantir une disponibilité 24/7 et réduire les trous temporels liés à l’infrastructure locale.
- Suivi plus fin des trous temporels, pour fiabiliser encore la robustesse du pipeline face aux discontinuités.
Compétences mises en avant
- Cadrage d’un cas d’usage Machine Learning à partir d’un cahier des charges contraignant (score minimal, RGPD, données complémentaires obligatoires).
- Audit et sélection d’une source de données complémentaire (Open-Meteo), avec analyse de sa fiabilité et de sa cohérence entre phase d’entraînement et phase de production.
- Présélection et comparaison raisonnée d’algorithmes de régression (ARIMA, SARIMA, Prophet, Régression linéaire, KNN, Random Forest, XGBoost).
- Feature engineering adapté à des contraintes réelles : encodage cyclique (sin/cos), One-Hot Encoding, binarisation, gestion de valeurs manquantes légitimes.
- Validation temporelle rigoureuse avec
TimeSeriesSplit, pour éviter toute fuite d’information passé/futur. - Identification et prévention du data leakage (exclusion des composantes directes de la variable cible).
- Interprétation de la Feature Importance pour rendre le modèle explicable à un public non technique (le jury, les chercheurs GoodAir).
- Intégration d’un modèle ML dans un pipeline de données existant (Airflow), avec gestion des rejets et du monitoring.
Récapitulatif des apprentissages
- Le cahier des charges et les contraintes de données orientent le choix technique bien avant l’algorithme lui-même : définir le cas d’usage, comprendre les données, puis seulement sélectionner un modèle.
- Les contraintes d’infrastructure (infra locale, trous temporels) ont un impact direct sur le choix des features et des algorithmes, et pas seulement sur la qualité des données : elles ont justifié l’exclusion des features de lag classiques et des modèles de séries temporelles.
- Un R² élevé ne suffit pas à valider un modèle : il faut aussi vérifier l’absence de data leakage et la disponibilité réelle des features en production (cas de PM25/PM10/NO2/O3, exclues malgré leur forte corrélation).
- La stabilité d’un modèle entre plusieurs splits temporels est un critère aussi important que sa performance moyenne : c’est ce qui a fait pencher la balance vers Random Forest plutôt que XGBoost sur ce projet.
- Adopter une structure de projet extensible dès le début facilite l’intégration de nouvelles phases (ici, l’ajout du module IA dans un pipeline existant).
- La feature la plus importante n’est pas toujours une donnée externe :
AQI_mean_6h, construite à partir de l’historique propre du pipeline, s’est révélée plus prédictive que les données météo complémentaires.
Liens pertinents et ressources
- Documentation de Open-Meteo : API météo gratuite, fiable et complète, utilisée pour enrichir le dataset GoodAir.
- Documentation de Scikit-Learn - TimeSeriesSplit : documentation officielle de la validation temporelle utilisée pour évaluer les modèles.
- Documentation de XGBoost : documentation officielle de l’algorithme de boosting utilisé pour la régression.
- Documentation de Apache Airflow : documentation officielle de l’orchestrateur de pipeline utilisé pour automatiser le flux de données.
- Dépot GitHub du projet : code source complet, Docker-compose, scripts SQL, et documentation.
- Les images des résultats du projet : captures d’écran des tables SQL, des fichiers Parquet, et des logs Airflow.
- Audit de la nouvelle source Open-Meteo : découverte, analyse du payload et mapping des variables
- Benchmark des outils du projet : comparaison des différentes technologies envisagées pour chaque composant du pipeline, et justification des choix finaux.
- Le modèle de prédiction AQI : analyse des performances du modèle, limites et stratégies d’amélioration.