diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_humidity_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_humidity_2025.png new file mode 100644 index 0000000..b6ead54 Binary files /dev/null and b/docs/03 - Premiers graphiques/figures/calendar/calendar_humidity_2025.png differ diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_illuminance_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_illuminance_2025.png index c0e164c..6a7f688 100644 Binary files a/docs/03 - Premiers graphiques/figures/calendar/calendar_illuminance_2025.png and b/docs/03 - Premiers graphiques/figures/calendar/calendar_illuminance_2025.png differ diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_pressure_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_pressure_2025.png index ee427ef..3782eb3 100644 Binary files a/docs/03 - Premiers graphiques/figures/calendar/calendar_pressure_2025.png and b/docs/03 - Premiers graphiques/figures/calendar/calendar_pressure_2025.png differ diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_rain_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_rain_2025.png index 4652699..e9358bb 100644 Binary files a/docs/03 - Premiers graphiques/figures/calendar/calendar_rain_2025.png and b/docs/03 - Premiers graphiques/figures/calendar/calendar_rain_2025.png differ diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_temperature_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_temperature_2025.png index 44eb2ca..0483259 100644 Binary files a/docs/03 - Premiers graphiques/figures/calendar/calendar_temperature_2025.png and b/docs/03 - Premiers graphiques/figures/calendar/calendar_temperature_2025.png differ diff --git a/docs/03 - Premiers graphiques/figures/calendar/calendar_wind_2025.png b/docs/03 - Premiers graphiques/figures/calendar/calendar_wind_2025.png index 2639236..e69ad1d 100644 Binary files a/docs/03 - Premiers graphiques/figures/calendar/calendar_wind_2025.png and b/docs/03 - Premiers graphiques/figures/calendar/calendar_wind_2025.png differ diff --git a/docs/03 - Premiers graphiques/index.md b/docs/03 - Premiers graphiques/index.md index dddfc8e..b560ec9 100644 --- a/docs/03 - Premiers graphiques/index.md +++ b/docs/03 - Premiers graphiques/index.md @@ -12,10 +12,10 @@ python "docs/03 - Premiers graphiques/scripts/plot_basic_variables.py" ![](figures/temperature_last_7_days.png) -![](figures/humidity_last_7_days.png) - ![](figures/pressure_last_7_days.png) +![](figures/humidity_last_7_days.png) + ![](figures/rain_rate_last_7_days.png) ![](figures/illuminance_last_7_days.png) @@ -35,12 +35,14 @@ Les images générées sont stockées dans `figures/calendar/` et les CSV corres python "docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py" ``` -![](figures/calendar/calendar_rain_2025.png) - ![](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) diff --git a/docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py b/docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py index 79f2efd..6dc4376 100644 --- a/docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py +++ b/docs/03 - Premiers graphiques/scripts/plot_calendar_overview.py @@ -4,8 +4,6 @@ from __future__ import annotations from pathlib import Path import sys -import calendar -import numpy as np import pandas as pd PROJECT_ROOT = Path(__file__).resolve().parents[3] @@ -13,43 +11,68 @@ if str(PROJECT_ROOT) not in sys.path: sys.path.insert(0, str(PROJECT_ROOT)) from meteo.dataset import load_raw_csv -from meteo.analysis import compute_daily_rainfall_totals -from meteo.plots import export_plot_dataset, plot_calendar_heatmap, plot_weekday_profiles -from meteo.variables import VARIABLES_BY_KEY +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" -WEEKDAY_VARIABLE_KEYS = ["temperature", "humidity", "wind_speed", "illuminance"] - - -def _format_calendar_matrix(series: pd.Series, year: int, agg_label: str) -> pd.DataFrame: - """ - Transforme une série quotidienne en matrice mois x jours (1-31). - """ - start = pd.Timestamp(year=year, month=1, day=1, tz=series.index.tz) - end = pd.Timestamp(year=year, month=12, day=31, tz=series.index.tz) - filtered = series.loc[(series.index >= start) & (series.index <= end)] - - matrix = pd.DataFrame( - np.nan, - index=[calendar.month_name[m][:3] for m in range(1, 13)], - columns=list(range(1, 32)), - ) - - for timestamp, value in filtered.items(): - month = timestamp.month - day = timestamp.day - matrix.at[calendar.month_name[month][:3], day] = value - - matrix.index.name = f"{agg_label} ({year})" - return matrix - - -def compute_daily_mean(df: pd.DataFrame, column: str) -> pd.Series: - return df[column].resample("1D").mean() +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: @@ -76,91 +99,20 @@ def main() -> None: latest_year = df.index.year.max() print(f"Année retenue pour le calendrier : {latest_year}") - daily_totals = compute_daily_rainfall_totals(df=df) - daily_rain = daily_totals["daily_total"] - rain_matrix = _format_calendar_matrix(daily_rain, latest_year, "Pluie (mm)") - rain_matrix.attrs["cmap"] = "Blues" - rain_matrix.attrs["colorbar_label"] = "mm" - rain_path = OUTPUT_DIR / f"calendar_rain_{latest_year}.png" - plot_calendar_heatmap( - matrix=rain_matrix, - output_path=rain_path, - title=f"Pluie quotidienne - {latest_year}", - cmap="Blues", - colorbar_label="mm", + results = generate_calendar_heatmaps( + df=df, + specs=HEATMAP_SPECS, + year=latest_year, + output_dir=OUTPUT_DIR, ) - print(f"✔ Heatmap pluie {latest_year} : {rain_path}") - daily_temp = compute_daily_mean(df, "temperature") - temp_matrix = _format_calendar_matrix(daily_temp, latest_year, "Température (°C)") - temp_matrix.attrs["cmap"] = "coolwarm" - temp_matrix.attrs["colorbar_label"] = "°C" - temp_path = OUTPUT_DIR / f"calendar_temperature_{latest_year}.png" - plot_calendar_heatmap( - matrix=temp_matrix, - output_path=temp_path, - title=f"Température moyenne quotidienne - {latest_year}", - cmap="coolwarm", - colorbar_label="°C", - ) - print(f"✔ Heatmap température {latest_year} : {temp_path}") - - if "pressure" in df.columns: - daily_pressure = compute_daily_mean(df, "pressure") - pressure_matrix = _format_calendar_matrix(daily_pressure, latest_year, "Pression (hPa)") - pressure_matrix.attrs["cmap"] = "Greens" - pressure_matrix.attrs["colorbar_label"] = "hPa" - pressure_path = OUTPUT_DIR / f"calendar_pressure_{latest_year}.png" - plot_calendar_heatmap( - matrix=pressure_matrix, - output_path=pressure_path, - title=f"Pression moyenne quotidienne - {latest_year}", - cmap="Greens", - colorbar_label="hPa", - ) - print(f"✔ Heatmap pression {latest_year} : {pressure_path}") - - if "illuminance" in df.columns: - daily_lux = compute_daily_mean(df, "illuminance") - lux_matrix = _format_calendar_matrix(daily_lux, latest_year, "Illuminance (lux)") - lux_matrix.attrs["cmap"] = "YlOrBr" - lux_matrix.attrs["colorbar_label"] = "lux" - lux_path = OUTPUT_DIR / f"calendar_illuminance_{latest_year}.png" - plot_calendar_heatmap( - matrix=lux_matrix, - output_path=lux_path, - title=f"Illuminance moyenne quotidienne - {latest_year}", - cmap="YlOrBr", - colorbar_label="lux", - ) - print(f"✔ Heatmap illuminance {latest_year} : {lux_path}") - - if "wind_speed" in df.columns: - daily_wind = compute_daily_mean(df, "wind_speed") - wind_matrix = _format_calendar_matrix(daily_wind, latest_year, "Vent (km/h)") - wind_matrix.attrs["cmap"] = "Purples" - wind_matrix.attrs["colorbar_label"] = "km/h" - wind_path = OUTPUT_DIR / f"calendar_wind_{latest_year}.png" - plot_calendar_heatmap( - matrix=wind_matrix, - output_path=wind_path, - title=f"Vitesse moyenne du vent - {latest_year}", - cmap="Purples", - colorbar_label="km/h", - ) - print(f"✔ Heatmap vent {latest_year} : {wind_path}") - - hourly = df[WEEKDAY_VARIABLE_KEYS].resample("1h").mean() - weekday_stats = hourly.groupby(hourly.index.dayofweek).mean() - weekday_path = OUTPUT_DIR / "weekday_profiles.png" - variables = [VARIABLES_BY_KEY[key] for key in WEEKDAY_VARIABLE_KEYS] - plot_weekday_profiles( - weekday_df=weekday_stats, - variables=variables, - output_path=weekday_path, - title="Profils moyens par jour de semaine", - ) - print(f"✔ Profils hebdomadaires : {weekday_path}") + 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.") diff --git a/meteo/plots/__init__.py b/meteo/plots/__init__.py index 7644bbf..07e2f64 100644 --- a/meteo/plots/__init__.py +++ b/meteo/plots/__init__.py @@ -2,6 +2,14 @@ from __future__ import annotations from .base import export_plot_dataset from .calendar import plot_calendar_heatmap, plot_weekday_profiles +from .calendar_overview import ( + CalendarHeatmapResult, + CalendarHeatmapSpec, + daily_mean_series, + format_calendar_matrix, + generate_calendar_heatmaps, + rainfall_daily_total_series, +) from .correlations import ( plot_correlation_heatmap, plot_lagged_correlation, @@ -31,6 +39,12 @@ __all__ = [ "export_plot_dataset", "plot_calendar_heatmap", "plot_weekday_profiles", + "CalendarHeatmapResult", + "CalendarHeatmapSpec", + "daily_mean_series", + "format_calendar_matrix", + "generate_calendar_heatmaps", + "rainfall_daily_total_series", "plot_correlation_heatmap", "plot_lagged_correlation", "plot_rolling_correlation_heatmap", diff --git a/meteo/plots/calendar_overview.py b/meteo/plots/calendar_overview.py new file mode 100644 index 0000000..f0b8c7b --- /dev/null +++ b/meteo/plots/calendar_overview.py @@ -0,0 +1,152 @@ +"""Utilitaires pour générer des heatmaps calendrier configurables.""" + +from __future__ import annotations + +import calendar +from dataclasses import dataclass +from pathlib import Path +from typing import Callable, Sequence + +import numpy as np +import pandas as pd + +from meteo.analysis import compute_daily_rainfall_totals + +from .calendar import plot_calendar_heatmap + +CalendarAggregator = Callable[[pd.DataFrame], pd.Series] + +__all__ = [ + "CalendarHeatmapSpec", + "CalendarHeatmapResult", + "daily_mean_series", + "rainfall_daily_total_series", + "format_calendar_matrix", + "generate_calendar_heatmaps", +] + + +@dataclass(frozen=True) +class CalendarHeatmapSpec: + """Description d'une heatmap calendrier à générer.""" + + key: str + agg_label: str + title_template: str + cmap: str + colorbar_label: str + aggregator: CalendarAggregator + + +@dataclass(frozen=True) +class CalendarHeatmapResult: + """Résultat d'une tentative de génération de heatmap calendrier.""" + + spec: CalendarHeatmapSpec + output_path: Path | None + reason: str | None = None + + +def daily_mean_series(column: str) -> CalendarAggregator: + """Retourne un agrégateur qui calcule la moyenne quotidienne d'une colonne.""" + + def _aggregator(df: pd.DataFrame) -> pd.Series: + if column not in df.columns: + raise KeyError(column) + if not isinstance(df.index, pd.DatetimeIndex): + raise TypeError("Le DataFrame doit être indexé par des timestamps pour les moyennes quotidiennes.") + return df[column].resample("1D").mean() + + return _aggregator + + +def rainfall_daily_total_series(df: pd.DataFrame) -> pd.Series: + """Calcule les cumuls quotidiens de précipitations.""" + + totals = compute_daily_rainfall_totals(df=df) + return totals["daily_total"] + + +def format_calendar_matrix(series: pd.Series, year: int, agg_label: str) -> pd.DataFrame: + """Transforme une série quotidienne en matrice calendrier mois x jours.""" + + if not isinstance(series.index, pd.DatetimeIndex): + raise TypeError("La série doit avoir un index temporel pour être convertie en calendrier.") + + tz = series.index.tz + start = pd.Timestamp(year=year, month=1, day=1, tz=tz) + end = pd.Timestamp(year=year, month=12, day=31, tz=tz) + filtered = series.loc[(series.index >= start) & (series.index <= end)] + + matrix = pd.DataFrame( + np.nan, + index=[calendar.month_name[m][:3] for m in range(1, 13)], + columns=list(range(1, 32)), + ) + + for timestamp, value in filtered.items(): + matrix.at[calendar.month_name[timestamp.month][:3], timestamp.day] = value + + matrix.index.name = f"{agg_label} ({year})" + return matrix + + +def generate_calendar_heatmaps( + df: pd.DataFrame, + specs: Sequence[CalendarHeatmapSpec], + *, + year: int, + output_dir: str | Path, +) -> list[CalendarHeatmapResult]: + """Génère l'ensemble des heatmaps calendrier décrites dans `specs`.""" + + output_dir = Path(output_dir) + output_dir.mkdir(parents=True, exist_ok=True) + + results: list[CalendarHeatmapResult] = [] + for spec in specs: + try: + daily_series = spec.aggregator(df) + except Exception as exc: + results.append( + CalendarHeatmapResult( + spec=spec, + output_path=None, + reason=str(exc), + ) + ) + continue + + if daily_series is None or daily_series.empty: + results.append( + CalendarHeatmapResult( + spec=spec, + output_path=None, + reason="Série vide ou invalide.", + ) + ) + continue + + try: + matrix = format_calendar_matrix(daily_series, year, spec.agg_label) + except Exception as exc: # pragma: no cover - remonté au résultat + results.append( + CalendarHeatmapResult( + spec=spec, + output_path=None, + reason=str(exc), + ) + ) + continue + + output_path = output_dir / f"calendar_{spec.key}_{year}.png" + plot_calendar_heatmap( + matrix=matrix, + output_path=output_path, + title=spec.title_template.format(year=year, label=spec.agg_label), + cmap=spec.cmap, + colorbar_label=spec.colorbar_label, + ) + results.append(CalendarHeatmapResult(spec=spec, output_path=output_path.resolve())) + + return results