1

Compare commits

..

25 Commits

Author SHA1 Message Date
d7f61ca93a Détermination objective du meilleur jour de la semaine 2025-11-26 22:57:15 +01:00
f4bdbe2c7f Reformulations 2025-11-26 17:22:57 +01:00
8b26c0bf17 Conclusion 2025-11-26 11:08:15 +01:00
2495e9365e Scripts et figures manquantes 2025-11-26 01:47:48 +01:00
7ba4287399 Ajout d'une transition à la conclusion 2025-11-26 01:07:16 +01:00
9a393972eb Modèle Chronos-2 2025-11-26 01:03:41 +01:00
ccd2195d27 Premiers modèles prédictifs 2025-11-25 18:58:21 +01:00
18afeb1e8b Corrélations multiples 2025-11-25 15:40:49 +01:00
1932938fd6 Exploration des relations binaires 2025-11-22 01:46:57 +01:00
2ff719107b Affiner les heatmaps de corrélation et l'annotation des lags 2025-11-21 01:47:05 +01:00
a36157b52f Graphiques de corrélations binaires simples 2025-11-20 21:45:24 +01:00
df7fbf07ed Amélioration des vues basiques 2025-11-20 21:12:02 +01:00
8979f48c23 Script de statistiques de base 2025-11-19 23:59:23 +01:00
a4d3ce7b49 Ajout des matrices de corrélation + Refactoring 2025-11-19 23:31:38 +01:00
3a1f7e2a7e Refactoring 2025-11-19 22:46:04 +01:00
6a78dbb50d Ajout des vues calendrier à l'étape 3 2025-11-19 22:35:03 +01:00
79603b7c3e Simplification du markdown 2025-11-19 17:16:39 +01:00
9fdf9b4268 Ajout d'un script pour simplifier le rafraichissement des données 2025-11-19 17:13:38 +01:00
da6c15d6b0 Ajout d'un script de nettoyage 2025-11-19 17:06:01 +01:00
617b12c02e Réorganisation 2025-11-19 17:01:45 +01:00
566d4400ce Nettoyage 2025-11-18 09:48:19 +01:00
9c4b971b92 Ignorer .DS_Store 2025-11-18 09:29:41 +01:00
b7f193b637 Texte 2025-11-18 09:29:14 +01:00
aebb8c0dca Uniformisation des graphiques basiques 2025-11-18 09:20:34 +01:00
85da4e4931 Refactoring 2025-11-18 09:01:34 +01:00
402 changed files with 9752 additions and 3169999 deletions

View File

@@ -4,4 +4,11 @@ INFLUXDB_ORG=
INFLUXDB_BUCKET=weather INFLUXDB_BUCKET=weather
STATION_LATITUDE= STATION_LATITUDE=
STATION_LONGITUDE= STATION_LONGITUDE=
STATION_ELEVATION= STATION_ELEVATION=
# Chronos (prévision)
# CHRONOS_MODEL=amazon/chronos-t5-small
# CHRONOS_CONTEXT=336
# CHRONOS_HORIZON=96
# CHRONOS_RESAMPLE=1h
# CHRONOS_SAMPLES=20

6
.gitignore vendored
View File

@@ -1,5 +1,7 @@
.venv .venv
.env .env
data data
scripts/__pycache__ __pycache__
meteo/__pycache__ .DS_Store
blog/
scripts/build_blog_from_docs.py

View File

@@ -1,17 +0,0 @@
# Installation de l'environnement de base
Après avoir cloné le dépôt :
```shell
python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
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

View File

@@ -0,0 +1,219 @@
# Installation, configuration et tests
## Installation de l'environnement de base
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.
Lidé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
source .venv/bin/activate
python -m pip install --upgrade pip
pip install -r requirements.txt
python -c "import pandas, influxdb_client, sklearn; print('OK')"
```
- `python3 -m venv .venv` crée un environnement virtuel Python local dans le dossier `.venv`, ce qui permet disoler 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 quil est actif, la commande `python` pointera vers linterpré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 simportent correctement ; si la commande affiche `OK`, lenvironnement de base est prêt à être utilisé.
## Configuration
```shell
cp .env.example .env
```
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 dexporter les variables à la main à chaque session.
- `INFLUXDB_URL` : URL de l'API du serveur InfluxDB 2.x (incluant généralement le port 8086), par exemple `http://localhost:8086` ou ladresse de votre serveur ; cest le point dentrée HTTP/HTTPS vers votre base de données de séries temporelles (voir aussi lintroduction 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 linterface 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 lon 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 ; cest 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 laltitude est mal connue.
## Tests de l'environnement de travail
Avant dattaquer 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 deffectuer quelques requêtes simples pour valider laccès.
```shell
python "docs/01 - Installation, configuration et tests/scripts/test_influx_connection.py"
```
```text
Configuration InfluxDB chargée :
URL : http://10.0.3.2:8086
Org : Dern
Bucket : weather
→ Ping du serveur InfluxDB…
✔ Ping OK
→ Requête de test sur le bucket…
✔ Requête de test réussie : 18 table(s), 58 enregistrement(s) trouvés.
Exemple de point :
time : 2025-11-16 22:30:50.263360+00:00
measurement : %
field : device_class_str
value : humidity
```
Si vous obtenez un résultat similaire (URL affichée, ping OK, requête de test qui retourne quelques enregistrements), cest 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"
```
```text
Bucket InfluxDB : weather
Measurements disponibles :
- %
- hPa
- km/h
- lx
- mm/h
- °
- °C
Champs pour measurement « % » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « hPa » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « km/h » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « lx » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- value (type: unknown)
Champs pour measurement « mm/h » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « ° » :
- friendly_name_str (type: unknown)
- value (type: unknown)
Champs pour measurement « °C » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- 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 lon 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"
```
```text
Bucket InfluxDB : weather
Measurement « % »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_humidite_relative
Measurement « hPa »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_pression_atmospherique
Measurement « km/h »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_vitesse_du_vent
Measurement « lx »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_luminance
Measurement « mm/h »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_precipitations
Measurement « ° »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_direction_du_vent
Measurement « °C »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_temperature
```
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 lhumidité 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` : cest là que lon 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.

