diff --git a/figures/calendar/calendar_combined_2025.png b/figures/calendar/calendar_combined_2025.png new file mode 100644 index 0000000..175744f Binary files /dev/null and b/figures/calendar/calendar_combined_2025.png differ diff --git a/figures/calendar/calendar_illuminance_2025.png b/figures/calendar/calendar_illuminance_2025.png new file mode 100644 index 0000000..e1cf921 Binary files /dev/null and b/figures/calendar/calendar_illuminance_2025.png differ diff --git a/figures/calendar/calendar_pressure_2025.png b/figures/calendar/calendar_pressure_2025.png new file mode 100644 index 0000000..5280d91 Binary files /dev/null and b/figures/calendar/calendar_pressure_2025.png differ diff --git a/figures/calendar/calendar_rain_2025.png b/figures/calendar/calendar_rain_2025.png new file mode 100644 index 0000000..faa5dfb Binary files /dev/null and b/figures/calendar/calendar_rain_2025.png differ diff --git a/figures/calendar/calendar_temperature_2025.png b/figures/calendar/calendar_temperature_2025.png new file mode 100644 index 0000000..a86edda Binary files /dev/null and b/figures/calendar/calendar_temperature_2025.png differ diff --git a/figures/calendar/calendar_wind_2025.png b/figures/calendar/calendar_wind_2025.png new file mode 100644 index 0000000..4a48cf4 Binary files /dev/null and b/figures/calendar/calendar_wind_2025.png differ diff --git a/figures/calendar/weekday_profiles.png b/figures/calendar/weekday_profiles.png new file mode 100644 index 0000000..9f800a2 Binary files /dev/null and b/figures/calendar/weekday_profiles.png differ diff --git a/figures/diurnal_cycle/diurnal_cycle.png b/figures/diurnal_cycle/diurnal_cycle.png index 380cf2b..d3e91df 100644 Binary files a/figures/diurnal_cycle/diurnal_cycle.png and b/figures/diurnal_cycle/diurnal_cycle.png differ diff --git a/figures/illuminance/monthly_daylight_hours.png b/figures/illuminance/monthly_daylight_hours.png new file mode 100644 index 0000000..5a316f9 Binary files /dev/null and b/figures/illuminance/monthly_daylight_hours.png differ diff --git a/figures/illuminance/seasonal_diurnal_illuminance.png b/figures/illuminance/seasonal_diurnal_illuminance.png new file mode 100644 index 0000000..2dcbc33 Binary files /dev/null and b/figures/illuminance/seasonal_diurnal_illuminance.png differ diff --git a/figures/monthly/monthly_anomalies.png b/figures/monthly/monthly_anomalies.png new file mode 100644 index 0000000..0e8ff83 Binary files /dev/null and b/figures/monthly/monthly_anomalies.png differ diff --git a/figures/monthly/monthly_boxplots.png b/figures/monthly/monthly_boxplots.png new file mode 100644 index 0000000..5da1435 Binary files /dev/null and b/figures/monthly/monthly_boxplots.png differ diff --git a/figures/seasonal/seasonal_boxplots.png b/figures/seasonal/seasonal_boxplots.png index 94624d8..12670bb 100644 Binary files a/figures/seasonal/seasonal_boxplots.png and b/figures/seasonal/seasonal_boxplots.png differ diff --git a/figures/wind_conditionals/wind_rose_all.png b/figures/wind_conditionals/wind_rose_all.png new file mode 100644 index 0000000..ba3f65d Binary files /dev/null and b/figures/wind_conditionals/wind_rose_all.png differ diff --git a/figures/wind_conditionals/wind_rose_dry.png b/figures/wind_conditionals/wind_rose_dry.png new file mode 100644 index 0000000..864f418 Binary files /dev/null and b/figures/wind_conditionals/wind_rose_dry.png differ diff --git a/figures/wind_conditionals/wind_rose_rain.png b/figures/wind_conditionals/wind_rose_rain.png new file mode 100644 index 0000000..60eab71 Binary files /dev/null and b/figures/wind_conditionals/wind_rose_rain.png differ diff --git a/figures/wind_conditionals/wind_vectors_monthly.png b/figures/wind_conditionals/wind_vectors_monthly.png new file mode 100644 index 0000000..98d64f8 Binary files /dev/null and b/figures/wind_conditionals/wind_vectors_monthly.png differ diff --git a/meteo/analysis.py b/meteo/analysis.py index 01fdc38..f6a555c 100644 --- a/meteo/analysis.py +++ b/meteo/analysis.py @@ -10,6 +10,8 @@ import pandas as pd from .variables import Variable from .season import SEASON_LABELS +MONTH_ORDER = list(range(1, 13)) + def compute_correlation_matrix( df: pd.DataFrame, @@ -599,3 +601,145 @@ def compute_rainfall_by_season( order = [season for season in SEASON_LABELS if season in agg.index] agg = agg.loc[order] return agg + + +def filter_by_condition( + df: pd.DataFrame, + *, + condition: pd.Series, +) -> pd.DataFrame: + """ + Renvoie une copie filtrée du DataFrame selon une condition booleenne alignée. + """ + mask = condition.reindex(df.index) + mask = mask.fillna(False) + return df.loc[mask] + + +def compute_monthly_climatology( + df: pd.DataFrame, + *, + columns: Sequence[str], +) -> pd.DataFrame: + """ + Moyenne par mois (1–12) pour les colonnes fournies. + """ + _ensure_datetime_index(df) + missing = [col for col in columns if col not in df.columns] + if missing: + raise KeyError(f"Colonnes absentes : {missing}") + + grouped = df[list(columns)].groupby(df.index.month).mean() + grouped = grouped.reindex(MONTH_ORDER) + grouped.index.name = "month" + return grouped + + +def compute_monthly_means( + df: pd.DataFrame, + *, + columns: Sequence[str], +) -> pd.DataFrame: + """ + Moyennes calendaire par mois (indexé sur la fin de mois). + """ + _ensure_datetime_index(df) + missing = [col for col in columns if col not in df.columns] + if missing: + raise KeyError(f"Colonnes absentes : {missing}") + + monthly = df[list(columns)].resample("1ME").mean() + return monthly.dropna(how="all") + + +def compute_seasonal_hourly_profile( + df: pd.DataFrame, + *, + value_column: str, + season_column: str = "season", +) -> pd.DataFrame: + """ + Retourne une matrice (heures x saisons) contenant la moyenne d'une variable. + """ + _ensure_datetime_index(df) + for col in (value_column, season_column): + if col not in df.columns: + raise KeyError(f"Colonne absente : {col}") + + subset = df[[value_column, season_column]].dropna() + if subset.empty: + return pd.DataFrame(index=range(24)) + + grouped = subset.groupby([season_column, subset.index.hour])[value_column].mean() + pivot = grouped.unstack(season_column) + pivot = pivot.reindex(index=range(24)) + order = [season for season in SEASON_LABELS if season in pivot.columns] + if order: + pivot = pivot[order] + pivot.index.name = "hour" + return pivot + + +def compute_monthly_daylight_hours( + df: pd.DataFrame, + *, + illuminance_column: str = "illuminance", + threshold_lux: float = 1000.0, +) -> pd.Series: + """ + Calcule la durée moyenne de luminosité (> threshold_lux) par mois (en heures par jour). + """ + _ensure_datetime_index(df) + if illuminance_column not in df.columns: + raise KeyError(f"Colonne absente : {illuminance_column}") + + subset = df[[illuminance_column]].dropna() + if subset.empty: + return pd.Series(dtype=float) + + time_step = _infer_time_step(subset.index) + hours_per_step = time_step.total_seconds() / 3600.0 + + daylight_flag = (subset[illuminance_column] >= threshold_lux).astype(float) + daylight_hours = daylight_flag * hours_per_step + + daily_hours = daylight_hours.resample("1D").sum() + monthly_avg = daily_hours.resample("1ME").mean() + return monthly_avg.dropna() + + +def compute_mean_wind_components( + df: pd.DataFrame, + *, + freq: str = "1M", +) -> pd.DataFrame: + """ + Calcule les composantes zonale (u) et méridienne (v) du vent pour une fréquence donnée. + Retourne également la vitesse moyenne. + """ + if "wind_speed" not in df.columns or "wind_direction" not in df.columns: + raise KeyError("Les colonnes 'wind_speed' et 'wind_direction' sont requises.") + + _ensure_datetime_index(df) + subset = df[["wind_speed", "wind_direction"]].dropna() + if subset.empty: + return pd.DataFrame(columns=["u", "v", "speed"]) + + radians = np.deg2rad(subset["wind_direction"].to_numpy(dtype=float)) + speed = subset["wind_speed"].to_numpy(dtype=float) + + u = speed * np.sin(radians) * -1 # composante est-ouest (positive vers l'est) + v = speed * np.cos(radians) * -1 # composante nord-sud (positive vers le nord) + + vector_df = pd.DataFrame( + { + "u": u, + "v": v, + "speed": speed, + }, + index=subset.index, + ) + + actual_freq = "1ME" if freq == "1M" else freq + grouped = vector_df.resample(actual_freq).mean() + return grouped.dropna(how="all") diff --git a/meteo/plots.py b/meteo/plots.py index 8909304..9c98852 100644 --- a/meteo/plots.py +++ b/meteo/plots.py @@ -1,6 +1,7 @@ # meteo/plots.py from __future__ import annotations +import calendar from pathlib import Path from typing import Callable, Sequence @@ -11,7 +12,7 @@ import matplotlib.dates as mdates import numpy as np import pandas as pd -from .analysis import DiurnalCycleStats, BinnedStatistics +from .analysis import DiurnalCycleStats, BinnedStatistics, MONTH_ORDER from .season import SEASON_LABELS from .variables import Variable @@ -672,6 +673,60 @@ def plot_seasonal_boxplots( return output_path.resolve() +def plot_monthly_boxplots( + df: pd.DataFrame, + variables: Sequence[Variable], + output_path: str | Path, +) -> Path: + """ + Boxplots par mois (janvier → décembre) pour plusieurs variables. + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if not isinstance(df.index, pd.DatetimeIndex): + raise TypeError("plot_monthly_boxplots nécessite un DatetimeIndex.") + + month_labels = [calendar.month_abbr[m].capitalize() for m in MONTH_ORDER] + n_vars = len(variables) + fig, axes = plt.subplots(n_vars, 1, figsize=(12, 3 * n_vars), sharex=True) + if n_vars == 1: + axes = [axes] + + for ax, var in zip(axes, variables): + data = [ + df.loc[df.index.month == month, var.column].dropna().to_numpy() + for month in MONTH_ORDER + ] + + if not any(len(arr) > 0 for arr in data): + ax.text(0.5, 0.5, f"Aucune donnée pour {var.label}.", ha="center", va="center") + ax.set_axis_off() + continue + + box = ax.boxplot( + data, + labels=month_labels, + showfliers=False, + patch_artist=True, + ) + colors = plt.get_cmap("Spectral")(np.linspace(0.2, 0.8, len(data))) + for patch, color in zip(box["boxes"], colors): + patch.set_facecolor(color) + patch.set_alpha(0.7) + + ylabel = f"{var.label} ({var.unit})" if var.unit else var.label + ax.set_ylabel(ylabel) + ax.grid(True, linestyle=":", alpha=0.5) + + axes[-1].set_xlabel("Mois") + fig.suptitle("Distribution mensuelle") + fig.tight_layout(rect=[0, 0, 1, 0.97]) + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + def plot_binned_profiles( stats: BinnedStatistics, variables: Sequence[Variable], @@ -889,3 +944,301 @@ def plot_rainfall_by_season( fig.savefig(output_path, dpi=150) plt.close(fig) return output_path.resolve() + + +def plot_monthly_anomalies( + monthly_means: pd.DataFrame, + climatology: pd.DataFrame, + variables: Sequence[Variable], + output_path: str | Path, + *, + title: str = "Moyennes mensuelles vs climatologie", +) -> Path: + """ + Compare les moyennes mensuelles observées à la climatologie pour plusieurs variables. + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if monthly_means.empty or climatology.empty: + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de données mensuelles disponibles.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + n_vars = len(variables) + fig, axes = plt.subplots(n_vars, 1, figsize=(12, 3 * n_vars), sharex=True) + if n_vars == 1: + axes = [axes] + + locator = mdates.AutoDateLocator() + formatter = mdates.ConciseDateFormatter(locator) + + for ax, var in zip(axes, variables): + actual = monthly_means[var.column].dropna() + if actual.empty: + ax.text(0.5, 0.5, f"Aucune donnée pour {var.label}.", ha="center", va="center") + ax.set_axis_off() + continue + + months = actual.index.month + clim = climatology.loc[months, var.column].to_numpy(dtype=float) + anomaly = actual.to_numpy(dtype=float) - clim + + ax.plot(actual.index, actual, color="tab:blue", label="Moyenne mensuelle") + ax.plot(actual.index, clim, color="tab:gray", linestyle="--", label="Climatologie") + ax.fill_between( + actual.index, + actual, + clim, + where=anomaly >= 0, + color="tab:blue", + alpha=0.15, + ) + ax.fill_between( + actual.index, + actual, + clim, + where=anomaly < 0, + color="tab:red", + alpha=0.15, + ) + + ylabel = f"{var.label} ({var.unit})" if var.unit else var.label + ax.set_ylabel(ylabel) + ax.grid(True, linestyle=":", alpha=0.5) + ax.xaxis.set_major_locator(locator) + ax.xaxis.set_major_formatter(formatter) + + axes[-1].set_xlabel("Date") + axes[0].legend(loc="upper right") + fig.suptitle(title) + fig.tight_layout(rect=[0, 0, 1, 0.97]) + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + +def plot_wind_vector_series( + vector_df: pd.DataFrame, + output_path: str | Path, + *, + title: str = "Vecteurs moyens du vent", +) -> Path: + """ + Représente les composantes moyennes du vent sous forme de flèches (u/v). + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if vector_df.empty: + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de données de vent.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + times = vector_df.index + x = mdates.date2num(times) + u = vector_df["u"].to_numpy(dtype=float) + v = vector_df["v"].to_numpy(dtype=float) + speed = vector_df["speed"].to_numpy(dtype=float) + + fig, ax = plt.subplots(figsize=(12, 4)) + q = ax.quiver( + x, + np.zeros_like(x), + u, + v, + speed, + angles="xy", + scale_units="xy", + scale=1, + cmap="viridis", + ) + ax.axhline(0, color="black", linewidth=0.5) + ax.set_ylim(-max(abs(v)) * 1.2 if np.any(v) else -1, max(abs(v)) * 1.2 if np.any(v) else 1) + ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())) + ax.set_ylabel("Composante nord (v)") + ax.set_xlabel("Date") + ax.set_title(title) + cbar = fig.colorbar(q, ax=ax) + cbar.set_label("Vitesse moyenne (km/h)") + + fig.tight_layout() + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + +def plot_calendar_heatmap( + matrix: pd.DataFrame, + output_path: str | Path, + *, + title: str, + cmap: str = "YlGnBu", + colorbar_label: str = "", +) -> Path: + """ + Affiche une heatmap calendrier (lignes = mois, colonnes = jours). + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if matrix.empty: + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de données pour la heatmap.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + fig, ax = plt.subplots(figsize=(14, 6)) + data = matrix.to_numpy(dtype=float) + im = ax.imshow(data, aspect="auto", cmap=cmap, interpolation="nearest") + + ax.set_xticks(np.arange(matrix.shape[1])) + ax.set_xticklabels(matrix.columns, rotation=90) + ax.set_yticks(np.arange(matrix.shape[0])) + ax.set_yticklabels(matrix.index) + + ax.set_xlabel("Jour du mois") + ax.set_ylabel("Mois") + ax.set_title(title) + + cbar = fig.colorbar(im, ax=ax) + if colorbar_label: + cbar.set_label(colorbar_label) + + fig.tight_layout() + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + +def plot_weekday_profiles( + weekday_df: pd.DataFrame, + variables: Sequence[Variable], + output_path: str | Path, + *, + title: str, +) -> Path: + """ + Affiche les moyennes par jour de semaine pour plusieurs variables. + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if weekday_df.empty: + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de données hebdomadaires.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + weekday_labels = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"] + n_vars = len(variables) + fig, axes = plt.subplots(n_vars, 1, figsize=(10, 3 * n_vars), sharex=True) + if n_vars == 1: + axes = [axes] + + x = np.arange(len(weekday_labels)) + + for ax, var in zip(axes, variables): + if var.column not in weekday_df.columns: + ax.text(0.5, 0.5, f"Aucune donnée pour {var.label}.", ha="center", va="center") + ax.set_axis_off() + continue + + values = weekday_df[var.column].to_numpy(dtype=float) + ax.plot(x, values, marker="o", label=var.label) + ax.set_ylabel(f"{var.label} ({var.unit})" if var.unit else var.label) + ax.grid(True, linestyle=":", alpha=0.5) + ax.set_xticks(x) + ax.set_xticklabels(weekday_labels) + + axes[-1].set_xlabel("Jour de semaine") + axes[0].legend(loc="upper right") + fig.suptitle(title) + fig.tight_layout(rect=[0, 0, 1, 0.97]) + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + +def plot_seasonal_hourly_profiles( + profile_df: pd.DataFrame, + output_path: str | Path, + *, + title: str, + ylabel: str, +) -> Path: + """ + Courbes moyennes par heure pour chaque saison. + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if profile_df.empty or profile_df.isna().all().all(): + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de profil saisonnier disponible.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + hours = profile_df.index.to_numpy(dtype=float) + fig, ax = plt.subplots(figsize=(10, 4)) + colors = plt.get_cmap("turbo")(np.linspace(0.1, 0.9, profile_df.shape[1])) + for color, season in zip(colors, profile_df.columns): + ax.plot(hours, profile_df[season], label=season.capitalize(), color=color) + + ax.set_xlabel("Heure locale") + ax.set_ylabel(ylabel) + ax.set_title(title) + ax.grid(True, linestyle=":", alpha=0.5) + ax.legend() + fig.tight_layout() + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() + + +def plot_daylight_hours( + monthly_series: pd.Series, + output_path: str | Path, + *, + title: str = "Durée moyenne de luminosité (> seuil)", +) -> Path: + """ + Représente la durée moyenne quotidienne de luminosité par mois. + """ + output_path = Path(output_path) + output_path.parent.mkdir(parents=True, exist_ok=True) + + if monthly_series.empty: + fig, ax = plt.subplots() + ax.text(0.5, 0.5, "Pas de données sur la luminosité.", ha="center", va="center") + ax.set_axis_off() + fig.savefig(output_path, dpi=150, bbox_inches="tight") + plt.close(fig) + return output_path.resolve() + + months = monthly_series.index + fig, ax = plt.subplots(figsize=(10, 4)) + ax.bar(months, monthly_series.values, color="goldenrod", alpha=0.8) + ax.set_ylabel("Heures de luminosité par jour") + ax.set_xlabel("Mois") + ax.xaxis.set_major_locator(mdates.AutoDateLocator()) + ax.xaxis.set_major_formatter(mdates.ConciseDateFormatter(ax.xaxis.get_major_locator())) + ax.set_title(title) + ax.grid(True, axis="y", linestyle=":", alpha=0.5) + fig.tight_layout() + fig.savefig(output_path, dpi=150) + plt.close(fig) + return output_path.resolve() diff --git a/scripts/plot_calendar_overview.py b/scripts/plot_calendar_overview.py new file mode 100644 index 0000000..5e7ea70 --- /dev/null +++ b/scripts/plot_calendar_overview.py @@ -0,0 +1,213 @@ +# scripts/plot_calendar_overview.py +from __future__ import annotations + +from pathlib import Path + +import calendar +import numpy as np +import pandas as pd +import matplotlib.pyplot as plt + +from meteo.dataset import load_raw_csv +from meteo.analysis import compute_daily_rainfall_totals +from meteo.plots import plot_calendar_heatmap, plot_weekday_profiles +from meteo.variables import VARIABLES_BY_KEY + + +CSV_PATH = Path("data/weather_minutely.csv") +OUTPUT_DIR = Path("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() + + +def plot_combined_calendar( + matrices: dict[str, pd.DataFrame], + output_path: Path, + *, + title: str, +) -> None: + if not matrices: + return + + n = len(matrices) + fig, axes = plt.subplots(n, 1, figsize=(14, 4 * n), sharex=True) + if n == 1: + axes = [axes] + + for ax, (label, matrix) in zip(axes, matrices.items()): + data = matrix.to_numpy(dtype=float) + im = ax.imshow(data, aspect="auto", interpolation="nearest", cmap=matrix.attrs.get("cmap", "viridis")) + ax.set_xticks(np.arange(matrix.shape[1])) + ax.set_xticklabels(matrix.columns, rotation=90) + ax.set_yticks(np.arange(matrix.shape[0])) + ax.set_yticklabels(matrix.index) + ax.set_ylabel(label) + cbar = fig.colorbar(im, ax=ax) + if matrix.attrs.get("colorbar_label"): + cbar.set_label(matrix.attrs["colorbar_label"]) + + axes[-1].set_xlabel("Jour du mois") + fig.suptitle(title) + fig.tight_layout(rect=[0, 0, 1, 0.97]) + fig.savefig(output_path, dpi=150) + plt.close(fig) + + +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}") + + 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", + ) + 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}") + + matrices_for_combined = { + "Pluie (mm)": rain_matrix, + "Température (°C)": temp_matrix, + } + + 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}") + matrices_for_combined["Pression (hPa)"] = pressure_matrix + + 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}") + matrices_for_combined["Illuminance (lux)"] = lux_matrix + + 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}") + matrices_for_combined["Vent (km/h)"] = wind_matrix + + combined_path = OUTPUT_DIR / f"calendar_combined_{latest_year}.png" + plot_combined_calendar( + matrices=matrices_for_combined, + output_path=combined_path, + title=f"Calendrier combiné {latest_year}", + ) + print(f"✔ Calendrier combiné : {combined_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}") + + print("✔ Graphiques calendrier générés.") + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_diurnal_cycle.py b/scripts/plot_diurnal_cycle.py index be325f8..b1e34d6 100644 --- a/scripts/plot_diurnal_cycle.py +++ b/scripts/plot_diurnal_cycle.py @@ -12,7 +12,7 @@ from meteo.plots import plot_diurnal_cycle CSV_PATH = Path("data/weather_minutely.csv") OUTPUT_PATH = Path("figures/diurnal_cycle/diurnal_cycle.png") -VARIABLE_KEYS = ["temperature", "humidity", "pressure", "wind_speed"] +VARIABLE_KEYS = ["temperature", "humidity", "pressure", "wind_speed", "illuminance"] def main() -> None: diff --git a/scripts/plot_illuminance_focus.py b/scripts/plot_illuminance_focus.py new file mode 100644 index 0000000..4ae4a5b --- /dev/null +++ b/scripts/plot_illuminance_focus.py @@ -0,0 +1,64 @@ +# scripts/plot_illuminance_focus.py +from __future__ import annotations + +from pathlib import Path + +from meteo.dataset import load_raw_csv +from meteo.analysis import compute_seasonal_hourly_profile, compute_monthly_daylight_hours +from meteo.plots import plot_seasonal_hourly_profiles, plot_daylight_hours + + +CSV_PATH = Path("data/weather_minutely.csv") +OUTPUT_DIR = Path("figures/illuminance") +DAYLIGHT_THRESHOLD_LUX = 1000.0 + + +def main() -> None: + if not CSV_PATH.exists(): + print(f"⚠ Fichier introuvable : {CSV_PATH}") + return + + df = load_raw_csv(CSV_PATH) + if "illuminance" not in df.columns: + print("⚠ La colonne 'illuminance' est absente du dataset.") + 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) + + seasonal_profile = compute_seasonal_hourly_profile( + df=df, + value_column="illuminance", + season_column="season", + ) + seasonal_path = OUTPUT_DIR / "seasonal_diurnal_illuminance.png" + plot_seasonal_hourly_profiles( + profile_df=seasonal_profile, + output_path=seasonal_path, + title="Illuminance moyenne par heure et par saison", + ylabel="Illuminance (lux)", + ) + print(f"✔ Profil saisonnier de l'illuminance : {seasonal_path}") + + daylight_hours = compute_monthly_daylight_hours( + df=df, + illuminance_column="illuminance", + threshold_lux=DAYLIGHT_THRESHOLD_LUX, + ) + daylight_path = OUTPUT_DIR / "monthly_daylight_hours.png" + plot_daylight_hours( + monthly_series=daylight_hours, + output_path=daylight_path, + title=f"Durée moyenne quotidienne > {DAYLIGHT_THRESHOLD_LUX:.0f} lx", + ) + print(f"✔ Durée de luminosité mensuelle : {daylight_path}") + + print("✔ Graphiques dédiés à l'illuminance générés.") + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_monthly_patterns.py b/scripts/plot_monthly_patterns.py new file mode 100644 index 0000000..fdb83bb --- /dev/null +++ b/scripts/plot_monthly_patterns.py @@ -0,0 +1,54 @@ +# scripts/plot_monthly_patterns.py +from __future__ import annotations + +from pathlib import Path + +from meteo.dataset import load_raw_csv +from meteo.variables import VARIABLES_BY_KEY +from meteo.analysis import compute_monthly_climatology, compute_monthly_means +from meteo.plots import plot_monthly_boxplots, plot_monthly_anomalies + + +CSV_PATH = Path("data/weather_minutely.csv") +OUTPUT_DIR = Path("figures/monthly") + +BOXPLOT_KEYS = ["temperature", "humidity", "pressure", "wind_speed", "illuminance"] +ANOMALY_KEYS = ["temperature", "humidity", "illuminance"] + + +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() + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + box_vars = [VARIABLES_BY_KEY[key] for key in BOXPLOT_KEYS] + boxplot_path = OUTPUT_DIR / "monthly_boxplots.png" + plot_monthly_boxplots(df=df, variables=box_vars, output_path=boxplot_path) + print(f"✔ Boxplots mensuels : {boxplot_path}") + + anomaly_vars = [VARIABLES_BY_KEY[key] for key in ANOMALY_KEYS] + monthly_means = compute_monthly_means(df=df, columns=[v.column for v in anomaly_vars]) + climatology = compute_monthly_climatology(df=df, columns=[v.column for v in anomaly_vars]) + + anomaly_path = OUTPUT_DIR / "monthly_anomalies.png" + plot_monthly_anomalies( + monthly_means=monthly_means, + climatology=climatology, + variables=anomaly_vars, + output_path=anomaly_path, + ) + print(f"✔ Anomalies mensuelles : {anomaly_path}") + + print("✔ Graphiques mensuels générés.") + + +if __name__ == "__main__": + main() diff --git a/scripts/plot_seasonal_overview.py b/scripts/plot_seasonal_overview.py index fe466a7..54d4160 100644 --- a/scripts/plot_seasonal_overview.py +++ b/scripts/plot_seasonal_overview.py @@ -13,7 +13,7 @@ from meteo.season import sort_season_labels, SEASON_LABELS CSV_PATH = Path("data/weather_minutely.csv") OUTPUT_DIR = Path("figures/seasonal") -BOXPLOT_VARIABLES = ["temperature", "humidity", "pressure", "wind_speed"] +BOXPLOT_VARIABLES = ["temperature", "humidity", "pressure", "wind_speed", "illuminance"] def infer_season_order(df) -> list[str]: diff --git a/scripts/plot_wind_conditionals.py b/scripts/plot_wind_conditionals.py new file mode 100644 index 0000000..61ec118 --- /dev/null +++ b/scripts/plot_wind_conditionals.py @@ -0,0 +1,86 @@ +# scripts/plot_wind_conditionals.py +from __future__ import annotations + +from pathlib import Path + +from meteo.dataset import load_raw_csv +from meteo.analysis import ( + compute_wind_rose_distribution, + filter_by_condition, + compute_mean_wind_components, +) +from meteo.plots import plot_wind_rose, plot_wind_vector_series + + +CSV_PATH = Path("data/weather_minutely.csv") +OUTPUT_DIR = Path("figures/wind_conditionals") +RAIN_THRESHOLD = 0.2 # mm/h + + +def _export_wind_rose(df, label: str, filename: str) -> None: + if df.empty: + print(f"⚠ Pas de données pour {label}.") + return + + frequencies, speed_labels, sector_size = compute_wind_rose_distribution( + df=df, + direction_sector_size=30, + speed_bins=(0, 5, 15, 30, 50, float("inf")), + ) + if frequencies.empty: + print(f"⚠ Impossible de construire la rose pour {label}.") + return + + output_path = OUTPUT_DIR / filename + plot_wind_rose( + frequencies=frequencies, + speed_bin_labels=speed_labels, + output_path=output_path, + sector_size_deg=sector_size, + cmap="plasma", + ) + print(f"✔ Rose des vents ({label}) : {output_path}") + + +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 + + print(f"Dataset minuté chargé : {CSV_PATH}") + print(f" Lignes : {len(df)}") + print(f" Colonnes : {list(df.columns)}") + print() + + if "rain_rate" not in df.columns: + print("⚠ Colonne 'rain_rate' absente.") + return + + OUTPUT_DIR.mkdir(parents=True, exist_ok=True) + + rain_condition = df["rain_rate"].fillna(0.0) >= RAIN_THRESHOLD + dry_condition = df["rain_rate"].fillna(0.0) < RAIN_THRESHOLD + + _export_wind_rose(df, "toutes conditions", "wind_rose_all.png") + _export_wind_rose(filter_by_condition(df, condition=rain_condition), "pluie", "wind_rose_rain.png") + _export_wind_rose(filter_by_condition(df, condition=dry_condition), "temps sec", "wind_rose_dry.png") + + # Vecteurs moyens par mois + vector_df = compute_mean_wind_components(df=df, freq="1M") + vector_path = OUTPUT_DIR / "wind_vectors_monthly.png" + plot_wind_vector_series( + vector_df=vector_df, + output_path=vector_path, + title="Vecteurs moyens du vent (mensuel)", + ) + print(f"✔ Vecteurs de vent mensuels : {vector_path}") + print("✔ Graphiques vent/pluie conditionnels générés.") + + +if __name__ == "__main__": + main()