Compare commits
14 Commits
3a1f7e2a7e
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| d7f61ca93a | |||
| f4bdbe2c7f | |||
| 8b26c0bf17 | |||
| 2495e9365e | |||
| 7ba4287399 | |||
| 9a393972eb | |||
| ccd2195d27 | |||
| 18afeb1e8b | |||
| 1932938fd6 | |||
| 2ff719107b | |||
| a36157b52f | |||
| df7fbf07ed | |||
| 8979f48c23 | |||
| a4d3ce7b49 |
@@ -5,3 +5,10 @@ INFLUXDB_BUCKET=weather
|
||||
STATION_LATITUDE=
|
||||
STATION_LONGITUDE=
|
||||
STATION_ELEVATION=
|
||||
|
||||
# Chronos (prévision)
|
||||
# CHRONOS_MODEL=amazon/chronos-t5-small
|
||||
# CHRONOS_CONTEXT=336
|
||||
# CHRONOS_HORIZON=96
|
||||
# CHRONOS_RESAMPLE=1h
|
||||
# CHRONOS_SAMPLES=20
|
||||
|
||||
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
data
|
||||
__pycache__
|
||||
.DS_Store
|
||||
blog/
|
||||
scripts/build_blog_from_docs.py
|
||||
@@ -2,7 +2,8 @@
|
||||
|
||||
## Installation de l'environnement de base
|
||||
|
||||
Après avoir cloné le dépôt :
|
||||
Après avoir cloné le dépôt, on commence par préparer un environnement Python isolé pour ne pas polluer le système global et pouvoir supprimer facilement tout ce qui concerne ce projet.
|
||||
L’idée est de regrouper au même endroit le moteur [`Python`](https://www.python.org/), les bibliothèques scientifiques et le client pour la base de données [`InfluxDB`](https://www.influxdata.com/), qui stocke nos séries temporelles.
|
||||
|
||||
```shell
|
||||
python3 -m venv .venv
|
||||
@@ -12,11 +13,11 @@ pip install -r requirements.txt
|
||||
python -c "import pandas, influxdb_client, sklearn; print('OK')"
|
||||
```
|
||||
|
||||
- On installe l'environnement virtuel de python
|
||||
- On entre dans cet environnement
|
||||
- On met à jour le gestionnaire de paquets pip
|
||||
- On installe les dépendances définies dans `requirements.txt`
|
||||
- On vérifie que les dépendances sont correctement installées
|
||||
- `python3 -m venv .venv` crée un environnement virtuel Python local dans le dossier `.venv`, ce qui permet d’isoler les paquets de ce projet du reste du système (voir par exemple la documentation sur les environnements virtuels [`venv`](https://docs.python.org/fr/3/library/venv.html)).
|
||||
- `source .venv/bin/activate` active cet environnement dans le shell courant ; tant qu’il est actif, la commande `python` pointera vers l’interpréteur de `.venv` et non vers celui du système.
|
||||
- `python -m pip install --upgrade pip` met à jour [`pip`](https://pip.pypa.io/), le gestionnaire de paquets de Python, afin d’éviter les bugs ou limitations des versions trop anciennes.
|
||||
- `pip install -r requirements.txt` installe toutes les dépendances nécessaires au projet (notamment [`pandas`](https://pandas.pydata.org/), le client Python pour InfluxDB et [`scikit-learn`](https://scikit-learn.org/stable/) pour les modèles prédictifs) en suivant la liste déclarée dans `requirements.txt`.
|
||||
- `python -c "import pandas, influxdb_client, sklearn; print('OK')"` vérifie que les principales bibliothèques s’importent correctement ; si la commande affiche `OK`, l’environnement de base est prêt à être utilisé.
|
||||
|
||||
## Configuration
|
||||
|
||||
@@ -24,23 +25,26 @@ python -c "import pandas, influxdb_client, sklearn; print('OK')"
|
||||
cp .env.example .env
|
||||
```
|
||||
|
||||
On copie le fichier de configuration d'exemple, puis on l'ouvre pour l'adapter à notre cas.
|
||||
On copie le fichier de configuration d'exemple, puis on l'ouvre pour l'adapter à notre cas. Ce fichier `.env` sera lu automatiquement par les scripts via [`python-dotenv`](https://github.com/theskumar/python-dotenv) (voir `meteo/config.py`), ce qui évite d’exporter les variables à la main à chaque session.
|
||||
|
||||
- `INFLUXDB_URL` : URL de l'api du serveur InfluxDB2 (cela inclue probablement le port 8086)
|
||||
- `INFLUXDB_TOKEN` : le jeton d'authentification à créer dans votre compte InfluxDB2
|
||||
- `INFLUXDB_ORG` : l'organisation à laquelle le token est rattaché
|
||||
- `INFLUXDB_BUCKET` : le nom du bucket dans lequel les données sont stockées
|
||||
- `STATION_LATITUDE` : latitude GPS de la station météo
|
||||
- `STATION_LONGITUDE` : longitude GPS de la station météo
|
||||
- `STATION_ELEVATION` : altitude de la station météo
|
||||
- `INFLUXDB_URL` : URL de l'API du serveur InfluxDB 2.x (incluant généralement le port 8086), par exemple `http://localhost:8086` ou l’adresse de votre serveur ; c’est le point d’entrée HTTP/HTTPS vers votre base de données de séries temporelles (voir aussi l’introduction aux [bases de données de séries temporelles](https://fr.wikipedia.org/wiki/Base_de_donn%C3%A9es_de_s%C3%A9ries_temporelles)).
|
||||
- `INFLUXDB_TOKEN` : jeton d'authentification généré dans l’interface d’[`InfluxDB 2.x`](https://docs.influxdata.com/influxdb/v2/), associé à un jeu de permissions (lecture/écriture) ; sans ce token, le client Python ne peut pas interroger le serveur.
|
||||
- `INFLUXDB_ORG` : nom de l'organisation InfluxDB à laquelle le token est rattaché ; InfluxDB 2.x organise les ressources (utilisateurs, buckets, tokens) par organisation, il faut donc préciser celle que l’on souhaite utiliser.
|
||||
- `INFLUXDB_BUCKET` : nom du bucket (espace logique de stockage avec sa politique de rétention) dans lequel les données sont enregistrées ; c’est ce bucket que les scripts interrogeront pour récupérer les mesures de la station.
|
||||
- `STATION_LATITUDE` : latitude GPS de la station météo (en degrés décimaux), utilisée plus loin pour les calculs d’élévation solaire et pour enrichir les données avec des métadonnées astronomiques.
|
||||
- `STATION_LONGITUDE` : longitude GPS de la station météo (en degrés décimaux), nécessaire pour les mêmes raisons que la latitude.
|
||||
- `STATION_ELEVATION` : altitude de la station météo (en mètres au-dessus du niveau de la mer) ; cette information affine légèrement certains calculs physiques, mais reste optionnelle si l’altitude est mal connue.
|
||||
|
||||
## Tests de l'environnement de travail
|
||||
|
||||
Avant d’attaquer des analyses plus lourdes, on vérifie que la connexion au serveur InfluxDB fonctionne bien et que la configuration est cohérente.
|
||||
Les scripts de test qui suivent n’écrivent rien dans la base : ils se contentent d’effectuer quelques requêtes simples pour valider l’accès.
|
||||
|
||||
```shell
|
||||
python "docs/01 - Installation, configuration et tests/scripts/test_influx_connection.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Configuration InfluxDB chargée :
|
||||
URL : http://10.0.3.2:8086
|
||||
Org : Dern
|
||||
@@ -57,13 +61,15 @@ Exemple de point :
|
||||
value : humidity
|
||||
```
|
||||
|
||||
Si vous obtenez un résultat similaire (URL affichée, ping OK, requête de test qui retourne quelques enregistrements), c’est que le serveur InfluxDB est joignable, que le token est valide et que le bucket indiqué existe bien.
|
||||
|
||||
Ensuite, on peut demander à InfluxDB de nous détailler ce qu'il stocke :
|
||||
|
||||
```shell
|
||||
python "docs/01 - Installation, configuration et tests/scripts/test_influx_schema.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Bucket InfluxDB : weather
|
||||
|
||||
Measurements disponibles :
|
||||
@@ -115,13 +121,16 @@ Champs pour measurement « °C » :
|
||||
- value (type: unknown)
|
||||
```
|
||||
|
||||
Ce deuxième script interroge le schéma du bucket : il liste les _measurements_ (grandeurs physiques comme `%`, `°C`, `km/h`, etc.), ainsi que les champs associés à chacun.
|
||||
Dans InfluxDB, un _measurement_ correspond en gros à un type de mesure (par exemple une unité), et les _fields_ contiennent les valeurs numériques que l’on exploitera plus tard ; les metadata (comme `entity_id`) sont stockées sous forme de _tags_ (voir la documentation sur le [modèle de données InfluxDB](https://docs.influxdata.com/influxdb/v2/reference/key-concepts/data-elements/)).
|
||||
|
||||
Mais pour obtenir les données dont on a besoin, il faut aussi connaitre les entités manipulées par Influx :
|
||||
|
||||
```shell
|
||||
python "docs/01 - Installation, configuration et tests/scripts/test_influx_entities.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Bucket InfluxDB : weather
|
||||
|
||||
Measurement « % »
|
||||
@@ -202,6 +211,9 @@ Measurement « °C »
|
||||
- station_meteo_bresser_exterieur_temperature
|
||||
```
|
||||
|
||||
Ces informations combinées se retrouvent dans le fichier `meteo/station_config.py` et dans `meteo/variables.py`.
|
||||
Ce dernier script fait le lien avec la source des données : il dresse la liste des clés de tags et des `entity_id` possibles pour chaque _measurement_.
|
||||
Ces identifiants correspondent aux entités exposées par votre système domotique (par exemple [`Home Assistant`](https://www.home-assistant.io/)), et permettent de distinguer clairement l’humidité extérieure, la pression, la vitesse du vent, etc.
|
||||
|
||||
Ces informations combinées se retrouvent dans le fichier `meteo/station_config.py` et dans `meteo/variables.py` : c’est là que l’on fixe, une fois pour toutes, quelles entités InfluxDB seront considérées comme « température extérieure », « pluie », « vent », et sous quels noms elles seront manipulées dans la suite de l’étude.
|
||||
|
||||
On aurait pu se passer de ces scripts pour déterminer la structure des données stockées dans Influx, mais ils évitent de se reposer sur des intuitions : ici, on demande à Influx de nous donner les informations dont on va avoir besoin au lieu de les deviner.
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
# Préparation des données
|
||||
|
||||
Cette étape regroupe l'export initial depuis InfluxDB ainsi que les scripts d'ajustement nécessaires pour obtenir un dataset minuté propre.
|
||||
Cette étape regroupe l'export initial depuis InfluxDB ainsi que les scripts d'ajustement nécessaires pour obtenir un dataset minuté propre, c’est‑à‑dire une [série temporelle](https://fr.wikipedia.org/wiki/S%C3%A9rie_temporelle) où chaque minute possède une observation complète pour toutes les variables utiles.
|
||||
L’objectif est de passer d’un format brut, pensé pour la domotique temps réel, à un format tabulaire lisible par les bibliothèques d’analyse comme `pandas`.
|
||||
|
||||
## Export des données
|
||||
|
||||
@@ -8,10 +9,13 @@ Cette étape regroupe l'export initial depuis InfluxDB ainsi que les scripts d'a
|
||||
python "docs/02 - Préparation des données/scripts/export_station_data.py"
|
||||
```
|
||||
|
||||
Ce script se connecte au serveur InfluxDB configuré au chapitre précédent, interroge les mesures de la station sur une fenêtre récente (par défaut les sept derniers jours) et les exporte dans un fichier CSV `data/weather_raw_7d.csv`.
|
||||
On quitte ainsi la base de données de séries temporelles pour un format beaucoup plus simple à manipuler avec `pandas`, tout en conservant l’horodatage précis.
|
||||
|
||||
La sortie est assez longue, et inclut un certain nombre d'avertissements qui peuvent être ignorés.
|
||||
L'important est que le script se termine sur :
|
||||
|
||||
```output
|
||||
```text
|
||||
✔ Export terminé : /Users/richard/Documents/donnees_meteo/data/weather_raw_7d.csv
|
||||
```
|
||||
|
||||
@@ -25,18 +29,24 @@ Vérifiez que le fichier est bien créé et qu'il contient des données.
|
||||
python "docs/02 - Préparation des données/scripts/export_station_data_full.py"
|
||||
```
|
||||
|
||||
Au lieu de télécharger les données des 7 derniers jours, l'ensemble des données stockées sur le serveur pour ce bucket seront téléchargées, ce qui, selon la granularité et l'ancienneté des données peut prendre un certain temps et occuper un espace disque conséquent.
|
||||
Au lieu de télécharger les données des sept derniers jours, l'ensemble des données stockées sur le serveur pour ce _bucket_ seront téléchargées, ce qui, selon la granularité et l'ancienneté des données, peut prendre un certain temps et occuper un espace disque conséquent.
|
||||
Mon fichier complet contient plus d'un million d'enregistrements et pèse 70 Mo.
|
||||
|
||||
En pratique, il est souvent plus confortable de développer et tester les scripts suivants sur quelques jours de données (fichier `weather_raw_7d.csv`), puis de relancer le pipeline complet sur l’historique complet une fois les étapes stabilisées.
|
||||
Les deux scripts s’appuient sur l’API HTTP d’[InfluxDB 2.x](https://docs.influxdata.com/influxdb/v2/) et son langage de requête Flux (voir la documentation dédiée à [Flux](https://docs.influxdata.com/flux/v0.x/)).
|
||||
|
||||
## Ajustements
|
||||
|
||||
Le CSV exporté reste très proche du format de stockage utilisé par la domotique : chaque capteur envoie une mesure à des instants légèrement différents, et InfluxDB stocke donc plusieurs lignes par seconde, chacune ne contenant qu’une seule variable renseignée.
|
||||
Pour l’analyse statistique, on cherche au contraire à obtenir une table où chaque horodatage regroupe toutes les variables de la station sur une grille temporelle régulière.
|
||||
|
||||
Le fichier peut être rapidement inspecté avec la commande `head` :
|
||||
|
||||
```shell
|
||||
head data/weather_raw_full.csv
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
time,temperature,humidity,pressure,illuminance,wind_speed,wind_direction,rain_rate
|
||||
2025-03-10 09:35:23.156646+00:00,,,996.95,,,,
|
||||
2025-03-10 09:35:23.158538+00:00,10.6,,,,,,
|
||||
@@ -49,7 +59,8 @@ time,temperature,humidity,pressure,illuminance,wind_speed,wind_direction,rain_ra
|
||||
2025-03-10 09:36:22.640356+00:00,,,,,,306.0,
|
||||
```
|
||||
|
||||
On peut voir que HomeAssistant écrit une nouvelle entrée pour chaque capteur, alors qu'on aurait pu s'attendre à une ligne unique pour l'ensemble des capteurs.
|
||||
On peut voir que [Home Assistant](https://www.home-assistant.io/) écrit une nouvelle entrée pour chaque capteur, alors qu'on aurait pu s'attendre à une ligne unique pour l'ensemble des capteurs.
|
||||
C’est un format très pratique pour la collecte temps réel, mais moins confortable pour l’analyse : il faut d’abord tout remettre en forme.
|
||||
|
||||
Le script suivant s'occupe de regrouper les données de capteurs dont l'enregistrement est proche :
|
||||
|
||||
@@ -57,7 +68,7 @@ Le script suivant s'occupe de regrouper les données de capteurs dont l'enregist
|
||||
python "docs/02 - Préparation des données/scripts/format_raw_csv.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Fichier brut chargé : data/weather_raw_full.csv
|
||||
Lignes : 1570931, colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate']
|
||||
Type d'index : <class 'pandas.core.indexes.datetimes.DatetimeIndex'>
|
||||
@@ -66,12 +77,15 @@ Après combinaison (1s) : 630171 lignes
|
||||
```
|
||||
|
||||
Un nouveau document CSV intermédiaire est donc créé.
|
||||
On voit que l’on passe d’environ 1,57 million de lignes à 630 000 lignes « combinées » : le script regroupe les mesures de tous les capteurs tombant dans la même seconde, en utilisant l’horodatage comme clé.
|
||||
L’index de type `DatetimeIndex` indiqué dans la sortie est l’axe temporel standard de `pandas` (voir la documentation de [`pandas.DatetimeIndex`](https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DatetimeIndex.html)).
|
||||
Cette étape correspond à une première forme de rééchantillonnage temporel, classique lorsqu’on prépare une série pour l’analyse.
|
||||
|
||||
```shell
|
||||
head data/weather_formatted_1s.csv
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
time,temperature,humidity,pressure,illuminance,wind_speed,wind_direction,rain_rate
|
||||
2025-03-10 09:35:23+00:00,10.6,83.0,996.95,,7.4,256.0,0.0
|
||||
2025-03-10 09:35:41+00:00,,,,20551.2,,,
|
||||
@@ -84,7 +98,8 @@ time,temperature,humidity,pressure,illuminance,wind_speed,wind_direction,rain_ra
|
||||
2025-03-10 09:40:22+00:00,,,,19720.8,10.0,209.0,
|
||||
```
|
||||
|
||||
Il reste des cellules vides : en effet, HA n'enregistre pas la valeur d'un capteur si elle n'a pas changé depuis la dernière fois.
|
||||
Il reste des cellules vides : en effet, Home Assistant n'enregistre pas la valeur d'un capteur si elle n'a pas changé depuis la dernière fois.
|
||||
On se retrouve donc avec une série temporelle à pas régulier (1 s), mais encore parsemée de trous.
|
||||
|
||||
On fait donc :
|
||||
|
||||
@@ -92,20 +107,22 @@ On fait donc :
|
||||
python "docs/02 - Préparation des données/scripts/fill_formatted_1s.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Fichier 1s formaté chargé : data/weather_formatted_1s.csv
|
||||
Lignes : 630171, colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate']
|
||||
Après propagation des dernières valeurs connues : 630171 lignes
|
||||
✔ Fichier 1s 'complet' écrit dans : /Users/richard/Documents/donnees_meteo/data/weather_filled_1s.csv
|
||||
```
|
||||
|
||||
Cette seconde passe applique un remplissage par propagation de la dernière valeur connue (_forward fill_ ou `ffill` dans `pandas`) : tant qu’un capteur ne publie pas de nouvelle mesure, on considère que la précédente reste valable. C’est une hypothèse raisonnable pour des variables relativement lisses comme la température ou la pression, moins pour des phénomènes très brusques ; l’objectif ici est surtout d’obtenir un _dataset_ sans [valeurs manquantes](https://fr.wikipedia.org/wiki/Donn%C3%A9es_manquantes), ce qui simplifie grandement les analyses et les modèles dans les chapitres suivants.
|
||||
|
||||
## Enrichissements (saisons et position du soleil)
|
||||
|
||||
Une fois les données nettoyées, on peut les enrichir avec des métadonnées météorologiques simples :
|
||||
Une fois les données nettoyées, on peut les enrichir avec des métadonnées météorologiques simples, qui aideront ensuite à interpréter les graphiques et à construire des modèles plus pertinents :
|
||||
|
||||
- regrouper les points par minute,
|
||||
- ajouter la saison correspondant à chaque observation (en fonction de l'hémisphère),
|
||||
- calculer la hauteur du soleil si la latitude/longitude de la station sont configurées.
|
||||
- regrouper les points par minute, pour lisser légèrement le bruit tout en restant réactif ; ce pas de 60 secondes est un bon compromis entre fidélité au signal brut et taille raisonnable du _dataset_ ;
|
||||
- ajouter la saison correspondant à chaque observation (en fonction de l'hémisphère), ce qui permet de comparer facilement les comportements printemps/été/automne/hiver et de relier nos courbes aux notions classiques de [saisons](https://fr.wikipedia.org/wiki/Saison) en météorologie ;
|
||||
- calculer la hauteur du soleil si la latitude/longitude de la station sont configurées, afin de disposer d’une estimation de l’élévation solaire au‑dessus de l’horizon (jour/nuit, midi solaire, etc.) en s’appuyant sur la bibliothèque d’astronomie [`astral`](https://astral.readthedocs.io/).
|
||||
|
||||
Ces opérations sont réalisées par :
|
||||
|
||||
@@ -113,26 +130,34 @@ Ces opérations sont réalisées par :
|
||||
python "docs/02 - Préparation des données/scripts/make_minutely_dataset.py"
|
||||
```
|
||||
|
||||
Le script produit `data/weather_minutely.csv`. Pensez à définir `STATION_LATITUDE`, `STATION_LONGITUDE` et `STATION_ELEVATION` dans votre `.env` pour permettre le calcul de la position du soleil ; sinon, seule la colonne `season` sera ajoutée.
|
||||
Le script produit `data/weather_minutely.csv`. Chaque ligne de ce fichier correspond à une minute, avec toutes les variables météo alignées (température, humidité, pression, vent, pluie, etc.) et, si les coordonnées de la station sont connues, des colonnes supplémentaires comme la saison et l’élévation du soleil.
|
||||
|
||||
> Pensez à définir `STATION_LATITUDE`, `STATION_LONGITUDE` et `STATION_ELEVATION` dans votre `.env` pour permettre le calcul de la position du soleil ; sinon, seule la colonne `season` sera ajoutée.
|
||||
|
||||
Ce fichier minuté est le jeu de données de référence utilisé dans la majorité des chapitres suivants.
|
||||
|
||||
## Pipeline simplifié
|
||||
|
||||
Un script tout simple permet de faire automatiquement tout ce qu'on vient de voir.
|
||||
Un script tout simple permet de faire automatiquement tout ce qu'on vient de voir, dans le bon ordre, sans avoir à lancer chaque étape à la main.
|
||||
Il supprime **tous** les fichiers CSV existants : il faudra donc relancer la génération des images dans les étapes suivantes pour qu'elles intègrent les nouvelles données.
|
||||
|
||||
```shell
|
||||
python -m scripts.refresh_data_pipeline
|
||||
```
|
||||
|
||||
Ce module Python orchestre l’export depuis InfluxDB, la mise en forme à 1 seconde, le remplissage des valeurs manquantes puis la construction du dataset minuté.
|
||||
Le fait de repartir de zéro à chaque exécution garantit que `weather_minutely.csv` reflète bien l’état actuel de la base, au prix d’un temps de calcul un peu plus long.
|
||||
|
||||
## Vérification des données
|
||||
|
||||
On peut s'assurer que plus aucune information n'est manquante :
|
||||
Avant d’explorer les graphiques ou de lancer des modèles, on vérifie que le dataset minuté est cohérent : pas de valeurs manquantes, des ordres de grandeur plausibles, et une grille temporelle effectivement régulière.
|
||||
On peut d’abord s'assurer que plus aucune information n'est manquante :
|
||||
|
||||
```shell
|
||||
python "docs/02 - Préparation des données/scripts/check_missing_values.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Dataset chargé : data/weather_minutely.csv
|
||||
Lignes : 321881
|
||||
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate']
|
||||
@@ -156,13 +181,15 @@ Valeurs manquantes par colonne :
|
||||
✔ Aucune valeur manquante dans le dataset minuté.
|
||||
```
|
||||
|
||||
Ce premier contrôle confirme que toutes les lignes de `weather_minutely.csv` sont complètes : aucune cellule n’est manquante, ce qui évitera bien des subtilités dans les analyses ultérieures.
|
||||
|
||||
Le script suivant nous permet de vérifier rapidement si des problèmes majeurs peuvent être découverts :
|
||||
|
||||
```shell
|
||||
python "docs/02 - Préparation des données/scripts/describe_minutely_dataset.py"
|
||||
```
|
||||
|
||||
```output
|
||||
```text
|
||||
Dataset minuté chargé : data/weather_minutely.csv
|
||||
Lignes : 321881
|
||||
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate'] Période : 2025-03-10 09:35:00+00:00 → 2025-11-17 00:41:00+00:00
|
||||
@@ -216,13 +243,17 @@ Name: count, dtype: int64
|
||||
Nombre d'intervalles ≠ 60s : 17589
|
||||
```
|
||||
|
||||
Ces écarts peuvent être identifiés avec le script suivant :
|
||||
Ce deuxième script fournit un aperçu statistique classique (fonction `describe()` de `pandas`) et quelques min/max datés pour chaque variable : on y vérifie que les valeurs restent plausibles (par exemple pas de température à +80 °C ni de vent négatif) et que les extrêmes correspondent à des dates réalistes.
|
||||
La section sur les différences d’intervalle permet déjà de repérer que certains pas ne sont pas strictement de 60 secondes, ce qui est courant sur des données issues de capteurs, mais qu’il faut garder en tête pour la suite.
|
||||
Ce type de contrôle s’inscrit dans une démarche d’[analyse exploratoire de données](https://fr.wikipedia.org/wiki/Analyse_exploratoire_de_donn%C3%A9es).
|
||||
|
||||
Ces écarts peuvent être identifiés plus finement avec le script suivant :
|
||||
|
||||
```shell
|
||||
python "docs/02 - Préparation des données/scripts/list_time_gaps.py"
|
||||
```
|
||||
|
||||
```
|
||||
```text
|
||||
Dataset minuté chargé : data/weather_minutely.csv
|
||||
Lignes : 321881
|
||||
|
||||
@@ -243,8 +274,9 @@ Top 10 des gaps les plus longs :
|
||||
- De 2025-06-21 17:25:00+00:00 à 2025-06-21 18:10:00+00:00 (durée: 0 days 00:45:00, manquants: 44, de 2025-06-21 17:26:00+00:00 à 2025-06-21 18:09:00+00:00)
|
||||
```
|
||||
|
||||
Ces trous dans les données peuvent correspondre à des pannes de connexion entre la station et mon réseau, un redémarrage de mon serveur (physique ou logiciel), au redémarrage de la box ou du point d'accès sans-fil, etc.
|
||||
Ils peuvent aussi correspondre aux modifications opérées dans les scripts précédents.
|
||||
Ce troisième script ne corrige rien : il se contente de lister précisément les intervalles dans lesquels des minutes manquent dans la série, ainsi que leur durée.
|
||||
Ces trous dans les données peuvent correspondre à des pannes de connexion entre la station et mon réseau, un redémarrage de mon serveur (physique ou logiciel), au redémarrage de la box ou du point d'accès sans‑fil, etc.
|
||||
Mais ils peuvent également correspondre aux modifications opérées dans les scripts précédents !
|
||||
|
||||
Ces scripts sont intéressants parce qu'ils mettent en évidence des facteurs indirects, contribuant à la qualité des données soumise.
|
||||
On peut prendre toutes les précautions, on peut avoir l'intuition d'avoir tout géré, et se rassurer parce qu'on utilise des outils fiables, mais il existera toujours des manques dans les données.
|
||||
|
||||
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 55 KiB |
|
Before Width: | Height: | Size: 49 KiB After Width: | Height: | Size: 49 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
|
Before Width: | Height: | Size: 46 KiB After Width: | Height: | Size: 46 KiB |
|
Before Width: | Height: | Size: 45 KiB After Width: | Height: | Size: 45 KiB |
BIN
docs/03 - Premiers graphiques/figures/humidity_overview.png
Normal file
|
After Width: | Height: | Size: 105 KiB |
BIN
docs/03 - Premiers graphiques/figures/illuminance_overview.png
Normal file
|
After Width: | Height: | Size: 115 KiB |
BIN
docs/03 - Premiers graphiques/figures/pressure_overview.png
Normal file
|
After Width: | Height: | Size: 98 KiB |
BIN
docs/03 - Premiers graphiques/figures/rain_rate_overview.png
Normal file
|
After Width: | Height: | Size: 38 KiB |
BIN
docs/03 - Premiers graphiques/figures/sun_elevation_overview.png
Normal file
|
After Width: | Height: | Size: 90 KiB |
BIN
docs/03 - Premiers graphiques/figures/temperature_overview.png
Normal file
|
After Width: | Height: | Size: 97 KiB |
|
After Width: | Height: | Size: 59 KiB |
BIN
docs/03 - Premiers graphiques/figures/wind_speed_overview.png
Normal file
|
After Width: | Height: | Size: 120 KiB |
@@ -1,40 +1,52 @@
|
||||
# Premiers graphiques
|
||||
|
||||
On peut désormais tracer nos premiers graphiques simples et bruts.
|
||||
S'ils ne sont pas très instructifs par rapport à ce que nous fournissent Home Assistant et InfluxDB, ils nous permettent au moins de nous assurer que tout fonctionne, et que les données semblent cohérentes.
|
||||
Les fichiers CSV correspondant à chaque figure sont conservés dans `data/` dans ce dossier.
|
||||
|
||||
On se limite dans un premier temps aux 7 derniers jours.
|
||||
On peut désormais tracer nos premiers graphiques simples et bruts à partir du dataset minuté construit au chapitre précédent.
|
||||
S'ils ne sont pas encore très instructifs par rapport à ce que nous fournissent déjà Home Assistant et InfluxDB, ils nous permettent au moins de nous assurer que tout fonctionne, que les données semblent cohérentes, et que la chaîne « export → préparation → visualisation » tient la route.
|
||||
Les scripts de ce chapitre s’appuient sur [`pandas`](https://pandas.pydata.org/) et sur la bibliothèque de visualisation [`Matplotlib`](https://matplotlib.org/) (via le module `meteo.plots`) pour produire des graphiques standards : séries temporelles, _heatmaps_, calendriers.
|
||||
Les fichiers CSV correspondant à chaque figure sont conservés dans le sous-dossier `data/` de ce chapitre, ce qui permet de réutiliser directement les séries pré-agrégées si besoin.
|
||||
|
||||
```shell
|
||||
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py"
|
||||
```
|
||||
|
||||

|
||||
Ce script lit `data/weather_minutely.csv`, sélectionne éventuellement une fenêtre temporelle (par exemple les derniers jours si l’on utilise l’option `--days`) puis choisit une fréquence d’agrégation adaptée pour ne pas saturer le graphique en points.
|
||||
Pour chaque variable (température, pression, humidité, pluie, vent, illuminance, élévation du soleil), il applique un style par défaut : courbe continue pour les variables lisses, diagramme en barres pour la pluie, nuage de points pour la direction du vent, etc.
|
||||
L’idée est d’obtenir une première vue d’ensemble de la [série temporelle](https://fr.wikipedia.org/wiki/S%C3%A9rie_temporelle) de chaque variable, sans prise de tête :
|
||||
|
||||

|
||||
- vérifier le cycle quotidien de la température et de l’élévation solaire ;
|
||||
- repérer les paliers ou dérives de capteurs (pression trop plate, humidité coincée, etc.) ;
|
||||
- confirmer que les unités, ranges et horodatages sont plausibles.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Vues calendrier
|
||||
|
||||
Les vues calendrier permettent de visualiser, jour par jour, les cumuls ou moyennes quotidiennes sur la dernière année complète disponible.
|
||||
Les images générées sont stockées dans `figures/calendar/` et les CSV correspondants dans `data/calendar/`.
|
||||
|
||||
```shell
|
||||
python "docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py"
|
||||
```
|
||||
|
||||
Le second script propose une vue complémentaire sous forme de _calendrier thermique_ (une [carte de chaleur](https://fr.wikipedia.org/wiki/Carte_de_chaleur) disposée en jours et mois).
|
||||
À partir du même dataset minuté, il calcule des moyennes quotidiennes (température, humidité, pression, illuminance, vent) ou des cumuls quotidiens (pluie), puis remplit une matrice « mois x jours » pour l’année la plus récente disponible.
|
||||
Ces vues servent surtout à :
|
||||
|
||||
- repérer rapidement les saisons, épisodes pluvieux ou périodes de vent soutenu ;
|
||||
- détecter des trous dans la série (jours entièrement manquants) ;
|
||||
- avoir un aperçu global de l’année sans zoomer/dézoomer en permanence sur une longue série temporelle.
|
||||
|
||||

|
||||
|
||||

|
||||
@@ -46,5 +58,3 @@ python "docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py"
|
||||

|
||||
|
||||

|
||||
|
||||
Ces vues, bien que simples en principe, mettent déjà mieux en évidence les fluctuations au cours du temps.
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# scripts/plot_basic_variables.py
|
||||
"""Génère des séries temporelles simples (7 jours) pour chaque variable météo."""
|
||||
"""Génère des séries temporelles simples pour chaque variable météo."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -7,7 +7,6 @@ import argparse
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
import pandas as pd
|
||||
|
||||
|
||||
@@ -16,7 +15,7 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from meteo.dataset import load_raw_csv
|
||||
from meteo.plots import export_plot_dataset
|
||||
from meteo.plots import PlotChoice, PlotStyle, plot_basic_series, recommended_style, resample_series_for_plot
|
||||
from meteo.variables import Variable, VARIABLES
|
||||
|
||||
|
||||
@@ -25,47 +24,32 @@ DOC_DIR = Path(__file__).resolve().parent.parent
|
||||
DEFAULT_OUTPUT_DIR = DOC_DIR / "figures"
|
||||
|
||||
|
||||
def _prepare_slice(df: pd.DataFrame, *, last_days: int) -> pd.DataFrame:
|
||||
"""Extrait la fenêtre temporelle souhaitée et applique une moyenne horaire pour lisser la courbe."""
|
||||
def _select_window(df: pd.DataFrame, *, last_days: int | None) -> pd.DataFrame:
|
||||
"""Extrait la fenêtre temporelle souhaitée (ou la totalité si None)."""
|
||||
|
||||
if last_days is None:
|
||||
return df
|
||||
end = df.index.max()
|
||||
start = end - pd.Timedelta(days=last_days)
|
||||
df_slice = df.loc[start:end]
|
||||
numeric_slice = df_slice.select_dtypes(include="number")
|
||||
if numeric_slice.empty:
|
||||
raise RuntimeError("Aucune colonne numérique disponible pour les moyennes horaires.")
|
||||
return numeric_slice.resample("1h").mean()
|
||||
return df.loc[start:end]
|
||||
|
||||
|
||||
def _plot_variable(df_hourly: pd.DataFrame, var: Variable, output_dir: Path) -> Path | None:
|
||||
"""Trace la série pour une variable et retourne le chemin de l'image générée."""
|
||||
|
||||
if var.column not in df_hourly.columns:
|
||||
print(f"⚠ Colonne absente pour {var.key} ({var.column}).")
|
||||
return None
|
||||
|
||||
series = df_hourly[var.column].dropna()
|
||||
if series.empty:
|
||||
print(f"⚠ Aucun point valide pour {var.key} dans l'intervalle choisi.")
|
||||
return None
|
||||
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
output_path = output_dir / f"{var.key}_last_7_days.png"
|
||||
|
||||
export_plot_dataset(series.to_frame(name=var.column), output_path)
|
||||
|
||||
plt.figure()
|
||||
plt.plot(series.index, series)
|
||||
plt.xlabel("Temps (UTC)")
|
||||
def _format_ylabel(var: Variable) -> str:
|
||||
unit_text = f" ({var.unit})" if var.unit else ""
|
||||
plt.ylabel(f"{var.label}{unit_text}")
|
||||
plt.title(f"{var.label} - Moyenne horaire sur les 7 derniers jours")
|
||||
plt.grid(True)
|
||||
plt.tight_layout()
|
||||
plt.savefig(output_path, dpi=150)
|
||||
plt.close()
|
||||
print(f"✔ Graphique généré : {output_path}")
|
||||
return output_path
|
||||
return f"{var.label}{unit_text}"
|
||||
|
||||
|
||||
def _aggregation_label(choice: PlotChoice, freq: str) -> str:
|
||||
"""Texte court pour indiquer l'agrégation appliquée."""
|
||||
|
||||
base = "moyenne"
|
||||
if callable(choice.agg) and getattr(choice.agg, "__name__", "") == "_circular_mean_deg":
|
||||
base = "moyenne circulaire"
|
||||
elif choice.agg == "sum":
|
||||
base = "somme"
|
||||
elif choice.agg == "median":
|
||||
base = "médiane"
|
||||
return f"{base} {freq}"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
@@ -78,8 +62,23 @@ def main(argv: list[str] | None = None) -> None:
|
||||
parser.add_argument(
|
||||
"--days",
|
||||
type=int,
|
||||
default=7,
|
||||
help="Nombre de jours à afficher (par défaut : 7).",
|
||||
default=None,
|
||||
help="Nombre de jours à afficher (par défaut : toute la période disponible).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--style",
|
||||
choices=[style.value for style in PlotStyle],
|
||||
help="Style de représentation à utiliser pour toutes les variables (par défaut : recommandations par variable).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resample",
|
||||
help="Fréquence pandas à utiliser pour l'agrégation temporelle (par défaut : calcul automatique).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-points",
|
||||
type=int,
|
||||
default=420,
|
||||
help="Nombre de points cible après agrégation automatique (par défaut : 420).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
@@ -93,7 +92,7 @@ def main(argv: list[str] | None = None) -> None:
|
||||
raise FileNotFoundError(f"Dataset introuvable : {CSV_PATH}")
|
||||
|
||||
df = load_raw_csv(CSV_PATH)
|
||||
df_hourly = _prepare_slice(df, last_days=args.days)
|
||||
df_window = _select_window(df, last_days=args.days)
|
||||
|
||||
selected: list[Variable]
|
||||
if args.only:
|
||||
@@ -105,8 +104,44 @@ def main(argv: list[str] | None = None) -> None:
|
||||
else:
|
||||
selected = list(VARIABLES)
|
||||
|
||||
output_dir: Path = args.output_dir
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for variable in selected:
|
||||
_plot_variable(df_hourly, variable, args.output_dir)
|
||||
if variable.column not in df_window.columns:
|
||||
print(f"⚠ Colonne absente pour {variable.key} ({variable.column}).")
|
||||
continue
|
||||
|
||||
series = df_window[variable.column].dropna()
|
||||
if series.empty:
|
||||
print(f"⚠ Aucun point valide pour {variable.key} sur la période choisie.")
|
||||
continue
|
||||
|
||||
style_choice = recommended_style(variable, args.style)
|
||||
|
||||
aggregated, freq_used = resample_series_for_plot(
|
||||
series,
|
||||
variable=variable,
|
||||
freq=args.resample,
|
||||
target_points=args.max_points,
|
||||
)
|
||||
if aggregated.empty:
|
||||
print(f"⚠ Pas de points après agrégation pour {variable.key}.")
|
||||
continue
|
||||
|
||||
output_path = output_dir / f"{variable.key}_overview.png"
|
||||
annotate_freq = _aggregation_label(style_choice, freq_used)
|
||||
|
||||
plot_basic_series(
|
||||
aggregated,
|
||||
variable=variable,
|
||||
output_path=output_path,
|
||||
style=style_choice.style,
|
||||
title=f"{variable.label} — évolution temporelle",
|
||||
ylabel=_format_ylabel(variable),
|
||||
annotate_freq=annotate_freq,
|
||||
)
|
||||
print(f"✔ Graphique généré : {output_path}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
|
||||
|
Before Width: | Height: | Size: 127 KiB After Width: | Height: | Size: 117 KiB |
|
After Width: | Height: | Size: 119 KiB |
|
Before Width: | Height: | Size: 497 KiB |
|
Before Width: | Height: | Size: 432 KiB |
|
Before Width: | Height: | Size: 83 KiB |
|
Before Width: | Height: | Size: 610 KiB |
|
Before Width: | Height: | Size: 587 KiB |
|
Before Width: | Height: | Size: 379 KiB |
|
Before Width: | Height: | Size: 235 KiB |
|
Before Width: | Height: | Size: 364 KiB |
|
Before Width: | Height: | Size: 351 KiB |
|
Before Width: | Height: | Size: 390 KiB |
|
Before Width: | Height: | Size: 102 KiB |
|
Before Width: | Height: | Size: 505 KiB |
|
Before Width: | Height: | Size: 479 KiB |
|
Before Width: | Height: | Size: 325 KiB |
|
Before Width: | Height: | Size: 88 KiB |
|
Before Width: | Height: | Size: 111 KiB |
|
Before Width: | Height: | Size: 108 KiB |
|
Before Width: | Height: | Size: 97 KiB |
|
Before Width: | Height: | Size: 445 KiB |
|
Before Width: | Height: | Size: 430 KiB |
|
Before Width: | Height: | Size: 363 KiB |
|
Before Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 512 KiB |
|
Before Width: | Height: | Size: 471 KiB |
|
Before Width: | Height: | Size: 340 KiB |
|
Before Width: | Height: | Size: 574 KiB |
|
Before Width: | Height: | Size: 403 KiB |
|
Before Width: | Height: | Size: 201 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 135 KiB |
|
After Width: | Height: | Size: 184 KiB |
|
After Width: | Height: | Size: 164 KiB |
|
After Width: | Height: | Size: 216 KiB |
|
After Width: | Height: | Size: 185 KiB |
|
After Width: | Height: | Size: 176 KiB |
|
After Width: | Height: | Size: 219 KiB |
|
After Width: | Height: | Size: 199 KiB |
|
After Width: | Height: | Size: 129 KiB |
|
After Width: | Height: | Size: 180 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 209 KiB |
|
After Width: | Height: | Size: 150 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
After Width: | Height: | Size: 90 KiB |
|
After Width: | Height: | Size: 154 KiB |
|
After Width: | Height: | Size: 192 KiB |
|
After Width: | Height: | Size: 194 KiB |
|
After Width: | Height: | Size: 182 KiB |
|
After Width: | Height: | Size: 127 KiB |
|
After Width: | Height: | Size: 174 KiB |
|
After Width: | Height: | Size: 153 KiB |
|
After Width: | Height: | Size: 206 KiB |
|
After Width: | Height: | Size: 146 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 179 KiB |
@@ -1,72 +1,112 @@
|
||||
# Corrélations binaires
|
||||
|
||||
Cette étape regroupe l'ensemble des scripts dédiés aux corrélations et comparaisons directes entre variables.
|
||||
Chaque figure déposée dans `figures/` possède son CSV compagnon exporté dans le dossier `data/` au même emplacement.
|
||||
L’objectif de ce chapitre est d’explorer les relations entre variables deux à deux : d’abord visuellement (superposition de séries temporelles, comme ci-dessous, et [nuages de points](https://fr.wikipedia.org/wiki/Nuage_de_points), comme dans le chapitre suivant), puis numériquement via des coefficients de [corrélation](<https://fr.wikipedia.org/wiki/Corr%C3%A9lation_(statistiques)>).
|
||||
On reste volontairement dans un cadre simple : une variable « primaire » et une variable « associée », à la fois dans le temps et dans les matrices de corrélation.
|
||||
|
||||
## Superpositions simples
|
||||
|
||||
```shell
|
||||
python "docs/04 - Corrélations binaires/scripts/plot_all_pairwise_scatter.py"
|
||||
python "docs/04 - Corrélations binaires/scripts/plot_pairwise_time_series.py"
|
||||
```
|
||||
|
||||

|
||||
Ce script parcourt toutes les paires de variables disponibles et produit, pour chacune, un graphique superposant les deux séries sur le même axe temporel (ou sur deux axes verticaux si nécessaire).
|
||||
Les données sont ré-échantillonnées pour limiter le nombre de points et lisser légèrement le bruit, en utilisant les mêmes mécanismes que dans le chapitre 3.
|
||||
Ces superpositions servent surtout à repérer des co‑évolutions évidentes (par exemple humidité qui baisse quand la température monte) ou, au contraire, des paires qui semblent indépendantes à l’œil nu.
|
||||
|
||||

|
||||
### Température
|
||||
|
||||

|
||||
Toutes les figures de cette section ont pour variable « primaire » la température ; l’autre variable change d’un graphique à l’autre.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
### Humidité relative
|
||||
|
||||

|
||||
Ici, on fixe l’humidité comme variable principale et on observe comment elle évolue en parallèle des autres variables.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
### Pression
|
||||
|
||||

|
||||
Dans ces vues, on suit la pression atmosphérique et on la compare aux autres champs mesurés.
|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||

|
||||
|
||||

|
||||
### Pluviométrie
|
||||
|
||||

|
||||
On regarde ici comment les épisodes de pluie (taux de précipitation) se positionnent par rapport au vent et à la hauteur du soleil.
|
||||
|
||||

|
||||

|
||||
|
||||
## Heatmap globale
|
||||

|
||||
|
||||

|
||||
|
||||
### Luminance
|
||||
|
||||
Ces superpositions éclairent les liens entre lumière, vent et position du soleil.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
### Vent (vitesse / direction)
|
||||
|
||||
Enfin, on se concentre sur le vent : d’abord sa vitesse en lien avec l’élévation solaire, puis la direction comparée à cette même référence.
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
## Matrices de corrélation (instantané, signé)
|
||||
|
||||
Le calcul des coefficients de Pearson et de Spearman peut nous donner une indication numérique de la force et du signe des corrélations entre les différentes variables.
|
||||
On passe ainsi du visuel (superpositions, nuages de points) à un résumé compact des co‑variations, même si cela ne capture que des dépendances linéaires ou monotones simples.
|
||||
On utilise ici :
|
||||
|
||||
- le coefficient de corrélation linéaire de Pearson (voir [corrélation linéaire](https://fr.wikipedia.org/wiki/Corr%C3%A9lation_lin%C3%A9aire)) pour mesurer à quel point deux variables varient ensemble de manière approximativement linéaire ;
|
||||
- le coefficient de Spearman (voir [corrélation de Spearman](https://fr.wikipedia.org/wiki/Corr%C3%A9lation_de_Spearman)) pour capturer des relations monotones (croissantes ou décroissantes), même si elles ne sont pas parfaitement linéaires.
|
||||
|
||||
```shell
|
||||
python "docs/04 - Corrélations binaires/scripts/plot_correlation_heatmap.py"
|
||||
python "docs/04 - Corrélations binaires/scripts/plot_correlation_heatmap.py" --transform=identity --upper-only
|
||||
```
|
||||
|
||||

|
||||
|
||||

|
||||
|
||||
Le signe et l'intensité des coefficients montrent à quel point deux variables bougent ensemble au même instant (co‑mouvement linéaire pour Pearson, monotone pour Spearman).
|
||||
Cette matrice sert donc surtout de carte globale : repérer rapidement les couples très corrélés ou indiquer un lien physique évident, mettre en alerte des variables à forte corrélation qui pourraient masquer d'autres effets (saisonnalité, cycle jour/nuit), et choisir quelles paires méritent qu'on teste des décalages temporels ou des relations non linéaires dans la suite.
|
||||
|
||||
@@ -1,52 +0,0 @@
|
||||
# scripts/plot_all_pairwise_scatter.py
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from meteo.dataset import load_raw_csv
|
||||
from meteo.variables import iter_variable_pairs
|
||||
from meteo.plots import plot_scatter_pair
|
||||
|
||||
|
||||
CSV_PATH = Path("data/weather_minutely.csv")
|
||||
DOC_DIR = Path(__file__).resolve().parent.parent
|
||||
OUTPUT_DIR = DOC_DIR / "figures" / "pairwise_scatter"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
if not CSV_PATH.exists():
|
||||
print(f"⚠ Fichier introuvable : {CSV_PATH}")
|
||||
return
|
||||
|
||||
df = load_raw_csv(CSV_PATH)
|
||||
print(f"Dataset minuté chargé : {CSV_PATH}")
|
||||
print(f" Lignes : {len(df)}")
|
||||
print(f" Colonnes : {list(df.columns)}")
|
||||
|
||||
pairs = iter_variable_pairs()
|
||||
print(f"Nombre de paires de variables : {len(pairs)}")
|
||||
|
||||
for var_x, var_y in pairs:
|
||||
filename = f"scatter_{var_x.key}_vs_{var_y.key}.png"
|
||||
output_path = OUTPUT_DIR / filename
|
||||
|
||||
print(f"→ Trace {var_y.key} en fonction de {var_x.key} → {output_path}")
|
||||
plot_scatter_pair(
|
||||
df=df,
|
||||
var_x=var_x,
|
||||
var_y=var_y,
|
||||
output_path=output_path,
|
||||
sample_step=10, # un point sur 10 : ≈ 32k points au lieu de 320k
|
||||
)
|
||||
|
||||
print("✔ Tous les graphiques de nuages de points ont été générés.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -1,8 +1,12 @@
|
||||
# scripts/plot_correlation_heatmap.py
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from pathlib import Path
|
||||
import sys
|
||||
import argparse
|
||||
import pandas as pd
|
||||
import numpy as np
|
||||
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
@@ -11,38 +15,152 @@ if str(PROJECT_ROOT) not in sys.path:
|
||||
|
||||
from meteo.dataset import load_raw_csv
|
||||
from meteo.variables import VARIABLES
|
||||
from meteo.analysis import compute_correlation_matrix_for_variables
|
||||
from meteo.analysis import compute_correlation_matrices_for_methods
|
||||
from meteo.plots import plot_correlation_heatmap
|
||||
|
||||
|
||||
CSV_PATH = Path("data/weather_minutely.csv")
|
||||
DOC_DIR = Path(__file__).resolve().parent.parent
|
||||
OUTPUT_PATH = DOC_DIR / "figures" / "correlation_heatmap.png"
|
||||
|
||||
CORRELATION_METHODS: tuple[str, ...] = ("pearson", "spearman")
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class HeatmapConfig:
|
||||
filename: str
|
||||
title: str
|
||||
colorbar_label: str
|
||||
cmap: str = "viridis"
|
||||
vmin: float = 0.0
|
||||
vmax: float = 1.0
|
||||
|
||||
|
||||
HEATMAP_CONFIGS: dict[str, HeatmapConfig] = {
|
||||
"pearson": HeatmapConfig(
|
||||
filename="correlation_heatmap.png",
|
||||
title="Corrélations (coef. de Pearson)",
|
||||
colorbar_label="Coefficient de corrélation",
|
||||
cmap="viridis",
|
||||
vmin=0.0,
|
||||
vmax=1.0,
|
||||
),
|
||||
"spearman": HeatmapConfig(
|
||||
filename="correlation_heatmap_spearman.png",
|
||||
title="Corrélations (coef. de Spearman)",
|
||||
colorbar_label="Coefficient de corrélation",
|
||||
cmap="viridis",
|
||||
vmin=0.0,
|
||||
vmax=1.0,
|
||||
),
|
||||
}
|
||||
|
||||
|
||||
def _get_heatmap_config(method: str) -> HeatmapConfig:
|
||||
if method in HEATMAP_CONFIGS:
|
||||
return HEATMAP_CONFIGS[method]
|
||||
|
||||
# Valeurs par défaut pour un scénario non prévu.
|
||||
return HeatmapConfig(
|
||||
filename=f"correlation_heatmap_{method}.png",
|
||||
title=f"Matrice de corrélation ({method})",
|
||||
colorbar_label="Coefficient de corrélation",
|
||||
cmap="viridis" if CORRELATION_TRANSFORM == "square" else "coolwarm",
|
||||
vmin=0.0 if CORRELATION_TRANSFORM == "square" else -1.0,
|
||||
vmax=1.0,
|
||||
)
|
||||
|
||||
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser(description="Trace des matrices de corrélation instantanées (signées, absolues ou r²).")
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=DOC_DIR / "figures",
|
||||
help="Dossier de sortie pour les heatmaps.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--transform",
|
||||
choices=["identity", "absolute", "square"],
|
||||
default="absolute",
|
||||
help="Transformation de la matrice (signée, |r| ou r²). Par défaut : |r|.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--upper-only",
|
||||
action="store_true",
|
||||
help="Masque la partie inférieure de la matrice pour alléger la lecture.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
if not CSV_PATH.exists():
|
||||
print(f"⚠ Fichier introuvable : {CSV_PATH}")
|
||||
print(" Assurez-vous d'avoir généré le dataset minuté.")
|
||||
return
|
||||
|
||||
df = load_raw_csv(CSV_PATH)
|
||||
df = df[[v.column for v in VARIABLES]]
|
||||
print(f"Dataset minuté chargé : {CSV_PATH}")
|
||||
print(f" Lignes : {len(df)}")
|
||||
print(f" Colonnes : {list(df.columns)}")
|
||||
print()
|
||||
|
||||
corr = compute_correlation_matrix_for_variables(df, VARIABLES, method="pearson")
|
||||
transform = args.transform
|
||||
matrices = compute_correlation_matrices_for_methods(
|
||||
df=df,
|
||||
variables=VARIABLES,
|
||||
methods=CORRELATION_METHODS,
|
||||
transform=transform,
|
||||
)
|
||||
|
||||
print("Matrice de corrélation (aperçu) :")
|
||||
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
for method, corr in matrices.items():
|
||||
if args.upper_only:
|
||||
mask = np.tril(np.ones_like(corr, dtype=bool), k=-1)
|
||||
corr = corr.mask(mask)
|
||||
|
||||
print(f"Matrice de corrélation (méthode={method}, transform={transform}) :")
|
||||
print(corr)
|
||||
print()
|
||||
|
||||
config = _get_heatmap_config(method)
|
||||
filename = config.filename
|
||||
title = config.title
|
||||
if transform == "absolute":
|
||||
title = f"{title} (|r|)"
|
||||
stem, suffix = filename.rsplit(".", 1)
|
||||
filename = f"{stem}_abs.{suffix}"
|
||||
elif transform == "square":
|
||||
title = f"{title} (r²)"
|
||||
stem, suffix = filename.rsplit(".", 1)
|
||||
filename = f"{stem}_r2.{suffix}"
|
||||
config = HeatmapConfig(
|
||||
filename=filename,
|
||||
title=title,
|
||||
colorbar_label="Coefficient de corrélation r²",
|
||||
cmap="viridis",
|
||||
vmin=0.0,
|
||||
vmax=1.0,
|
||||
)
|
||||
elif transform == "identity":
|
||||
config = HeatmapConfig(
|
||||
filename=filename,
|
||||
title=title,
|
||||
colorbar_label="Coefficient de corrélation r",
|
||||
cmap="coolwarm",
|
||||
vmin=-1.0,
|
||||
vmax=1.0,
|
||||
)
|
||||
|
||||
output_path = plot_correlation_heatmap(
|
||||
corr=corr,
|
||||
variables=VARIABLES,
|
||||
output_path=OUTPUT_PATH,
|
||||
output_path=args.output_dir / filename,
|
||||
annotate=True,
|
||||
title=title,
|
||||
cmap=config.cmap,
|
||||
vmin=config.vmin,
|
||||
vmax=config.vmax,
|
||||
colorbar_label=config.colorbar_label,
|
||||
)
|
||||
|
||||
print(f"✔ Heatmap de corrélation sauvegardée dans : {output_path}")
|
||||
|
||||
@@ -0,0 +1,169 @@
|
||||
# scripts/plot_pairwise_time_series.py
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import sys
|
||||
|
||||
import argparse
|
||||
import pandas as pd
|
||||
|
||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
||||
if str(PROJECT_ROOT) not in sys.path:
|
||||
sys.path.insert(0, str(PROJECT_ROOT))
|
||||
|
||||
from meteo.dataset import load_raw_csv
|
||||
from meteo.plots import (
|
||||
PlotChoice,
|
||||
PlotStyle,
|
||||
plot_dual_time_series,
|
||||
recommended_style,
|
||||
resample_series_for_plot,
|
||||
)
|
||||
from meteo.variables import Variable, VARIABLES, VARIABLES_BY_KEY, iter_variable_pairs
|
||||
|
||||
|
||||
CSV_PATH = Path("data/weather_minutely.csv")
|
||||
DOC_DIR = Path(__file__).resolve().parent.parent
|
||||
OUTPUT_DIR = DOC_DIR / "figures" / "pairwise_timeseries"
|
||||
|
||||
|
||||
def _select_variables(keys: list[str] | None) -> list[Variable]:
|
||||
if not keys:
|
||||
return list(VARIABLES)
|
||||
missing = [key for key in keys if key not in VARIABLES_BY_KEY]
|
||||
if missing:
|
||||
raise KeyError(f"Variables inconnues : {', '.join(missing)}")
|
||||
return [VARIABLES_BY_KEY[key] for key in keys]
|
||||
|
||||
|
||||
def _aggregation_label(choice_a: PlotChoice, choice_b: PlotChoice, freq: str) -> str:
|
||||
agg_labels = set()
|
||||
for choice in (choice_a, choice_b):
|
||||
base = "moyenne"
|
||||
if isinstance(choice.agg, str):
|
||||
if choice.agg == "sum":
|
||||
base = "somme"
|
||||
elif choice.agg == "median":
|
||||
base = "médiane"
|
||||
elif getattr(choice.agg, "__name__", "") == "_circular_mean_deg":
|
||||
base = "moyenne circulaire"
|
||||
agg_labels.add(base)
|
||||
if len(agg_labels) == 1:
|
||||
label = agg_labels.pop()
|
||||
else:
|
||||
label = "agrégations mixtes"
|
||||
return f"{label} {freq}"
|
||||
|
||||
|
||||
def main(argv: list[str] | None = None) -> None:
|
||||
parser = argparse.ArgumentParser(description="Superpose les séries temporelles de toutes les paires de variables.")
|
||||
parser.add_argument(
|
||||
"--only",
|
||||
nargs="*",
|
||||
help="Clés de variables à inclure (par défaut : toutes).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--days",
|
||||
type=int,
|
||||
default=None,
|
||||
help="Limiter aux N derniers jours (par défaut : période complète).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--style",
|
||||
choices=[style.value for style in PlotStyle],
|
||||
help="Style à imposer à toutes les variables (par défaut : style recommandé par variable).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--resample",
|
||||
help="Fréquence pandas pour l'agrégation temporelle (par défaut : calcul automatique).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--max-points",
|
||||
type=int,
|
||||
default=420,
|
||||
help="Nombre de points cible après agrégation automatique (par défaut : 420).",
|
||||
)
|
||||
parser.add_argument(
|
||||
"--output-dir",
|
||||
type=Path,
|
||||
default=OUTPUT_DIR,
|
||||
help="Dossier où stocker les figures.",
|
||||
)
|
||||
args = parser.parse_args(argv)
|
||||
|
||||
if not CSV_PATH.exists():
|
||||
print(f"⚠ Fichier introuvable : {CSV_PATH}")
|
||||
return
|
||||
|
||||
df = load_raw_csv(CSV_PATH)
|
||||
if args.days is not None:
|
||||
end = df.index.max()
|
||||
start = end - pd.Timedelta(days=args.days)
|
||||
df = df.loc[start:end]
|
||||
|
||||
variables = _select_variables(args.only)
|
||||
pairs = [(vx, vy) for (vx, vy) in iter_variable_pairs() if vx in variables and vy in variables]
|
||||
if not pairs:
|
||||
print("⚠ Aucune paire à tracer.")
|
||||
return
|
||||
|
||||
output_dir: Path = args.output_dir
|
||||
output_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
print(f"Dataset chargé ({len(df)} lignes) → génération de {len(pairs)} paires.")
|
||||
|
||||
for var_a, var_b in pairs:
|
||||
missing: list[str] = []
|
||||
for col in (var_a.column, var_b.column):
|
||||
if col not in df.columns:
|
||||
missing.append(col)
|
||||
if missing:
|
||||
print(f"⚠ Colonnes absentes, on passe : {', '.join(missing)}")
|
||||
continue
|
||||
|
||||
series_a = df[var_a.column].dropna()
|
||||
series_b = df[var_b.column].dropna()
|
||||
if series_a.empty or series_b.empty:
|
||||
print(f"⚠ Données insuffisantes pour {var_a.key} / {var_b.key}, on passe.")
|
||||
continue
|
||||
|
||||
choice_a = recommended_style(var_a, args.style)
|
||||
choice_b = recommended_style(var_b, args.style)
|
||||
|
||||
aggregated_a, freq_used = resample_series_for_plot(
|
||||
series_a,
|
||||
variable=var_a,
|
||||
freq=args.resample,
|
||||
target_points=args.max_points,
|
||||
)
|
||||
aggregated_b, _ = resample_series_for_plot(
|
||||
series_b,
|
||||
variable=var_b,
|
||||
freq=freq_used,
|
||||
target_points=args.max_points,
|
||||
)
|
||||
if aggregated_a.empty or aggregated_b.empty:
|
||||
print(f"⚠ Pas de points après agrégation pour {var_a.key} / {var_b.key}.")
|
||||
continue
|
||||
|
||||
output_path = output_dir / f"timeseries_{var_a.key}_vs_{var_b.key}.png"
|
||||
label_freq = _aggregation_label(choice_a, choice_b, freq_used)
|
||||
|
||||
print(f"→ {var_a.key} vs {var_b.key} ({freq_used}) → {output_path}")
|
||||
plot_dual_time_series(
|
||||
aggregated_a,
|
||||
var_a,
|
||||
choice_a,
|
||||
aggregated_b,
|
||||
var_b,
|
||||
choice_b,
|
||||
output_path=output_path,
|
||||
title=f"{var_a.label} et {var_b.label} — évolution temporelle",
|
||||
annotate_freq=label_freq,
|
||||
)
|
||||
|
||||
print("✔ Superpositions temporelles générées.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
After Width: | Height: | Size: 108 KiB |
|
After Width: | Height: | Size: 88 KiB |
|
After Width: | Height: | Size: 66 KiB |
|
Before Width: | Height: | Size: 52 KiB After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 81 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 75 KiB |
|
After Width: | Height: | Size: 94 KiB |
|
Before Width: | Height: | Size: 50 KiB |
|
After Width: | Height: | Size: 69 KiB |
|
After Width: | Height: | Size: 78 KiB |
|
Before Width: | Height: | Size: 51 KiB After Width: | Height: | Size: 67 KiB |
|
Before Width: | Height: | Size: 55 KiB After Width: | Height: | Size: 65 KiB |
|
After Width: | Height: | Size: 67 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
Before Width: | Height: | Size: 54 KiB After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 68 KiB |
|
After Width: | Height: | Size: 65 KiB |