View File

@@ -1,10 +1,17 @@
# tests/test_influx_connection.py # tests/test_influx_connection.py
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing from contextlib import closing
from influxdb_client.client.exceptions import InfluxDBError from influxdb_client.client.exceptions import InfluxDBError
PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.config import InfluxSettings from meteo.config import InfluxSettings
from meteo.influx_client import create_influx_client, test_basic_query from meteo.influx_client import create_influx_client, test_basic_query
@@ -34,10 +41,7 @@ def main() -> None:
print("✔ Ping OK") print("✔ Ping OK")
print("→ Requête de test sur le bucket…") print("→ Requête de test sur le bucket…")
try: tables = test_basic_query(client, settings.bucket)
tables = test_basic_query(client, settings.bucket)
except InfluxDBError as exc:
raise SystemExit(f"Erreur lors de la requête Flux : {exc}") from exc
# On fait un retour synthétique # On fait un retour synthétique
nb_tables = len(tables) nb_tables = len(tables)

View File

@@ -1,8 +1,15 @@
# tests/test_influx_entities.py # tests/test_influx_entities.py
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing from contextlib import closing
PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.config import InfluxSettings from meteo.config import InfluxSettings
from meteo.influx_client import create_influx_client from meteo.influx_client import create_influx_client
from meteo.schema import ( from meteo.schema import (

View File

@@ -1,8 +1,15 @@
# tests/test_influx_schema.py # tests/test_influx_schema.py
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing from contextlib import closing
PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.config import InfluxSettings from meteo.config import InfluxSettings
from meteo.influx_client import create_influx_client from meteo.influx_client import create_influx_client
from meteo.schema import list_measurements, list_measurement_fields from meteo.schema import list_measurements, list_measurement_fields

View File

@@ -1,12 +0,0 @@
# Configuration
```shell
cp .env.example .env
```
On copie le fichier de configuration d'exemple, puis on l'ouvre pour l'adapter à notre cas.
- `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

View File

@@ -0,0 +1,286 @@
# 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, cestà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.
Lobjectif est de passer dun format brut, pensé pour la domotique temps réel, à un format tabulaire lisible par les bibliothèques danalyse comme `pandas`.
## Export des données
```shell
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 lhorodatage 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 :
```text
✔ Export terminé : /Users/richard/Documents/donnees_meteo/data/weather_raw_7d.csv
```
(Le chemin changera sur votre propre machine)
Vérifiez que le fichier est bien créé et qu'il contient des données.
À la place de `export_station_data.py`, on peut aussi lancer :
```shell
python "docs/02 - Préparation des données/scripts/export_station_data_full.py"
```
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 lhistorique complet une fois les étapes stabilisées.
Les deux scripts sappuient sur lAPI 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 quune seule variable renseignée.
Pour lanalyse 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
```
```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,,,,,,
2025-03-10 09:35:23.162398+00:00,,83.0,,,,,
2025-03-10 09:35:23.164634+00:00,,,,,7.4,,
2025-03-10 09:35:23.170122+00:00,,,,,,256.0,
2025-03-10 09:35:23.183555+00:00,,,,,,,0.0
2025-03-10 09:35:41.211148+00:00,,,,20551.2,,,
2025-03-10 09:36:22.638255+00:00,,,,,12.2,,
2025-03-10 09:36:22.640356+00:00,,,,,,306.0,
```
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.
Cest un format très pratique pour la collecte temps réel, mais moins confortable pour lanalyse : il faut dabord tout remettre en forme.
Le script suivant s'occupe de regrouper les données de capteurs dont l'enregistrement est proche :
```shell
python "docs/02 - Préparation des données/scripts/format_raw_csv.py"
```
```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'>
Après combinaison (1s) : 630171 lignes
✔ Fichier formaté écrit dans : /Users/richard/Documents/donnees_meteo/data/weather_formatted_1s.csv
```
Un nouveau document CSV intermédiaire est donc créé.
On voit que lon passe denviron 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 lhorodatage comme clé.
Lindex de type `DatetimeIndex` indiqué dans la sortie est laxe 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 lorsquon prépare une série pour lanalyse.
```shell
head data/weather_formatted_1s.csv
```
```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,,,
2025-03-10 09:36:22+00:00,,,,20247.6,12.2,306.0,
2025-03-10 09:36:52+00:00,,,,20199.6,9.3,246.0,
2025-03-10 09:37:22+00:00,,,,20034.0,7.9,,
2025-03-10 09:37:52+00:00,,,,20124.0,7.4,284.0,
2025-03-10 09:38:22+00:00,,,,19860.0,9.7,215.0,
2025-03-10 09:39:22+00:00,,,,19722.0,11.4,203.0,
2025-03-10 09:40:22+00:00,,,,19720.8,10.0,209.0,
```
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 :
```shell
python "docs/02 - Préparation des données/scripts/fill_formatted_1s.py"
```
```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 quun capteur ne publie pas de nouvelle mesure, on considère que la précédente reste valable. Cest 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 ; lobjectif ici est surtout dobtenir 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, qui aideront ensuite à interpréter les graphiques et à construire des modèles plus pertinents :
- 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 dune estimation de lélévation solaire audessus de lhorizon (jour/nuit, midi solaire, etc.) en sappuyant sur la bibliothèque dastronomie [`astral`](https://astral.readthedocs.io/).
Ces opérations sont réalisées par :
```shell
python "docs/02 - Préparation des données/scripts/make_minutely_dataset.py"
```
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, 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 lexport 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 dun temps de calcul un peu plus long.
## Vérification des données
Avant dexplorer 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 dabord s'assurer que plus aucune information n'est manquante :
```shell
python "docs/02 - Préparation des données/scripts/check_missing_values.py"
```
```text
Dataset chargé : data/weather_minutely.csv
Lignes : 321881
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate']
=== Synthèse des valeurs manquantes ===
Total de cellules : 2253167
Cellules manquantes : 0
Fraction manquante : 0.000000
Lignes complètes : 321881
Lignes avec des trous : 0
Fraction lignes complètes : 1.000000
Valeurs manquantes par colonne :
- temperature : 0
- humidity : 0
- pressure : 0
- illuminance : 0
- wind_speed : 0
- wind_direction : 0
- rain_rate : 0
✔ 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 nest 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"
```
```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
=== describe() ===
temperature humidity pressure ... wind_speed wind_direction rain_rate
count 321881.000000 321881.000000 321881.000000 ... 321881.000000 321881.000000 321881.000000
mean 15.004488 74.131993 1010.683189 ... 2.877190 181.977411 0.108216
std 6.349077 18.885843 8.210283 ... 3.151080 88.089334 0.820691
min -2.200000 20.000000 976.973123 ... 0.000000 0.000000 0.000000
25% 10.277778 59.000000 1005.420000 ... 0.000000 96.000000 0.000000
50% 14.600000 77.666667 1011.514287 ... 2.333549 210.000000 0.000000
75% 19.000000 91.000000 1015.900000 ... 4.650000 247.666196 0.000000
max 34.888889 99.000000 1033.187174 ... 26.554176 360.000000 42.672000
[8 rows x 7 columns]
=== Min / max avec dates ===
- temperature:
min = -2.2 à 2025-03-17 05:16:00+00:00
max = 34.8888888888889 à 2025-07-02 15:59:00+00:00
- humidity:
min = 20.0 à 2025-04-30 15:22:00+00:00
max = 99.0 à 2025-03-11 06:29:00+00:00
- pressure:
min = 976.973122738378 à 2025-10-23 05:06:00+00:00
max = 1033.18717416804 à 2025-10-10 17:12:00+00:00
- illuminance:
min = 0.0 à 2025-03-10 17:44:00+00:00
max = 133520.394 à 2025-07-29 11:48:00+00:00
- wind_speed:
min = 0.0 à 2025-03-10 14:31:00+00:00
max = 26.554176 à 2025-06-26 00:10:00+00:00
- wind_direction:
min = 0.0 à 2025-03-12 04:57:00+00:00
max = 360.0 à 2025-03-12 07:33:00+00:00
- rain_rate:
min = 0.0 à 2025-03-10 09:35:00+00:00
max = 42.672 à 2025-06-15 03:10:00+00:00
=== Vérification de la continuité temporelle ===
Différences d'intervalle (top 5):
time
0 days 00:01:00 304291
0 days 00:02:00 9426
0 days 00:03:00 3562
0 days 00:04:00 1740
0 days 00:05:00 1142
Name: count, dtype: int64
Nombre d'intervalles ≠ 60s : 17589
```
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 dintervalle 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 quil faut garder en tête pour la suite.
Ce type de contrôle sinscrit 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
=== Gaps temporels détectés ===
Nombre de gaps : 17589
Total minutes manquantes (théoriques) : 40466
Top 10 des gaps les plus longs :
- De 2025-06-21 19:09:00+00:00 à 2025-06-21 20:10:00+00:00 (durée: 0 days 01:01:00, manquants: 60, de 2025-06-21 19:10:00+00:00 à 2025-06-21 20:09:00+00:00)
- De 2025-08-10 22:17:00+00:00 à 2025-08-10 23:15:00+00:00 (durée: 0 days 00:58:00, manquants: 57, de 2025-08-10 22:18:00+00:00 à 2025-08-10 23:14:00+00:00)
- De 2025-09-24 20:34:00+00:00 à 2025-09-24 21:32:00+00:00 (durée: 0 days 00:58:00, manquants: 57, de 2025-09-24 20:35:00+00:00 à 2025-09-24 21:31:00+00:00)
- De 2025-06-21 10:58:00+00:00 à 2025-06-21 11:55:00+00:00 (durée: 0 days 00:57:00, manquants: 56, de 2025-06-21 10:59:00+00:00 à 2025-06-21 11:54:00+00:00)
- De 2025-07-10 07:17:00+00:00 à 2025-07-10 08:14:00+00:00 (durée: 0 days 00:57:00, manquants: 56, de 2025-07-10 07:18:00+00:00 à 2025-07-10 08:13:00+00:00)
- De 2025-07-24 03:52:00+00:00 à 2025-07-24 04:46:00+00:00 (durée: 0 days 00:54:00, manquants: 53, de 2025-07-24 03:53:00+00:00 à 2025-07-24 04:45:00+00:00)
- De 2025-10-28 08:31:00+00:00 à 2025-10-28 09:23:00+00:00 (durée: 0 days 00:52:00, manquants: 51, de 2025-10-28 08:32:00+00:00 à 2025-10-28 09:22:00+00:00)
- De 2025-03-16 15:31:00+00:00 à 2025-03-16 16:20:00+00:00 (durée: 0 days 00:49:00, manquants: 48, de 2025-03-16 15:32:00+00:00 à 2025-03-16 16:19:00+00:00)
- De 2025-06-21 12:22:00+00:00 à 2025-06-21 13:08:00+00:00 (durée: 0 days 00:46:00, manquants: 45, de 2025-06-21 12:23:00+00:00 à 2025-06-21 13:07:00+00:00)
- 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)
```
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 sansfil, 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.
Il faut être capable de les identifier, et il faut les prendre en compte dans tout calcul ultérieur.
Une fois que tout est passé en revue, on passe d'un jeu contenant plus d'un million d'enregistrements à un jeu n'en contenant plus que 300 000.

View File

@@ -2,6 +2,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path 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.dataset import load_raw_csv
from meteo.quality import summarize_missing_values from meteo.quality import summarize_missing_values

View File

@@ -2,9 +2,15 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path from pathlib import Path
import sys
import pandas as pd 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.dataset import load_raw_csv

View File

@@ -1,8 +1,15 @@
# tests/export_station_data.py # tests/export_station_data.py
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing from contextlib import closing
PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.config import InfluxSettings from meteo.config import InfluxSettings
from meteo.influx_client import create_influx_client from meteo.influx_client import create_influx_client
from meteo.station_config import default_station_config from meteo.station_config import default_station_config

View File

@@ -1,8 +1,15 @@
# tests/export_station_data_full.py # tests/export_station_data_full.py
from __future__ import annotations from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing from contextlib import closing
PROJECT_ROOT = Path(__file__).resolve().parents[3]
if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.config import InfluxSettings from meteo.config import InfluxSettings
from meteo.influx_client import create_influx_client from meteo.influx_client import create_influx_client
from meteo.station_config import default_station_config from meteo.station_config import default_station_config

View File

@@ -2,6 +2,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path 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, fill_missing_with_previous from meteo.dataset import load_raw_csv, fill_missing_with_previous
@@ -13,7 +19,7 @@ OUTPUT_CSV_PATH = Path("data/weather_filled_1s.csv")
def main() -> None: def main() -> None:
if not INPUT_CSV_PATH.exists(): if not INPUT_CSV_PATH.exists():
print(f"⚠ Fichier introuvable : {INPUT_CSV_PATH}") print(f"⚠ Fichier introuvable : {INPUT_CSV_PATH}")
print(" Lancez d'abord : python -m scripts.format_raw_csv") print(' Lancez d\'abord : python "docs/02 - Préparation des données/scripts/format_raw_csv.py"')
return return
df_1s = load_raw_csv(INPUT_CSV_PATH) df_1s = load_raw_csv(INPUT_CSV_PATH)

View File

@@ -1,6 +1,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path 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, combine_close_observations from meteo.dataset import load_raw_csv, combine_close_observations

View File

@@ -2,6 +2,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path 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.dataset import load_raw_csv
from meteo.gaps import find_time_gaps from meteo.gaps import find_time_gaps

View File

@@ -2,6 +2,12 @@
from __future__ import annotations from __future__ import annotations
from pathlib import Path 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, resample_to_minutes from meteo.dataset import load_raw_csv, resample_to_minutes
from meteo.config import StationLocation from meteo.config import StationLocation
@@ -16,7 +22,7 @@ OUTPUT_CSV_PATH = Path("data/weather_minutely.csv")
def main() -> None: def main() -> None:
if not FORMATTED_CSV_PATH.exists(): if not FORMATTED_CSV_PATH.exists():
print(f"⚠ Fichier formaté introuvable : {FORMATTED_CSV_PATH}") print(f"⚠ Fichier formaté introuvable : {FORMATTED_CSV_PATH}")
print(" Lancez d'abord : python -m scripts.format_raw_csv") print(' Lancez d\'abord : python "docs/02 - Préparation des données/scripts/fill_formatted_1s.py"')
return return
df_1s = load_raw_csv(FORMATTED_CSV_PATH) df_1s = load_raw_csv(FORMATTED_CSV_PATH)
@@ -27,11 +33,7 @@ def main() -> None:
print(f"Après resampling 60s : {len(df_min)} lignes") print(f"Après resampling 60s : {len(df_min)} lignes")
hemisphere = "north" hemisphere = "north"
try: location = StationLocation.from_env(optional=True)
location = StationLocation.from_env(optional=True)
except RuntimeError as exc:
print(f"⚠ Coordonnées GPS invalides : {exc}")
location = None
if location is not None: if location is not None:
hemisphere = "south" if location.latitude < 0 else "north" hemisphere = "south" if location.latitude < 0 else "north"

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 49 KiB

After

Width:  |  Height:  |  Size: 49 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 46 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 45 KiB

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 105 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 41 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 38 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 71 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 56 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 59 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

View File

@@ -0,0 +1,60 @@
# Premiers graphiques
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 sappuient 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 lon utilise loption `--days`) puis choisit une fréquence dagré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.
Lidée est dobtenir une première vue densemble 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.
![](figures/temperature_overview.png)
![](figures/pressure_overview.png)
![](figures/humidity_overview.png)
![](figures/rain_rate_overview.png)
![](figures/wind_speed_overview.png)
![](figures/wind_direction_overview.png)
![](figures/illuminance_overview.png)
![](figures/sun_elevation_overview.png)
## Vues calendrier
```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 lanné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 lannée sans zoomer/dézoomer en permanence sur une longue série temporelle.
![](figures/calendar/calendar_temperature_2025.png)
![](figures/calendar/calendar_pressure_2025.png)
![](figures/calendar/calendar_humidity_2025.png)
![](figures/calendar/calendar_rain_2025.png)
![](figures/calendar/calendar_illuminance_2025.png)
![](figures/calendar/calendar_wind_2025.png)

View File

@@ -0,0 +1,148 @@
# scripts/plot_basic_variables.py
"""Génère des séries temporelles simples pour chaque variable météo."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
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_basic_series, recommended_style, resample_series_for_plot
from meteo.variables import Variable, VARIABLES
CSV_PATH = Path("data/weather_minutely.csv")
DOC_DIR = Path(__file__).resolve().parent.parent
DEFAULT_OUTPUT_DIR = DOC_DIR / "figures"
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)
return df.loc[start:end]
def _format_ylabel(var: Variable) -> str:
unit_text = f" ({var.unit})" if var.unit else ""
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:
parser = argparse.ArgumentParser(description="Trace les séries simples pour chaque variable météo.")
parser.add_argument(
"--only",
nargs="*",
help="Clés de variables à tracer (par défaut : toutes).",
)
parser.add_argument(
"--days",
type=int,
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",
type=Path,
default=DEFAULT_OUTPUT_DIR,
help="Dossier où stocker les figures.",
)
args = parser.parse_args(argv)
if not CSV_PATH.exists():
raise FileNotFoundError(f"Dataset introuvable : {CSV_PATH}")
df = load_raw_csv(CSV_PATH)
df_window = _select_window(df, last_days=args.days)
selected: list[Variable]
if args.only:
keys = set(args.only)
selected = [var for var in VARIABLES if var.key in keys]
missing = keys - {var.key for var in selected}
if missing:
raise KeyError(f"Variables inconnues : {sorted(missing)}")
else:
selected = list(VARIABLES)
output_dir: Path = args.output_dir
output_dir.mkdir(parents=True, exist_ok=True)
for variable in selected:
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__":
main()

View File

@@ -0,0 +1,121 @@
# docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py
from __future__ import annotations
from pathlib import Path
import sys
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 (
CalendarHeatmapSpec,
daily_mean_series,
generate_calendar_heatmaps,
rainfall_daily_total_series,
)
DOC_DIR = Path(__file__).resolve().parent.parent
CSV_PATH = PROJECT_ROOT / "data" / "weather_minutely.csv"
OUTPUT_DIR = DOC_DIR / "figures" / "calendar"
HEATMAP_SPECS: tuple[CalendarHeatmapSpec, ...] = (
CalendarHeatmapSpec(
key="rain",
agg_label="Pluie (mm)",
title_template="Pluie quotidienne - {year}",
cmap="Blues",
colorbar_label="mm",
aggregator=rainfall_daily_total_series,
),
CalendarHeatmapSpec(
key="temperature",
agg_label="Température (°C)",
title_template="Température moyenne quotidienne - {year}",
cmap="coolwarm",
colorbar_label="°C",
aggregator=daily_mean_series("temperature"),
),
CalendarHeatmapSpec(
key="humidity",
agg_label="Humidité relative (%)",
title_template="Humidité relative quotidienne - {year}",
cmap="PuBu",
colorbar_label="%",
aggregator=daily_mean_series("humidity"),
),
CalendarHeatmapSpec(
key="pressure",
agg_label="Pression (hPa)",
title_template="Pression moyenne quotidienne - {year}",
cmap="Greens",
colorbar_label="hPa",
aggregator=daily_mean_series("pressure"),
),
CalendarHeatmapSpec(
key="illuminance",
agg_label="Illuminance (lux)",
title_template="Illuminance moyenne quotidienne - {year}",
cmap="YlOrBr",
colorbar_label="lux",
aggregator=daily_mean_series("illuminance"),
),
CalendarHeatmapSpec(
key="wind",
agg_label="Vent (km/h)",
title_template="Vitesse moyenne du vent - {year}",
cmap="Purples",
colorbar_label="km/h",
aggregator=daily_mean_series("wind_speed"),
),
)
def main() -> None:
if not CSV_PATH.exists():
print(f"⚠ Fichier introuvable : {CSV_PATH}")
return
df = load_raw_csv(CSV_PATH)
if df.empty:
print("⚠ Dataset vide.")
return
if not isinstance(df.index, pd.DatetimeIndex):
print("⚠ Le dataset doit avoir un index temporel.")
return
print(f"Dataset minuté chargé : {CSV_PATH}")
print(f" Lignes : {len(df)}")
print(f" Colonnes : {list(df.columns)}")
print()
OUTPUT_DIR.mkdir(parents=True, exist_ok=True)
latest_year = df.index.year.max()
print(f"Année retenue pour le calendrier : {latest_year}")
results = generate_calendar_heatmaps(
df=df,
specs=HEATMAP_SPECS,
year=latest_year,
output_dir=OUTPUT_DIR,
)
for result in results:
title = result.spec.title_template.format(year=latest_year)
if result.output_path:
print(f"{title} : {result.output_path}")
else:
reason = f" ({result.reason})" if result.reason else ""
print(f"⚠ Heatmap ignorée pour {result.spec.key}{reason}.")
print("✔ Graphiques calendrier générés.")
if __name__ == "__main__":
main()

View File

@@ -1,171 +0,0 @@
# Test de l'environnement de travail
```shell
python -m scripts.test_influx_connection
```
```output
Configuration InfluxDB chargée :
URL : http://10.0.3.2:8086
Org : Dern
Bucket : weather
→ Ping du serveur InfluxDB…
✔ Ping OK
→ Requête de test sur le bucket…
✔ Requête de test réussie : 18 table(s), 58 enregistrement(s) trouvés.
Exemple de point :
time : 2025-11-16 22:30:50.263360+00:00
measurement : %
field : device_class_str
value : humidity
```
Ensuite, on peut demander à InfluxDB de nous détailler ce qu'il stocke :
```shell
python -m scripts.test_influx_schema
```
```output
Bucket InfluxDB : weather
Measurements disponibles :
- %
- hPa
- km/h
- lx
- mm/h
- °
- °C
Champs pour measurement « % » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « hPa » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « km/h » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « lx » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- value (type: unknown)
Champs pour measurement « mm/h » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
Champs pour measurement « ° » :
- friendly_name_str (type: unknown)
- value (type: unknown)
Champs pour measurement « °C » :
- device_class_str (type: unknown)
- friendly_name_str (type: unknown)
- state_class_str (type: unknown)
- value (type: unknown)
```
Mais pour obtenir les données dont on a besoin, il faut aussi connaitre les entités manipulées par Influx :
```shell
python -m scripts.test_influx_entities
```
```output
Bucket InfluxDB : weather
Measurement « % »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_humidite_relative
Measurement « hPa »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_pression_atmospherique
Measurement « km/h »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_vitesse_du_vent
Measurement « lx »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_luminance
Measurement « mm/h »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_precipitations
Measurement « ° »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_direction_du_vent
Measurement « °C »
Tag keys :
- _field
- _measurement
- _start
- _stop
- domain
- entity_id
entity_id possibles :
- station_meteo_bresser_exterieur_temperature
```
Ces informations combinées se retrouvent dans le fichier `meteo/station_config.py` et dans `meteo/variables.py`.
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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 117 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 119 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 135 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 184 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 216 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 185 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 199 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 180 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 209 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 120 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 90 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 154 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 192 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 194 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 153 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 179 KiB

View File

@@ -0,0 +1,112 @@
# Corrélations binaires
Lobjectif de ce chapitre est dexplorer les relations entre variables deux à deux : dabord 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_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 ; lautre variable change dun graphique à lautre.
![](figures/pairwise_timeseries/timeseries_temperature_vs_humidity.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_pressure.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_rain_rate.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_illuminance.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_wind_speed.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_temperature_vs_sun_elevation.png)
### Humidité relative
Ici, on fixe lhumidité comme variable principale et on observe comment elle évolue en parallèle des autres variables.
![](figures/pairwise_timeseries/timeseries_humidity_vs_pressure.png)
![](figures/pairwise_timeseries/timeseries_humidity_vs_rain_rate.png)
![](figures/pairwise_timeseries/timeseries_humidity_vs_illuminance.png)
![](figures/pairwise_timeseries/timeseries_humidity_vs_wind_speed.png)
![](figures/pairwise_timeseries/timeseries_humidity_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_humidity_vs_sun_elevation.png)
### Pression
Dans ces vues, on suit la pression atmosphérique et on la compare aux autres champs mesurés.
![](figures/pairwise_timeseries/timeseries_pressure_vs_rain_rate.png)
![](figures/pairwise_timeseries/timeseries_pressure_vs_illuminance.png)
![](figures/pairwise_timeseries/timeseries_pressure_vs_wind_speed.png)
![](figures/pairwise_timeseries/timeseries_pressure_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_pressure_vs_sun_elevation.png)
### 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.
![](figures/pairwise_timeseries/timeseries_rain_rate_vs_wind_speed.png)
![](figures/pairwise_timeseries/timeseries_rain_rate_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_rain_rate_vs_sun_elevation.png)
### Luminance
Ces superpositions éclairent les liens entre lumière, vent et position du soleil.
![](figures/pairwise_timeseries/timeseries_illuminance_vs_wind_speed.png)
![](figures/pairwise_timeseries/timeseries_illuminance_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_illuminance_vs_sun_elevation.png)
### Vent (vitesse / direction)
Enfin, on se concentre sur le vent : dabord sa vitesse en lien avec lélévation solaire, puis la direction comparée à cette même référence.
![](figures/pairwise_timeseries/timeseries_wind_speed_vs_wind_direction.png)
![](figures/pairwise_timeseries/timeseries_wind_speed_vs_sun_elevation.png)
![](figures/pairwise_timeseries/timeseries_wind_direction_vs_sun_elevation.png)
## 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 covariations, 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" --transform=identity --upper-only
```
![](figures/correlation_heatmap.png)
![](figures/correlation_heatmap_spearman.png)
Le signe et l'intensité des coefficients montrent à quel point deux variables bougent ensemble au même instant (comouvement 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.

View File

@@ -0,0 +1,170 @@
# 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]
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 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
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()
transform = args.transform
matrices = compute_correlation_matrices_for_methods(
df=df,
variables=VARIABLES,
methods=CORRELATION_METHODS,
transform=transform,
)
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=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}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,96 @@
# scripts/plot_correlations.py
"""Produit les nuages de points ciblés entre variables sélectionnées."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
from typing import Sequence
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.variables import Variable, VARIABLES, VARIABLES_BY_KEY, iter_variable_pairs
from meteo.plots import plot_scatter_pair
CSV_PATH = Path("data/weather_minutely.csv")
DOC_DIR = Path(__file__).resolve().parent.parent
SCATTER_DIR = DOC_DIR / "figures" / "pairwise_scatter"
def _select_variables(keys: Sequence[str] | None) -> list[Variable]:
if not keys:
return list(VARIABLES)
try:
selected = [VARIABLES_BY_KEY[key] for key in keys]
except KeyError as exc:
raise SystemExit(f"Variable inconnue : {exc.args[0]!r}.") from exc
return selected
def _generate_pairwise_scatter(
df: pd.DataFrame,
variables: Sequence[Variable],
*,
sample_step: int,
) -> None:
pairs = iter_variable_pairs()
selected = [(vx, vy) for vx, vy in pairs if vx in variables and vy in variables]
if not selected:
print("⚠ Aucun couple sélectionné pour les nuages de points.")
return
SCATTER_DIR.mkdir(parents=True, exist_ok=True)
for var_x, var_y in selected:
output_path = SCATTER_DIR / f"scatter_{var_x.key}_vs_{var_y.key}.png"
print(f"→ Scatter {var_y.key} vs {var_x.key}")
plot_scatter_pair(df, var_x=var_x, var_y=var_y, output_path=output_path, sample_step=sample_step)
print(f"{len(selected)} nuage(s) de points généré(s) dans {SCATTER_DIR}.")
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description="Génère des nuages de points pairwise entre variables.")
parser.add_argument(
"--dataset",
type=Path,
default=CSV_PATH,
help="Dataset à utiliser (par défaut : data/weather_minutely.csv).",
)
parser.add_argument(
"--variables",
nargs="*",
help="Restreint l'analyse à certaines clés de variables.",
)
parser.add_argument(
"--scatter-step",
type=int,
default=20,
help="Pas d'échantillonnage pour les nuages de points individuels.",
)
args = parser.parse_args(argv)
dataset_path = args.dataset
if not dataset_path.exists():
raise SystemExit(f"Dataset introuvable : {dataset_path}")
df = load_raw_csv(dataset_path)
print(f"Dataset chargé : {dataset_path} ({len(df)} lignes)")
print()
variables = _select_variables(args.variables)
_generate_pairwise_scatter(df, variables, sample_step=args.scatter_step)
print("✔ Terminé.")
return 0
if __name__ == "__main__": # pragma: no cover
raise SystemExit(main())

View File

@@ -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()

View File

@@ -1,20 +0,0 @@
# Export des données
```shell
python -m scripts.export_station_data
```
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
✔ Export terminé : /Users/richard/Documents/donnees_meteo/data/weather_raw_7d.csv
```
(Le chemin changera sur votre propre machine)
Vérifiez que le fichier est bien créé et qu'il contient des données.
À la place de `scripts.export_station_data`, on pourrait aussi lancer `scripts.export_station_data_full`.
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.
Mon fichier complet contient plus d'un million d'enregistrements et pèse 70Mo.

View File

@@ -1,209 +0,0 @@
# Ajustements
Le fichier peut être rapidement inspecté avec la commande `head` :
```shell
head data/weather_raw_full.csv
```
```output
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,,,,,,
2025-03-10 09:35:23.162398+00:00,,83.0,,,,,
2025-03-10 09:35:23.164634+00:00,,,,,7.4,,
2025-03-10 09:35:23.170122+00:00,,,,,,256.0,
2025-03-10 09:35:23.183555+00:00,,,,,,,0.0
2025-03-10 09:35:41.211148+00:00,,,,20551.2,,,
2025-03-10 09:36:22.638255+00:00,,,,,12.2,,
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.
Le script suivant s'occupe de regrouper les données de capteurs dont l'enregistrement est proche :
```shell
python -m scripts.format_raw_csv
```
```output
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'>
Après combinaison (1s) : 630171 lignes
✔ Fichier formaté écrit dans : /Users/richard/Documents/donnees_meteo/data/weather_formatted_1s.csv
```
Un nouveau document CSV intermédiaire est donc créé.
```shell
head data/weather_formatted_1s.csv
```
```output
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,,,
2025-03-10 09:36:22+00:00,,,,20247.6,12.2,306.0,
2025-03-10 09:36:52+00:00,,,,20199.6,9.3,246.0,
2025-03-10 09:37:22+00:00,,,,20034.0,7.9,,
2025-03-10 09:37:52+00:00,,,,20124.0,7.4,284.0,
2025-03-10 09:38:22+00:00,,,,19860.0,9.7,215.0,
2025-03-10 09:39:22+00:00,,,,19722.0,11.4,203.0,
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.
On fait donc :
```shell
python -m scripts.fill_formatted_1s
```
```output
Fichier 1s formaté chargé : data/weather_formatted_1s.csv
Lignes : 630171, colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wi
nd_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
```
On peut maintenant s'assurer d'avoir une seule ligne par minute, avec toutes les valeurs de capteurs :
```shell
python -m scripts.make_minutely_dataset
```
Ce qui va produire le fichier `data/weather_minutely.csv`.
On peut s'assurer que plus aucune information n'est manquante :
```shell
python -m scripts.check_missing_values
```
```output
Dataset chargé : data/weather_minutely.csv
Lignes : 321881
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'r
ain_rate']
=== Synthèse des valeurs manquantes ===
Total de cellules : 2253167
Cellules manquantes : 0
Fraction manquante : 0.000000
Lignes complètes : 321881
Lignes avec des trous : 0
Fraction lignes complètes : 1.000000
Valeurs manquantes par colonne :
- temperature : 0
- humidity : 0
- pressure : 0
- illuminance : 0
- wind_speed : 0
- wind_direction : 0
- rain_rate : 0
✔ Aucune valeur manquante dans le dataset minuté.
```
Le script suivant nous permet de vérifier rapidement si des problèmes majeurs peuvent être découverts :
```shell
python -m scripts.describe_minutely_dataset
```
```output
Dataset minuté chargé : data/weather_minutely.csv
Lignes : 321881
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'r
ain_rate'] Période : 2025-03-10 09:35:00+00:00 → 2025-11-17 00:41:00+00:00
=== describe() ===
temperature humidity pressure ... wind_speed wind_direction rain_rate
count 321881.000000 321881.000000 321881.000000 ... 321881.000000 321881.000000 321881.000000
mean 15.004488 74.131993 1010.683189 ... 2.877190 181.977411 0.108216
std 6.349077 18.885843 8.210283 ... 3.151080 88.089334 0.820691
min -2.200000 20.000000 976.973123 ... 0.000000 0.000000 0.000000
25% 10.277778 59.000000 1005.420000 ... 0.000000 96.000000 0.000000
50% 14.600000 77.666667 1011.514287 ... 2.333549 210.000000 0.000000
75% 19.000000 91.000000 1015.900000 ... 4.650000 247.666196 0.000000
max 34.888889 99.000000 1033.187174 ... 26.554176 360.000000 42.672000
[8 rows x 7 columns]
=== Min / max avec dates ===
- temperature:
min = -2.2 à 2025-03-17 05:16:00+00:00
max = 34.8888888888889 à 2025-07-02 15:59:00+00:00
- humidity:
min = 20.0 à 2025-04-30 15:22:00+00:00
max = 99.0 à 2025-03-11 06:29:00+00:00
- pressure:
min = 976.973122738378 à 2025-10-23 05:06:00+00:00
max = 1033.18717416804 à 2025-10-10 17:12:00+00:00
- illuminance:
min = 0.0 à 2025-03-10 17:44:00+00:00
max = 133520.394 à 2025-07-29 11:48:00+00:00
- wind_speed:
min = 0.0 à 2025-03-10 14:31:00+00:00
max = 26.554176 à 2025-06-26 00:10:00+00:00
- wind_direction:
min = 0.0 à 2025-03-12 04:57:00+00:00
max = 360.0 à 2025-03-12 07:33:00+00:00
- rain_rate:
min = 0.0 à 2025-03-10 09:35:00+00:00
max = 42.672 à 2025-06-15 03:10:00+00:00
=== Vérification de la continuité temporelle ===
Différences d'intervalle (top 5):
time
0 days 00:01:00 304291
0 days 00:02:00 9426
0 days 00:03:00 3562
0 days 00:04:00 1740
0 days 00:05:00 1142
Name: count, dtype: int64
Nombre d'intervalles ≠ 60s : 17589
```
Il y a donc des trous entre certains jeux de données.
Ces écarts peuvent être identifiés avec le script suivant :
```shell
python -m scripts.list_time_gaps
```
```
Dataset minuté chargé : data/weather_minutely.csv
Lignes : 321881
=== Gaps temporels détectés ===
Nombre de gaps : 17589
Total minutes manquantes (théoriques) : 40466
Top 10 des gaps les plus longs :
- De 2025-06-21 19:09:00+00:00 à 2025-06-21 20:10:00+00:00 (durée: 0 days 01:01:00, manquants: 60, de
2025-06-21 19:10:00+00:00 à 2025-06-21 20:09:00+00:00) - De 2025-08-10 22:17:00+00:00 à 2025-08-10 23:15:00+00:00 (durée: 0 days 00:58:00, manquants: 57, de
2025-08-10 22:18:00+00:00 à 2025-08-10 23:14:00+00:00) - De 2025-09-24 20:34:00+00:00 à 2025-09-24 21:32:00+00:00 (durée: 0 days 00:58:00, manquants: 57, de
2025-09-24 20:35:00+00:00 à 2025-09-24 21:31:00+00:00) - De 2025-06-21 10:58:00+00:00 à 2025-06-21 11:55:00+00:00 (durée: 0 days 00:57:00, manquants: 56, de
2025-06-21 10:59:00+00:00 à 2025-06-21 11:54:00+00:00) - De 2025-07-10 07:17:00+00:00 à 2025-07-10 08:14:00+00:00 (durée: 0 days 00:57:00, manquants: 56, de
2025-07-10 07:18:00+00:00 à 2025-07-10 08:13:00+00:00) - De 2025-07-24 03:52:00+00:00 à 2025-07-24 04:46:00+00:00 (durée: 0 days 00:54:00, manquants: 53, de
2025-07-24 03:53:00+00:00 à 2025-07-24 04:45:00+00:00) - De 2025-10-28 08:31:00+00:00 à 2025-10-28 09:23:00+00:00 (durée: 0 days 00:52:00, manquants: 51, de
2025-10-28 08:32:00+00:00 à 2025-10-28 09:22:00+00:00) - De 2025-03-16 15:31:00+00:00 à 2025-03-16 16:20:00+00:00 (durée: 0 days 00:49:00, manquants: 48, de
2025-03-16 15:32:00+00:00 à 2025-03-16 16:19:00+00:00) - De 2025-06-21 12:22:00+00:00 à 2025-06-21 13:08:00+00:00 (durée: 0 days 00:46:00, manquants: 45, de
2025-06-21 12:23:00+00:00 à 2025-06-21 13:07:00+00:00) - 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.
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.
Il faut être capable de les identifier, et il faut les prendre en compte dans tout calcul ultérieur.
Une fois que tout est passé en revue, on passe d'un jeu contenant plus d'un million d'enregistrements à un jeu n'en contenant plus que 300 000.

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 68 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 69 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 67 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 85 KiB

Some files were not shown because too many files have changed in this diff Show More