1

Réorganisation

This commit is contained in:
2025-11-19 17:01:45 +01:00
parent 566d4400ce
commit 617b12c02e
91 changed files with 874 additions and 1715 deletions

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

@@ -1,7 +1,43 @@
# Test de l'environnement de travail
# Installation, configuration et tests
## Installation de l'environnement de base
Après avoir cloné le dépôt :
```shell
python -m scripts.test_influx_connection
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
## 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
- `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
## Tests de l'environnement de travail
```shell
python "docs/01 - Installation, configuration et tests/scripts/test_influx_connection.py"
```
```output
@@ -24,7 +60,7 @@ Exemple de point :
Ensuite, on peut demander à InfluxDB de nous détailler ce qu'il stocke :
```shell
python -m scripts.test_influx_schema
python "docs/01 - Installation, configuration et tests/scripts/test_influx_schema.py"
```
```output
@@ -82,7 +118,7 @@ Champs pour measurement « °C » :
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
python "docs/01 - Installation, configuration et tests/scripts/test_influx_entities.py"
```
```output

View File

@@ -0,0 +1,65 @@
# tests/test_influx_connection.py
from __future__ import annotations
from pathlib import Path
import sys
from contextlib import closing
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.influx_client import create_influx_client, test_basic_query
def main() -> None:
"""
Teste la communication avec le serveur InfluxDB :
1. Chargement de la configuration depuis l'environnement.
2. Ping du serveur.
3. Exécution d'une requête simple sur le bucket configuré.
"""
settings = InfluxSettings.from_env()
print("Configuration InfluxDB chargée :")
print(f" URL : {settings.url}")
print(f" Org : {settings.org}")
print(f" Bucket : {settings.bucket}")
print()
# Utilisation de `closing` pour garantir la fermeture du client.
with closing(create_influx_client(settings)) as client:
print("→ Ping du serveur InfluxDB…")
if not client.ping():
raise SystemExit("Échec du ping InfluxDB. Vérifiez l'URL et l'état du serveur.")
print("✔ Ping OK")
print("→ Requête de test sur le bucket…")
tables = test_basic_query(client, settings.bucket)
# On fait un retour synthétique
nb_tables = len(tables)
nb_records = sum(len(table.records) for table in tables)
print(f"✔ Requête de test réussie : {nb_tables} table(s), {nb_records} enregistrement(s) trouvés.")
if nb_records == 0:
print("⚠ Le bucket est accessible, mais aucune donnée sur la dernière heure.")
else:
# Affiche un aperçu de la première table / premier record
first_table = tables[0]
first_record = first_table.records[0]
print("Exemple de point :")
print(f" time : {first_record.get_time()}")
print(f" measurement : {first_record.get_measurement()}")
print(f" field : {first_record.get_field()}")
print(f" value : {first_record.get_value()}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,68 @@
# tests/test_influx_entities.py
from __future__ import annotations
from pathlib import Path
import sys
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.influx_client import create_influx_client
from meteo.schema import (
list_measurements,
list_measurement_tag_keys,
list_measurement_tag_values,
)
def main() -> None:
"""
Explore les tags des measurements du bucket :
- affiche les keys de tags pour chaque measurement
- si un tag `entity_id` est présent, affiche la liste de ses valeurs
"""
settings = InfluxSettings.from_env()
print(f"Bucket InfluxDB : {settings.bucket}")
print()
with closing(create_influx_client(settings)) as client:
measurements = list_measurements(client, settings.bucket)
if not measurements:
print("⚠ Aucun measurement trouvé dans ce bucket.")
return
for meas in measurements:
print(f"Measurement « {meas} »")
tag_keys = list_measurement_tag_keys(client, settings.bucket, meas)
if not tag_keys:
print(" (aucun tag trouvé)")
print()
continue
print(" Tag keys :")
for key in tag_keys:
print(f" - {key}")
if "entity_id" in tag_keys:
entity_ids = list_measurement_tag_values(
client,
settings.bucket,
meas,
tag="entity_id",
)
print(" entity_id possibles :")
for eid in entity_ids:
print(f" - {eid}")
print()
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,54 @@
# tests/test_influx_schema.py
from __future__ import annotations
from pathlib import Path
import sys
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.influx_client import create_influx_client
from meteo.schema import list_measurements, list_measurement_fields
def main() -> None:
"""
Explore le schéma du bucket InfluxDB configuré :
- liste des measurements disponibles
- pour chacun, liste des champs (_field) et de leur type
"""
settings = InfluxSettings.from_env()
print(f"Bucket InfluxDB : {settings.bucket}")
print()
with closing(create_influx_client(settings)) as client:
measurements = list_measurements(client, settings.bucket)
if not measurements:
print("⚠ Aucun measurement trouvé dans ce bucket.")
return
print("Measurements disponibles :")
for name in measurements:
print(f" - {name}")
print()
for name in measurements:
print(f"Champs pour measurement « {name} » :")
fields = list_measurement_fields(client, settings.bucket, name)
if not fields:
print(" (aucun champ trouvé)")
else:
for field in fields:
print(f" - {field.name} (type: {field.type})")
print()
if __name__ == "__main__":
main()

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

@@ -1,4 +1,34 @@
# Ajustements
# 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.
## Export des données
```shell
python "docs/02 - Préparation des données/scripts/export_station_data.py"
```
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 `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 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.
## Ajustements
Le fichier peut être rapidement inspecté avec la commande `head` :
@@ -24,7 +54,7 @@ On peut voir que HomeAssistant écrit une nouvelle entrée pour chaque capteur,
Le script suivant s'occupe de regrouper les données de capteurs dont l'enregistrement est proche :
```shell
python -m scripts.format_raw_csv
python "docs/02 - Préparation des données/scripts/format_raw_csv.py"
```
```output
@@ -59,35 +89,42 @@ Il reste des cellules vides : en effet, HA n'enregistre pas la valeur d'un capte
On fait donc :
```shell
python -m scripts.fill_formatted_1s
python "docs/02 - Préparation des données/scripts/fill_formatted_1s.py"
```
```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
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
```
On peut maintenant s'assurer d'avoir une seule ligne par minute, avec toutes les valeurs de capteurs :
## 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 :
- 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.
Ces opérations sont réalisées par :
```shell
python -m scripts.make_minutely_dataset
python "docs/02 - Préparation des données/scripts/make_minutely_dataset.py"
```
Ce qui va produire le fichier `data/weather_minutely.csv`.
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.
On peut s'assurer que plus aucune information n'est manquante :
```shell
python -m scripts.check_missing_values
python "docs/02 - Préparation des données/scripts/check_missing_values.py"
```
```output
Dataset chargé : data/weather_minutely.csv
Lignes : 321881
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'r
ain_rate']
Colonnes : ['temperature', 'humidity', 'pressure', 'illuminance', 'wind_speed', 'wind_direction', 'rain_rate']
=== Synthèse des valeurs manquantes ===
Total de cellules : 2253167
Cellules manquantes : 0
@@ -111,14 +148,13 @@ Valeurs manquantes par colonne :
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
python "docs/02 - Préparation des données/scripts/describe_minutely_dataset.py"
```
```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
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
@@ -169,11 +205,10 @@ 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
python "docs/02 - Préparation des données/scripts/list_time_gaps.py"
```
```
@@ -185,17 +220,16 @@ 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)
- 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.

View File

@@ -0,0 +1,58 @@
# scripts/check_missing_values.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.quality import summarize_missing_values
CSV_PATH = Path("data/weather_minutely.csv")
def main() -> None:
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)
print(f"Dataset chargé : {CSV_PATH}")
print(f" Lignes : {len(df)}")
print(f" Colonnes : {list(df.columns)}")
summary = summarize_missing_values(df)
print()
print("=== Synthèse des valeurs manquantes ===")
print(f"Total de cellules : {summary.total_cells}")
print(f"Cellules manquantes : {summary.missing_cells}")
print(f"Fraction manquante : {summary.fraction_missing:.6f}")
print(f"Lignes complètes : {summary.rows_fully_complete}")
print(f"Lignes avec des trous : {summary.rows_with_missing}")
print(f"Fraction lignes complètes : {summary.fraction_rows_complete:.6f}")
print()
print("Valeurs manquantes par colonne :")
for col, n_missing in summary.missing_by_column.items():
print(f" - {col:13s} : {n_missing}")
if summary.missing_cells == 0:
print()
print("✔ Aucune valeur manquante dans le dataset minuté.")
else:
print()
print("⚠ Il reste des valeurs manquantes.")
print(" Exemple de lignes concernées :")
rows_with_missing = df[df.isna().any(axis=1)]
print(rows_with_missing.head(10))
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,67 @@
# scripts/describe_minutely_dataset.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
CSV_PATH = Path("data/weather_minutely.csv")
def main() -> None:
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)
print(f"Dataset minuté chargé : {CSV_PATH}")
print(f" Lignes : {len(df)}")
print(f" Colonnes : {list(df.columns)}")
print(f" Période : {df.index[0]}{df.index[-1]}")
print()
# 1. Résumé statistique classique
print("=== describe() ===")
print(df.describe())
print()
# 2. Min / max par variable avec leurs dates
print("=== Min / max avec dates ===")
for col in df.columns:
series = df[col]
min_val = series.min()
max_val = series.max()
min_ts = series.idxmin()
max_ts = series.idxmax()
print(f"- {col}:")
print(f" min = {min_val} à {min_ts}")
print(f" max = {max_val} à {max_ts}")
print()
# 3. Vérification rapide de la continuité temporelle
print("=== Vérification de la continuité temporelle ===")
diffs = df.index.to_series().diff().dropna()
counts = diffs.value_counts().sort_index()
print("Différences d'intervalle (top 5):")
print(counts.head())
print()
nb_not_60s = (diffs != pd.Timedelta(minutes=1)).sum()
print(f"Nombre d'intervalles ≠ 60s : {nb_not_60s}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,51 @@
# tests/export_station_data.py
from __future__ import annotations
from pathlib import Path
import sys
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.influx_client import create_influx_client
from meteo.station_config import default_station_config
from meteo.export import export_station_data
def main() -> None:
"""
Exporte les données de la station météo vers un fichier CSV brut.
Par défaut, on exporte les 7 derniers jours dans `data/weather_raw_7d.csv`.
"""
settings = InfluxSettings.from_env()
station_config = default_station_config()
print("Configuration InfluxDB :")
print(f" URL : {settings.url}")
print(f" Org : {settings.org}")
print(f" Bucket : {settings.bucket}")
print()
with closing(create_influx_client(settings)) as client:
print("→ Export des 7 derniers jours…")
output_path = export_station_data(
client=client,
bucket=settings.bucket,
config=station_config,
start="-7d", # à ajuster plus tard si besoin
stop=None, # now()
output_path="data/weather_raw_7d.csv",
file_format="csv",
)
print()
print(f"✔ Export terminé : {output_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,56 @@
# tests/export_station_data_full.py
from __future__ import annotations
from pathlib import Path
import sys
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.influx_client import create_influx_client
from meteo.station_config import default_station_config
from meteo.export import export_station_data
def main() -> None:
"""
Exporte l'historique complet de la station météo vers un fichier CSV.
On utilise `start=0`, ce qui signifie "depuis le début des données"
(en pratique depuis l'epoch, donc tout ce que le bucket contient).
"""
settings = InfluxSettings.from_env()
station_config = default_station_config()
print("Configuration InfluxDB :")
print(f" URL : {settings.url}")
print(f" Org : {settings.org}")
print(f" Bucket : {settings.bucket}")
print()
print("⚠ Attention : un export complet peut produire un fichier volumineux "
"et prendre un certain temps si l'historique est long.")
print()
with closing(create_influx_client(settings)) as client:
print("→ Export de l'historique complet…")
output_path = export_station_data(
client=client,
bucket=settings.bucket,
config=station_config,
start="0", # depuis le début des données
stop=None, # jusqu'à maintenant
output_path="data/weather_raw_full.csv",
file_format="csv", # vous pouvez mettre "parquet" si vous préférez
)
print()
print(f"✔ Export terminé : {output_path}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,38 @@
# scripts/fill_formatted_1s.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, fill_missing_with_previous
INPUT_CSV_PATH = Path("data/weather_formatted_1s.csv")
OUTPUT_CSV_PATH = Path("data/weather_filled_1s.csv")
def main() -> None:
if not INPUT_CSV_PATH.exists():
print(f"⚠ Fichier introuvable : {INPUT_CSV_PATH}")
print(' Lancez d\'abord : python "docs/02 - Préparation des données/scripts/format_raw_csv.py"')
return
df_1s = load_raw_csv(INPUT_CSV_PATH)
print(f"Fichier 1s formaté chargé : {INPUT_CSV_PATH}")
print(f" Lignes : {len(df_1s)}, colonnes : {list(df_1s.columns)}")
df_filled = fill_missing_with_previous(df_1s)
print(f"Après propagation des dernières valeurs connues : {len(df_filled)} lignes")
OUTPUT_CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
df_filled.to_csv(OUTPUT_CSV_PATH, index_label="time")
print(f"✔ Fichier 1s 'complet' écrit dans : {OUTPUT_CSV_PATH.resolve()}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,37 @@
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, combine_close_observations
RAW_CSV_PATH = Path("data/weather_raw_full.csv")
OUTPUT_CSV_PATH = Path("data/weather_formatted_1s.csv")
def main() -> None:
if not RAW_CSV_PATH.exists():
print(f"⚠ Fichier brut introuvable : {RAW_CSV_PATH}")
return
df_raw = load_raw_csv(RAW_CSV_PATH)
print(f"Fichier brut chargé : {RAW_CSV_PATH}")
print(f" Lignes : {len(df_raw)}, colonnes : {list(df_raw.columns)}")
print(f" Type d'index : {type(df_raw.index)}")
df_fmt = combine_close_observations(df_raw, freq="1s", agg="mean")
print(f"Après combinaison (1s) : {len(df_fmt)} lignes")
OUTPUT_CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
df_fmt.to_csv(OUTPUT_CSV_PATH, index_label="time")
print(f"✔ Fichier formaté écrit dans : {OUTPUT_CSV_PATH.resolve()}")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,52 @@
# scripts/list_time_gaps.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.gaps import find_time_gaps
CSV_PATH = Path("data/weather_minutely.csv")
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)}")
gaps = find_time_gaps(df)
total_missing = sum(g.missing_intervals for g in gaps)
print()
print("=== Gaps temporels détectés ===")
print(f"Nombre de gaps : {len(gaps)}")
print(f"Total minutes manquantes (théoriques) : {total_missing}")
print()
if not gaps:
print("✔ Aucun gap détecté, la série est parfaitement régulière.")
return
print("Top 10 des gaps les plus longs :")
gaps_sorted = sorted(gaps, key=lambda g: g.missing_intervals, reverse=True)[:10]
for g in gaps_sorted:
print(
f"- De {g.before} à {g.after} "
f"(durée: {g.duration}, manquants: {g.missing_intervals}, "
f"de {g.missing_start} à {g.missing_end})"
)
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,65 @@
# scripts/make_minutely_dataset.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, resample_to_minutes
from meteo.config import StationLocation
from meteo.solar import add_solar_elevation_column
from meteo.season import add_season_column
FORMATTED_CSV_PATH = Path("data/weather_filled_1s.csv")
OUTPUT_CSV_PATH = Path("data/weather_minutely.csv")
def main() -> None:
if not FORMATTED_CSV_PATH.exists():
print(f"⚠ Fichier formaté introuvable : {FORMATTED_CSV_PATH}")
print(' Lancez d\'abord : python "docs/02 - Préparation des données/scripts/fill_formatted_1s.py"')
return
df_1s = load_raw_csv(FORMATTED_CSV_PATH)
print(f"Fichier 1s chargé : {FORMATTED_CSV_PATH}")
print(f" Lignes : {len(df_1s)}, colonnes : {list(df_1s.columns)}")
df_min = resample_to_minutes(df_1s)
print(f"Après resampling 60s : {len(df_min)} lignes")
hemisphere = "north"
location = StationLocation.from_env(optional=True)
if location is not None:
hemisphere = "south" if location.latitude < 0 else "north"
print(
f"Ajout de l'élévation solaire (lat={location.latitude}, lon={location.longitude}, "
f"alt={location.elevation_m} m)..."
)
add_solar_elevation_column(
df_min,
latitude=location.latitude,
longitude=location.longitude,
elevation_m=location.elevation_m,
)
else:
print(
" Coordonnées GPS non définies (STATION_LATITUDE / STATION_LONGITUDE). "
"La colonne sun_elevation ne sera pas ajoutée."
)
print(" Saison : hypothèse par défaut = hémisphère nord. Définissez STATION_LATITUDE pour adapter.")
add_season_column(df_min, hemisphere=hemisphere)
OUTPUT_CSV_PATH.parent.mkdir(parents=True, exist_ok=True)
df_min.to_csv(OUTPUT_CSV_PATH, index_label="time")
print(f"✔ Dataset minuté écrit dans : {OUTPUT_CSV_PATH.resolve()}")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 65 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 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: 58 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

View File

@@ -0,0 +1,69 @@
# 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.
## Température
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only temperature
```
![](figures/temperature_last_7_days.png)
## Humidité relative
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only humidity
```
![](figures/humidity_last_7_days.png)
## Pression atmosphérique
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only pressure
```
![](figures/pressure_last_7_days.png)
## Précipitations instantanées
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only rain_rate
```
![](figures/rain_rate_last_7_days.png)
## Luminance
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only illuminance
```
![](figures/illuminance_last_7_days.png)
## Vitesse du vent
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only wind_speed
```
![](figures/wind_speed_last_7_days.png)
## Direction du vent
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only wind_direction
```
![](figures/wind_direction_last_7_days.png)
## Élévation solaire (si disponible après enrichissement)
```shell
python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" --only sun_elevation
```
![](figures/sun_elevation_last_7_days.png)

View File

@@ -0,0 +1,113 @@
# scripts/plot_basic_variables.py
"""Génère des séries temporelles simples (7 jours) pour chaque variable météo."""
from __future__ import annotations
import argparse
from pathlib import Path
import sys
import matplotlib.pyplot as plt
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 export_plot_dataset
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 _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."""
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()
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)")
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
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=7,
help="Nombre de jours à afficher (par défaut : 7).",
)
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_hourly = _prepare_slice(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)
for variable in selected:
_plot_variable(df_hourly, variable, args.output_dir)
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 497 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 432 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 610 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 587 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 379 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 235 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 364 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 351 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 390 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 325 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 88 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 111 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 108 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 97 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 445 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 430 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 363 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 512 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 471 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 340 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 574 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 403 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 201 KiB

View File

@@ -0,0 +1,72 @@
# 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.
```shell
python "docs/04 - Corrélations binaires/scripts/plot_all_pairwise_scatter.py"
```
![](figures/pairwise_scatter/scatter_humidity_vs_illuminance.png)
![](figures/pairwise_scatter/scatter_humidity_vs_pressure.png)
![](figures/pairwise_scatter/scatter_humidity_vs_rain_rate.png)
![](figures/pairwise_scatter/scatter_humidity_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_humidity_vs_wind_direction.png)
![](figures/pairwise_scatter/scatter_humidity_vs_wind_speed.png)
![](figures/pairwise_scatter/scatter_illuminance_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_illuminance_vs_wind_direction.png)
![](figures/pairwise_scatter/scatter_illuminance_vs_wind_speed.png)
![](figures/pairwise_scatter/scatter_pressure_vs_illuminance.png)
![](figures/pairwise_scatter/scatter_pressure_vs_rain_rate.png)
![](figures/pairwise_scatter/scatter_pressure_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_pressure_vs_wind_direction.png)
![](figures/pairwise_scatter/scatter_pressure_vs_wind_speed.png)
![](figures/pairwise_scatter/scatter_rain_rate_vs_illuminance.png)
![](figures/pairwise_scatter/scatter_rain_rate_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_rain_rate_vs_wind_direction.png)
![](figures/pairwise_scatter/scatter_rain_rate_vs_wind_speed.png)
![](figures/pairwise_scatter/scatter_temperature_vs_humidity.png)
![](figures/pairwise_scatter/scatter_temperature_vs_illuminance.png)
![](figures/pairwise_scatter/scatter_temperature_vs_pressure.png)
![](figures/pairwise_scatter/scatter_temperature_vs_rain_rate.png)
![](figures/pairwise_scatter/scatter_temperature_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_temperature_vs_wind_direction.png)
![](figures/pairwise_scatter/scatter_temperature_vs_wind_speed.png)
![](figures/pairwise_scatter/scatter_wind_direction_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_wind_speed_vs_sun_elevation.png)
![](figures/pairwise_scatter/scatter_wind_speed_vs_wind_direction.png)
## Heatmap globale
```shell
python "docs/04 - Corrélations binaires/scripts/plot_correlation_heatmap.py"
```
![](figures/correlation_heatmap.png)

View File

@@ -0,0 +1,52 @@
# 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()

View File

@@ -0,0 +1,52 @@
# scripts/plot_correlation_heatmap.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 VARIABLES
from meteo.analysis import compute_correlation_matrix_for_variables
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"
def main() -> None:
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)
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")
print("Matrice de corrélation (aperçu) :")
print(corr)
print()
output_path = plot_correlation_heatmap(
corr=corr,
variables=VARIABLES,
output_path=OUTPUT_PATH,
annotate=True,
)
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

@@ -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.

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 48 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 256 KiB

View File

@@ -0,0 +1,29 @@
# Corrélations binaires avancées
## Corrélations décalées
```shell
python "docs/05 - Corrélations binaires avancées/scripts/plot_lagged_correlations.py"
```
![](figures/lagged_correlations/lagcorr_humidity_to_rain_rate.png)
![](figures/lagged_correlations/lagcorr_illuminance_to_temperature.png)
![](figures/lagged_correlations/lagcorr_pressure_to_illuminance.png)
![](figures/lagged_correlations/lagcorr_pressure_to_rain_rate.png)
![](figures/lagged_correlations/lagcorr_pressure_to_wind_speed.png)
![](figures/lagged_correlations/lagcorr_temperature_to_humidity.png)
![](figures/lagged_correlations/lagcorr_temperature_to_rain_rate.png)
## Corrélations glissantes
```shell
python "docs/05 - Corrélations binaires avancées/scripts/plot_rolling_correlation_heatmap.py"
```
![](figures/rolling_correlations/rolling_correlation_heatmap.png)

View File

@@ -0,0 +1,64 @@
# scripts/plot_lagged_correlations.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 VARIABLES_BY_KEY
from meteo.analysis import compute_lagged_correlation
from meteo.plots import plot_lagged_correlation
from meteo.correlation_presets import DEFAULT_LAGGED_PAIRS
CSV_PATH = Path("data/weather_minutely.csv")
DOC_DIR = Path(__file__).resolve().parent.parent
OUTPUT_DIR = DOC_DIR / "figures" / "lagged_correlations"
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)}")
print()
for key_x, key_y in DEFAULT_LAGGED_PAIRS:
var_x = VARIABLES_BY_KEY[key_x]
var_y = VARIABLES_BY_KEY[key_y]
print(f"→ Corrélation décalée : {var_x.key}{var_y.key}")
lag_df = compute_lagged_correlation(
df=df,
var_x=var_x,
var_y=var_y,
max_lag_minutes=360, # ± 6 heures
step_minutes=10, # pas de 10 minutes
method="pearson",
)
filename = f"lagcorr_{var_x.key}_to_{var_y.key}.png"
output_path = OUTPUT_DIR / filename
plot_lagged_correlation(
lag_df=lag_df,
var_x=var_x,
var_y=var_y,
output_path=output_path,
)
print("✔ Graphiques de corrélation décalée générés.")
if __name__ == "__main__":
main()

View File

@@ -0,0 +1,65 @@
# scripts/plot_rolling_correlation_heatmap.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 VARIABLES_BY_KEY
from meteo.analysis import compute_rolling_correlations_for_pairs
from meteo.plots import plot_rolling_correlation_heatmap
from meteo.correlation_presets import DEFAULT_ROLLING_PAIRS
CSV_PATH = Path("data/weather_minutely.csv")
DOC_DIR = Path(__file__).resolve().parent.parent
OUTPUT_PATH = DOC_DIR / "figures" / "rolling_correlations" / "rolling_correlation_heatmap.png"
WINDOW_MINUTES = 180 # 3 heures pour observer les tendances synoptiques
STEP_MINUTES = 30 # on n'échantillonne qu'un point sur 30 minutes
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)}")
print()
pairs = [(VARIABLES_BY_KEY[a], VARIABLES_BY_KEY[b]) for a, b in DEFAULT_ROLLING_PAIRS]
rolling_df = compute_rolling_correlations_for_pairs(
df=df,
pairs=pairs,
window_minutes=WINDOW_MINUTES,
min_valid_fraction=0.7,
step_minutes=STEP_MINUTES,
method="pearson",
)
if rolling_df.empty:
print("⚠ Impossible de calculer les corrélations glissantes (données insuffisantes).")
return
output_path = plot_rolling_correlation_heatmap(
rolling_corr=rolling_df,
output_path=OUTPUT_PATH,
cmap="coolwarm",
vmin=-1.0,
vmax=1.0,
)
print(f"✔ Heatmap de corrélations glissantes enregistrée : {output_path}")
if __name__ == "__main__":
main()

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 83 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

View File

@@ -0,0 +1,13 @@
# Corrélations multiples
## Hexbin colorés
```shell
python "docs/06 - Corrélations multiples/scripts/plot_hexbin_explorations.py"
```
![](figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png)
![](figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png)
![](figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png)

View File

@@ -0,0 +1,70 @@
# scripts/plot_hexbin_explorations.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 VARIABLES_BY_KEY
from meteo.plots import plot_hexbin_with_third_variable
from meteo.correlation_presets import DEFAULT_HEXBIN_SCENARIOS
CSV_PATH = Path("data/weather_minutely.csv")
DOC_DIR = Path(__file__).resolve().parent.parent
OUTPUT_DIR = DOC_DIR / "figures" / "hexbin_explorations"
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)}")
print()
for scenario in DEFAULT_HEXBIN_SCENARIOS:
var_x = VARIABLES_BY_KEY[scenario.key_x]
var_y = VARIABLES_BY_KEY[scenario.key_y]
var_color = VARIABLES_BY_KEY[scenario.key_color]
filename = scenario.filename
output_path = OUTPUT_DIR / filename
reduce_func = scenario.get_reduce_func()
reduce_label = scenario.get_reduce_label()
gridsize = scenario.gridsize
mincnt = scenario.mincnt
description = scenario.description
print(f"→ Hexbin {var_y.key} vs {var_x.key} (couleur = {var_color.key})")
print(f" {description}")
plot_hexbin_with_third_variable(
df=df,
var_x=var_x,
var_y=var_y,
var_color=var_color,
output_path=output_path,
gridsize=gridsize,
mincnt=mincnt,
reduce_func=reduce_func,
reduce_func_label=reduce_label,
cmap="magma",
)
print(f" ✔ Graphique enregistré : {output_path}")
print()
print("✔ Tous les graphiques hexbin ont été générés.")
if __name__ == "__main__":
main()

View File

@@ -1,68 +0,0 @@
# 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.
## Température
```shell
python -m scripts.plot_basic_variables --only temperature
```
![](../figures/basic/temperature_last_7_days.png)
## Humidité relative
```shell
python -m scripts.plot_basic_variables --only humidity
```
![](../figures/basic/humidity_last_7_days.png)
## Pression atmosphérique
```shell
python -m scripts.plot_basic_variables --only pressure
```
![](../figures/basic/pressure_last_7_days.png)
## Précipitations instantanées
```shell
python -m scripts.plot_basic_variables --only rain_rate
```
![](../figures/basic/rain_rate_last_7_days.png)
## Luminance
```shell
python -m scripts.plot_basic_variables --only illuminance
```
![](../figures/basic/illuminance_last_7_days.png)
## Vitesse du vent
```shell
python -m scripts.plot_basic_variables --only wind_speed
```
![](../figures/basic/wind_speed_last_7_days.png)
## Direction du vent
```shell
python -m scripts.plot_basic_variables --only wind_direction
```
![](../figures/basic/wind_direction_last_7_days.png)
## Élévation solaire (si disponible après enrichissement)
```shell
python -m scripts.plot_basic_variables --only sun_elevation
```
![](../figures/basic/sun_elevation_last_7_days.png)