Visualisations étendues (illuminance, calendriers, vent)
BIN
figures/calendar/calendar_combined_2025.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
figures/calendar/calendar_illuminance_2025.png
Normal file
|
After Width: | Height: | Size: 56 KiB |
BIN
figures/calendar/calendar_pressure_2025.png
Normal file
|
After Width: | Height: | Size: 49 KiB |
BIN
figures/calendar/calendar_rain_2025.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
figures/calendar/calendar_temperature_2025.png
Normal file
|
After Width: | Height: | Size: 46 KiB |
BIN
figures/calendar/calendar_wind_2025.png
Normal file
|
After Width: | Height: | Size: 45 KiB |
BIN
figures/calendar/weekday_profiles.png
Normal file
|
After Width: | Height: | Size: 189 KiB |
|
Before Width: | Height: | Size: 269 KiB After Width: | Height: | Size: 342 KiB |
BIN
figures/illuminance/monthly_daylight_hours.png
Normal file
|
After Width: | Height: | Size: 32 KiB |
BIN
figures/illuminance/seasonal_diurnal_illuminance.png
Normal file
|
After Width: | Height: | Size: 88 KiB |
BIN
figures/monthly/monthly_anomalies.png
Normal file
|
After Width: | Height: | Size: 167 KiB |
BIN
figures/monthly/monthly_boxplots.png
Normal file
|
After Width: | Height: | Size: 177 KiB |
|
Before Width: | Height: | Size: 91 KiB After Width: | Height: | Size: 114 KiB |
BIN
figures/wind_conditionals/wind_rose_all.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
figures/wind_conditionals/wind_rose_dry.png
Normal file
|
After Width: | Height: | Size: 143 KiB |
BIN
figures/wind_conditionals/wind_rose_rain.png
Normal file
|
After Width: | Height: | Size: 112 KiB |
BIN
figures/wind_conditionals/wind_vectors_monthly.png
Normal file
|
After Width: | Height: | Size: 54 KiB |
@ -10,6 +10,8 @@ import pandas as pd
|
|||||||
from .variables import Variable
|
from .variables import Variable
|
||||||
from .season import SEASON_LABELS
|
from .season import SEASON_LABELS
|
||||||
|
|
||||||
|
MONTH_ORDER = list(range(1, 13))
|
||||||
|
|
||||||
|
|
||||||
def compute_correlation_matrix(
|
def compute_correlation_matrix(
|
||||||
df: pd.DataFrame,
|
df: pd.DataFrame,
|
||||||
@ -599,3 +601,145 @@ def compute_rainfall_by_season(
|
|||||||
order = [season for season in SEASON_LABELS if season in agg.index]
|
order = [season for season in SEASON_LABELS if season in agg.index]
|
||||||
agg = agg.loc[order]
|
agg = agg.loc[order]
|
||||||
return agg
|
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")
|
||||||
|
|||||||
355
meteo/plots.py
@ -1,6 +1,7 @@
|
|||||||
# meteo/plots.py
|
# meteo/plots.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import calendar
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from typing import Callable, Sequence
|
from typing import Callable, Sequence
|
||||||
|
|
||||||
@ -11,7 +12,7 @@ import matplotlib.dates as mdates
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from .analysis import DiurnalCycleStats, BinnedStatistics
|
from .analysis import DiurnalCycleStats, BinnedStatistics, MONTH_ORDER
|
||||||
from .season import SEASON_LABELS
|
from .season import SEASON_LABELS
|
||||||
from .variables import Variable
|
from .variables import Variable
|
||||||
|
|
||||||
@ -672,6 +673,60 @@ def plot_seasonal_boxplots(
|
|||||||
return output_path.resolve()
|
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(
|
def plot_binned_profiles(
|
||||||
stats: BinnedStatistics,
|
stats: BinnedStatistics,
|
||||||
variables: Sequence[Variable],
|
variables: Sequence[Variable],
|
||||||
@ -889,3 +944,301 @@ def plot_rainfall_by_season(
|
|||||||
fig.savefig(output_path, dpi=150)
|
fig.savefig(output_path, dpi=150)
|
||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
return output_path.resolve()
|
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()
|
||||||
|
|||||||
213
scripts/plot_calendar_overview.py
Normal file
@ -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()
|
||||||
@ -12,7 +12,7 @@ from meteo.plots import plot_diurnal_cycle
|
|||||||
CSV_PATH = Path("data/weather_minutely.csv")
|
CSV_PATH = Path("data/weather_minutely.csv")
|
||||||
OUTPUT_PATH = Path("figures/diurnal_cycle/diurnal_cycle.png")
|
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:
|
def main() -> None:
|
||||||
|
|||||||
64
scripts/plot_illuminance_focus.py
Normal file
@ -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()
|
||||||
54
scripts/plot_monthly_patterns.py
Normal file
@ -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()
|
||||||
@ -13,7 +13,7 @@ from meteo.season import sort_season_labels, SEASON_LABELS
|
|||||||
CSV_PATH = Path("data/weather_minutely.csv")
|
CSV_PATH = Path("data/weather_minutely.csv")
|
||||||
OUTPUT_DIR = Path("figures/seasonal")
|
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]:
|
def infer_season_order(df) -> list[str]:
|
||||||
|
|||||||
86
scripts/plot_wind_conditionals.py
Normal file
@ -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()
|
||||||