diff --git a/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png new file mode 100644 index 0000000..ea71612 Binary files /dev/null and b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png differ diff --git a/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png new file mode 100644 index 0000000..9d4ee9b Binary files /dev/null and b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png differ diff --git a/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_lux_color_rain.png b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_lux_color_rain.png new file mode 100644 index 0000000..1a00b83 Binary files /dev/null and b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_lux_color_rain.png differ diff --git a/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_temp_color_humidity.png b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_temp_color_humidity.png new file mode 100644 index 0000000..1bc8ea1 Binary files /dev/null and b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_sunelev_temp_color_humidity.png differ diff --git a/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png new file mode 100644 index 0000000..b129e0e Binary files /dev/null and b/docs/07 - Corrélations multiples/figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png differ diff --git a/docs/07 - Corrélations multiples/index.md b/docs/07 - Corrélations multiples/index.md index e954d78..25c3886 100644 --- a/docs/07 - Corrélations multiples/index.md +++ b/docs/07 - Corrélations multiples/index.md @@ -2,12 +2,42 @@ ## Hexbin colorés +Les nuages de points tri-variés saturent vite : on regroupe ici les points sur une grille hexagonale et on colore chaque case par une statistique de la 3ᵉ variable (max/médiane/moyenne selon les besoins). Cela limite le bruit des outliers et met en évidence les régimes dominants plutôt que les valeurs isolées. Les scénarios sont décrits dans `meteo/correlation_presets.py` et exécutés via un helper générique (`meteo.plots.hexbin`) pour rester réutilisables ailleurs dans le dépôt. + ```shell -python "docs/06 - Corrélations multiples/scripts/plot_hexbin_explorations.py" +python "docs/07 - Corrélations multiples/scripts/plot_hexbin_explorations.py" ``` -![](figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png) +### Température vs humidité — couleur = pluie (max) -![](figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png) +Plafond d'humidité quasi systématique sous 5–10 °C, et même quand la température remonte, la pluie ne survient que dans une bande 8–16 °C où l'humidité reste >90 %. L'agrégat `max` met en valeur les épisodes pluvieux rares. -![](figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png) +![Température vs humidité, couleur pluie](./figures/hexbin_explorations/hexbin_temp_humidity_color_rain.png) + +### Température vs élévation solaire — couleur = humidité (médiane) + +La courbe en cloche suit le soleil : températures les plus hautes autour de 60° d'élévation, tandis que l'humidité médiane dégringole dès que le soleil est positif puis remonte au crépuscule, ce qui matérialise l'assèchement diurne. + +![Température vs élévation solaire, couleur humidité](./figures/hexbin_explorations/hexbin_sunelev_temp_color_humidity.png) + +### Pression vs pluie — couleur = vent (médiane) + +La matrice est clairsemée (pluie rare), mais les cases actives se concentrent sous ~1015 hPa avec des médianes de vent plus élevées, signe que les épisodes pluvieux et venteux coïncident surtout avec des pressions modestes. + +![Pression vs pluie, couleur vent](./figures/hexbin_explorations/hexbin_pressure_rain_color_wind.png) + +### Humidité vs illuminance — couleur = température (moyenne) + +Deux régimes se détachent : nuits très humides et fraîches (illuminance proche de zéro), journées sèches et plus chaudes. La température moyenne colorée rend visibles les transitions plus fraîches sous ciel couvert. + +![Humidité vs illuminance, couleur température](./figures/hexbin_explorations/hexbin_lux_humidity_color_temp.png) + +### Élévation solaire vs illuminance — couleur = pluie (max) + +Le nuage suit la diagonale « géométrie du soleil → lumière attendue », avec des cases assombries (illuminance faible malgré un soleil haut) où la pluie maximale ressort : on visualise directement l'impact d'un ciel très chargé sur l'apport lumineux. + +![Élévation solaire vs illuminance, couleur pluie](./figures/hexbin_explorations/hexbin_sunelev_lux_color_rain.png) + +## Conclusion + +Ces hexbins confirment les mécanismes physiques attendus : le rayonnement solaire chauffe et assèche, l’humidité suit la capacité de l’air en vapeur, la pluie survient surtout sous pression plus basse et lumière écrasée par les nuages. Mais notre jeu s’arrête en novembre : il manque la saison froide, donc les régimes hivernaux (neige, pluies froides, journées très courtes, plafond d’humidité quasi permanent) restent invisibles. Toute généralisation doit tenir compte de cette lacune saisonnière ; il faudrait compléter la série ou intégrer des données externes (nébulosité, contexte synoptique) pour confirmer ces motifs en hiver. diff --git a/docs/07 - Corrélations multiples/scripts/plot_hexbin_explorations.py b/docs/07 - Corrélations multiples/scripts/plot_hexbin_explorations.py index 6c9e609..d7dc9a9 100644 --- a/docs/07 - Corrélations multiples/scripts/plot_hexbin_explorations.py +++ b/docs/07 - Corrélations multiples/scripts/plot_hexbin_explorations.py @@ -9,8 +9,7 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from meteo.dataset import load_raw_csv -from meteo.variables import VARIABLES_BY_KEY -from meteo.plots import plot_hexbin_with_third_variable +from meteo.plots import generate_hexbin_scenarios from meteo.correlation_presets import DEFAULT_HEXBIN_SCENARIOS @@ -30,37 +29,19 @@ def main() -> None: 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] + results = generate_hexbin_scenarios( + df=df, + scenarios=DEFAULT_HEXBIN_SCENARIOS, + base_output_dir=OUTPUT_DIR, + cmap="magma", + ) - 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}") + for result in results: + scenario = result.scenario + print(f"→ Hexbin {result.var_y.key} vs {result.var_x.key} (couleur = {result.var_color.key})") + print(f" {scenario.description}") + print(f" Points valides : {result.point_count}") + print(f" ✔ Graphique enregistré : {result.output_path}") print() print("✔ Tous les graphiques hexbin ont été générés.") diff --git a/meteo/correlation_presets.py b/meteo/correlation_presets.py index 9e4f001..010498a 100644 --- a/meteo/correlation_presets.py +++ b/meteo/correlation_presets.py @@ -111,6 +111,19 @@ DEFAULT_HEXBIN_SCENARIOS: Sequence[HexbinScenario] = ( gridsize=50, mincnt=8, ), + HexbinScenario( + key_x="sun_elevation", + key_y="temperature", + key_color="humidity", + filename="hexbin_sunelev_temp_color_humidity.png", + description=( + "Suivre le cycle diurne : la température grimpe avec l'élévation solaire " + "et l'humidité médiane chute nettement une fois le soleil levé." + ), + reduce="median", + gridsize=60, + mincnt=8, + ), HexbinScenario( key_x="pressure", key_y="rain_rate", @@ -137,6 +150,20 @@ DEFAULT_HEXBIN_SCENARIOS: Sequence[HexbinScenario] = ( gridsize=55, mincnt=6, ), + HexbinScenario( + key_x="sun_elevation", + key_y="illuminance", + key_color="rain_rate", + filename="hexbin_sunelev_lux_color_rain.png", + description=( + "Comparer l'ensoleillement théorique (élévation solaire) et la luminance mesurée, " + "et repérer les épisodes où la pluie ou un ciel très couvert écrasent la lumière " + "malgré un soleil haut." + ), + reduce="max", + gridsize=48, + mincnt=3, + ), ) diff --git a/meteo/plots/__init__.py b/meteo/plots/__init__.py index fefaca9..d932eaf 100644 --- a/meteo/plots/__init__.py +++ b/meteo/plots/__init__.py @@ -24,6 +24,12 @@ from .relationships import ( plot_pairwise_relationship_grid, plot_scatter_pair, ) +from .hexbin import ( + HexbinPlotResult, + generate_hexbin_for_scenario, + generate_hexbin_scenarios, + prepare_hexbin_dataframe, +) from .basic_series import ( PlotChoice, PlotStyle, @@ -66,6 +72,10 @@ __all__ = [ "plot_hexbin_with_third_variable", "plot_pairwise_relationship_grid", "plot_scatter_pair", + "HexbinPlotResult", + "generate_hexbin_for_scenario", + "generate_hexbin_scenarios", + "prepare_hexbin_dataframe", "PlotChoice", "PlotStyle", "plot_basic_series", diff --git a/meteo/plots/hexbin.py b/meteo/plots/hexbin.py new file mode 100644 index 0000000..9ea3bc7 --- /dev/null +++ b/meteo/plots/hexbin.py @@ -0,0 +1,127 @@ +"""Fonctions utilitaires pour générer des hexbins à partir de scénarios.""" + +from __future__ import annotations + +from dataclasses import dataclass +from pathlib import Path +from typing import Sequence + +import pandas as pd + +from meteo.correlation_presets import HexbinScenario +from meteo.variables import VARIABLES_BY_KEY, Variable + +from .relationships import plot_hexbin_with_third_variable + +__all__ = [ + "HexbinPlotResult", + "prepare_hexbin_dataframe", + "generate_hexbin_for_scenario", + "generate_hexbin_scenarios", +] + + +@dataclass(frozen=True) +class HexbinPlotResult: + """Résultat d'un hexbin généré à partir d'un scénario prédéfini.""" + + scenario: HexbinScenario + var_x: Variable + var_y: Variable + var_color: Variable + output_path: Path + point_count: int + + +def _get_variables_for_scenario(scenario: HexbinScenario) -> tuple[Variable, Variable, Variable]: + try: + return ( + VARIABLES_BY_KEY[scenario.key_x], + VARIABLES_BY_KEY[scenario.key_y], + VARIABLES_BY_KEY[scenario.key_color], + ) + except KeyError as exc: + raise KeyError(f"Variable inconnue dans le scénario hexbin : {exc}") from exc + + +def prepare_hexbin_dataframe( + df: pd.DataFrame, + var_x: Variable, + var_y: Variable, + var_color: Variable, +) -> pd.DataFrame: + """Sélectionne et nettoie les colonnes utiles à un hexbin.""" + + columns = [var_x.column, var_y.column, var_color.column] + missing = [col for col in columns if col not in df.columns] + if missing: + raise KeyError(f"Colonnes absentes dans le DataFrame : {missing}") + + return df[columns].dropna() + + +def generate_hexbin_for_scenario( + df: pd.DataFrame, + scenario: HexbinScenario, + *, + base_output_dir: str | Path, + cmap: str = "viridis", +) -> HexbinPlotResult: + """Construit un hexbin complet à partir d'un scénario et retourne ses métadonnées.""" + + var_x, var_y, var_color = _get_variables_for_scenario(scenario) + cleaned_df = prepare_hexbin_dataframe(df, var_x, var_y, var_color) + + output_dir = Path(base_output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + output_path = output_dir / scenario.filename + + reduce_func = scenario.get_reduce_func() + reduce_label = scenario.get_reduce_label() + + plot_hexbin_with_third_variable( + df=cleaned_df, + var_x=var_x, + var_y=var_y, + var_color=var_color, + output_path=output_path, + gridsize=scenario.gridsize, + mincnt=scenario.mincnt, + reduce_func=reduce_func, + reduce_func_label=reduce_label, + cmap=cmap, + ) + + return HexbinPlotResult( + scenario=scenario, + var_x=var_x, + var_y=var_y, + var_color=var_color, + output_path=output_path.resolve(), + point_count=len(cleaned_df), + ) + + +def generate_hexbin_scenarios( + df: pd.DataFrame, + scenarios: Sequence[HexbinScenario], + *, + base_output_dir: str | Path, + cmap: str = "viridis", +) -> list[HexbinPlotResult]: + """Génère en série plusieurs hexbins prédéfinis et retourne les résultats.""" + + if not scenarios: + raise ValueError("Aucun scénario hexbin fourni.") + + results: list[HexbinPlotResult] = [] + for scenario in scenarios: + result = generate_hexbin_for_scenario( + df=df, + scenario=scenario, + base_output_dir=base_output_dir, + cmap=cmap, + ) + results.append(result) + + return results