Ajout de visualisations classiques
This commit is contained in:
parent
fd42a692d9
commit
268d5d65ec
BIN
figures/diurnal_cycle/diurnal_cycle.png
Normal file
BIN
figures/diurnal_cycle/diurnal_cycle.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
BIN
figures/rainfall_hyetograph/daily_rainfall_hyetograph.png
Normal file
BIN
figures/rainfall_hyetograph/daily_rainfall_hyetograph.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 58 KiB |
BIN
figures/wind_rose/wind_rose.png
Normal file
BIN
figures/wind_rose/wind_rose.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 143 KiB |
BIN
figures/wind_rose/wind_rose_during_rain.png
Normal file
BIN
figures/wind_rose/wind_rose_during_rain.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 112 KiB |
@ -1,6 +1,7 @@
|
|||||||
# meteo/analysis.py
|
# meteo/analysis.py
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from dataclasses import dataclass
|
||||||
from typing import Literal, Sequence
|
from typing import Literal, Sequence
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
@ -123,6 +124,16 @@ def _ensure_datetime_index(df: pd.DataFrame) -> pd.DatetimeIndex:
|
|||||||
return df.index
|
return df.index
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass
|
||||||
|
class DiurnalCycleStats:
|
||||||
|
mean: pd.DataFrame
|
||||||
|
median: pd.DataFrame
|
||||||
|
quantile_low: pd.DataFrame | None
|
||||||
|
quantile_high: pd.DataFrame | None
|
||||||
|
quantile_low_level: float | None = None
|
||||||
|
quantile_high_level: float | None = None
|
||||||
|
|
||||||
|
|
||||||
def compute_rolling_correlation_series(
|
def compute_rolling_correlation_series(
|
||||||
df: pd.DataFrame,
|
df: pd.DataFrame,
|
||||||
var_x: Variable,
|
var_x: Variable,
|
||||||
@ -311,3 +322,137 @@ def build_event_aligned_segments(
|
|||||||
|
|
||||||
aligned = pd.concat(segments)
|
aligned = pd.concat(segments)
|
||||||
return aligned
|
return aligned
|
||||||
|
|
||||||
|
|
||||||
|
def compute_diurnal_cycle_statistics(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
variables: Sequence[Variable],
|
||||||
|
*,
|
||||||
|
quantiles: tuple[float, float] | None = (0.25, 0.75),
|
||||||
|
) -> DiurnalCycleStats:
|
||||||
|
"""
|
||||||
|
Agrège les variables par heure locale pour visualiser un cycle diurne moyen.
|
||||||
|
"""
|
||||||
|
_ensure_datetime_index(df)
|
||||||
|
columns = [v.column for v in variables]
|
||||||
|
|
||||||
|
grouped = df[columns].groupby(df.index.hour)
|
||||||
|
mean_df = grouped.mean()
|
||||||
|
median_df = grouped.median()
|
||||||
|
|
||||||
|
quantile_low_df: pd.DataFrame | None = None
|
||||||
|
quantile_high_df: pd.DataFrame | None = None
|
||||||
|
q_low = q_high = None
|
||||||
|
|
||||||
|
if quantiles is not None:
|
||||||
|
q_low, q_high = quantiles
|
||||||
|
if q_low is not None:
|
||||||
|
quantile_low_df = grouped.quantile(q_low)
|
||||||
|
if q_high is not None:
|
||||||
|
quantile_high_df = grouped.quantile(q_high)
|
||||||
|
|
||||||
|
return DiurnalCycleStats(
|
||||||
|
mean=mean_df,
|
||||||
|
median=median_df,
|
||||||
|
quantile_low=quantile_low_df,
|
||||||
|
quantile_high=quantile_high_df,
|
||||||
|
quantile_low_level=q_low,
|
||||||
|
quantile_high_level=q_high,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _format_speed_bin_labels(speed_bins: Sequence[float]) -> list[str]:
|
||||||
|
labels: list[str] = []
|
||||||
|
for i in range(len(speed_bins) - 1):
|
||||||
|
low = speed_bins[i]
|
||||||
|
high = speed_bins[i + 1]
|
||||||
|
if np.isinf(high):
|
||||||
|
labels.append(f"≥{low:g}")
|
||||||
|
else:
|
||||||
|
labels.append(f"{low:g}–{high:g}")
|
||||||
|
return labels
|
||||||
|
|
||||||
|
|
||||||
|
def compute_wind_rose_distribution(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
direction_sector_size: int = 30,
|
||||||
|
speed_bins: Sequence[float] = (0, 10, 20, 30, 50, float("inf")),
|
||||||
|
) -> tuple[pd.DataFrame, list[str], float]:
|
||||||
|
"""
|
||||||
|
Regroupe la distribution vent/direction en secteurs angulaires et classes de vitesse.
|
||||||
|
Retourne un DataFrame indexé par le début du secteur (en degrés) et colonnes = classes de vitesse (%).
|
||||||
|
"""
|
||||||
|
if direction_sector_size <= 0 or direction_sector_size > 180:
|
||||||
|
raise ValueError("direction_sector_size doit être compris entre 1 et 180 degrés.")
|
||||||
|
|
||||||
|
if "wind_speed" not in df.columns or "wind_direction" not in df.columns:
|
||||||
|
raise KeyError("Le DataFrame doit contenir 'wind_speed' et 'wind_direction'.")
|
||||||
|
|
||||||
|
data = df[["wind_speed", "wind_direction"]].dropna()
|
||||||
|
if data.empty:
|
||||||
|
return pd.DataFrame(), [], float(direction_sector_size)
|
||||||
|
|
||||||
|
n_sectors = int(360 / direction_sector_size)
|
||||||
|
direction = data["wind_direction"].to_numpy(dtype=float) % 360.0
|
||||||
|
sector_indices = np.floor(direction / direction_sector_size).astype(int) % n_sectors
|
||||||
|
|
||||||
|
bins = list(speed_bins)
|
||||||
|
if not np.isinf(bins[-1]):
|
||||||
|
bins.append(float("inf"))
|
||||||
|
labels = _format_speed_bin_labels(bins)
|
||||||
|
|
||||||
|
speed_categories = pd.cut(
|
||||||
|
data["wind_speed"],
|
||||||
|
bins=bins,
|
||||||
|
right=False,
|
||||||
|
include_lowest=True,
|
||||||
|
labels=labels,
|
||||||
|
)
|
||||||
|
|
||||||
|
counts = (
|
||||||
|
pd.crosstab(sector_indices, speed_categories)
|
||||||
|
.reindex(range(n_sectors), fill_value=0)
|
||||||
|
.reindex(columns=labels, fill_value=0)
|
||||||
|
)
|
||||||
|
|
||||||
|
total = counts.values.sum()
|
||||||
|
frequencies = counts / total * 100.0 if total > 0 else counts.astype(float)
|
||||||
|
frequencies.index = frequencies.index * direction_sector_size
|
||||||
|
return frequencies, labels, float(direction_sector_size)
|
||||||
|
|
||||||
|
|
||||||
|
def compute_daily_rainfall_totals(
|
||||||
|
df: pd.DataFrame,
|
||||||
|
*,
|
||||||
|
rate_column: str = "rain_rate",
|
||||||
|
) -> pd.DataFrame:
|
||||||
|
"""
|
||||||
|
Convertit un taux de pluie (mm/h) en cumuls journaliers et cumulés.
|
||||||
|
"""
|
||||||
|
_ensure_datetime_index(df)
|
||||||
|
if rate_column not in df.columns:
|
||||||
|
raise KeyError(f"Colonne absente : {rate_column}")
|
||||||
|
|
||||||
|
series = df[rate_column].fillna(0.0).sort_index()
|
||||||
|
if series.empty:
|
||||||
|
return pd.DataFrame(columns=["daily_total", "cumulative_total"])
|
||||||
|
|
||||||
|
time_step = _infer_time_step(series.index)
|
||||||
|
diffs = series.index.to_series().diff()
|
||||||
|
diffs = diffs.fillna(time_step)
|
||||||
|
hours = diffs.dt.total_seconds() / 3600.0
|
||||||
|
|
||||||
|
rainfall_mm = series.to_numpy(dtype=float) * hours.to_numpy(dtype=float)
|
||||||
|
rainfall_series = pd.Series(rainfall_mm, index=series.index)
|
||||||
|
|
||||||
|
daily_totals = rainfall_series.resample("1D").sum()
|
||||||
|
cumulative = daily_totals.cumsum()
|
||||||
|
|
||||||
|
result = pd.DataFrame(
|
||||||
|
{
|
||||||
|
"daily_total": daily_totals,
|
||||||
|
"cumulative_total": cumulative,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return result
|
||||||
|
|||||||
180
meteo/plots.py
180
meteo/plots.py
@ -6,9 +6,12 @@ from typing import Callable, Sequence
|
|||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
from matplotlib.colors import Normalize
|
from matplotlib.colors import Normalize
|
||||||
|
from matplotlib.ticker import FuncFormatter
|
||||||
|
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
|
||||||
from .variables import Variable
|
from .variables import Variable
|
||||||
|
|
||||||
|
|
||||||
@ -473,3 +476,180 @@ def plot_event_composite(
|
|||||||
plt.close(fig)
|
plt.close(fig)
|
||||||
|
|
||||||
return output_path.resolve()
|
return output_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_wind_rose(
|
||||||
|
frequencies: pd.DataFrame,
|
||||||
|
speed_bin_labels: Sequence[str],
|
||||||
|
output_path: str | Path,
|
||||||
|
*,
|
||||||
|
sector_size_deg: float,
|
||||||
|
cmap: str = "viridis",
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Trace une rose des vents empilée par classes de vitesses (en % du temps).
|
||||||
|
"""
|
||||||
|
output_path = Path(output_path)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if frequencies.empty:
|
||||||
|
fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
|
||||||
|
ax.text(0.5, 0.5, "Données de vent insuffisantes.", 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(subplot_kw={"projection": "polar"}, figsize=(6, 6))
|
||||||
|
cmap_obj = plt.get_cmap(cmap, len(speed_bin_labels))
|
||||||
|
colors = cmap_obj(np.linspace(0.2, 0.95, len(speed_bin_labels)))
|
||||||
|
|
||||||
|
angles = np.deg2rad(frequencies.index.to_numpy(dtype=float) + sector_size_deg / 2.0)
|
||||||
|
width = np.deg2rad(sector_size_deg)
|
||||||
|
bottom = np.zeros_like(angles, dtype=float)
|
||||||
|
|
||||||
|
for label, color in zip(speed_bin_labels, colors):
|
||||||
|
values = frequencies[label].to_numpy(dtype=float)
|
||||||
|
bars = ax.bar(
|
||||||
|
angles,
|
||||||
|
values,
|
||||||
|
width=width,
|
||||||
|
bottom=bottom,
|
||||||
|
color=color,
|
||||||
|
edgecolor="white",
|
||||||
|
linewidth=0.5,
|
||||||
|
align="center",
|
||||||
|
)
|
||||||
|
bottom += values
|
||||||
|
|
||||||
|
ax.set_theta_zero_location("N")
|
||||||
|
ax.set_theta_direction(-1)
|
||||||
|
ax.set_xticks(np.deg2rad(np.arange(0, 360, 45)))
|
||||||
|
ax.set_xticklabels(["N", "NE", "E", "SE", "S", "SO", "O", "NO"])
|
||||||
|
max_radius = np.max(bottom)
|
||||||
|
ax.set_ylim(0, max(max_radius * 1.1, 1))
|
||||||
|
ax.yaxis.set_major_formatter(FuncFormatter(lambda val, _pos: f"{val:.0f}%"))
|
||||||
|
ax.set_title("Rose des vents (fréquence en %)")
|
||||||
|
legend_handles = [
|
||||||
|
plt.Line2D([0], [0], color=color, linewidth=6, label=label) for label, color in zip(speed_bin_labels, colors)
|
||||||
|
]
|
||||||
|
ax.legend(
|
||||||
|
handles=legend_handles,
|
||||||
|
loc="lower center",
|
||||||
|
bbox_to_anchor=(0.5, -0.15),
|
||||||
|
ncol=2,
|
||||||
|
title="Vitesses (km/h)",
|
||||||
|
)
|
||||||
|
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(output_path, dpi=150, bbox_inches="tight")
|
||||||
|
plt.close(fig)
|
||||||
|
return output_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_diurnal_cycle(
|
||||||
|
stats: DiurnalCycleStats,
|
||||||
|
variables: Sequence[Variable],
|
||||||
|
output_path: str | Path,
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Trace les cycles diurnes moyens (moyenne/médiane + quantiles).
|
||||||
|
"""
|
||||||
|
output_path = Path(output_path)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
hours = stats.mean.index.to_numpy(dtype=float)
|
||||||
|
n_vars = len(variables)
|
||||||
|
fig, axes = plt.subplots(n_vars, 1, figsize=(10, 3 * n_vars), sharex=True)
|
||||||
|
if n_vars == 1:
|
||||||
|
axes = [axes]
|
||||||
|
|
||||||
|
for ax, var in zip(axes, variables):
|
||||||
|
col = var.column
|
||||||
|
ax.plot(hours, stats.mean[col], label="Moyenne", color="tab:blue")
|
||||||
|
ax.plot(hours, stats.median[col], label="Médiane", color="tab:orange", linestyle="--")
|
||||||
|
if stats.quantile_low is not None and stats.quantile_high is not None:
|
||||||
|
ax.fill_between(
|
||||||
|
hours,
|
||||||
|
stats.quantile_low[col],
|
||||||
|
stats.quantile_high[col],
|
||||||
|
color="tab:blue",
|
||||||
|
alpha=0.15,
|
||||||
|
label=(
|
||||||
|
f"Quantiles {int(stats.quantile_low_level * 100)}–{int(stats.quantile_high_level * 100)}%"
|
||||||
|
if stats.quantile_low_level is not None and stats.quantile_high_level is not None
|
||||||
|
else "Quantiles"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
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("Heure locale")
|
||||||
|
axes[0].legend(loc="upper right")
|
||||||
|
axes[-1].set_xticks(range(0, 24, 2))
|
||||||
|
axes[-1].set_xlim(0, 23)
|
||||||
|
fig.suptitle("Cycle diurne moyen")
|
||||||
|
fig.tight_layout(rect=[0, 0, 1, 0.97])
|
||||||
|
fig.savefig(output_path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
return output_path.resolve()
|
||||||
|
|
||||||
|
|
||||||
|
def plot_daily_rainfall_hyetograph(
|
||||||
|
daily_rain: pd.DataFrame,
|
||||||
|
output_path: str | Path,
|
||||||
|
) -> Path:
|
||||||
|
"""
|
||||||
|
Affiche les cumuls quotidiens de pluie (barres) et le cumul annuel (ligne).
|
||||||
|
"""
|
||||||
|
output_path = Path(output_path)
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if daily_rain.empty:
|
||||||
|
fig, ax = plt.subplots()
|
||||||
|
ax.text(0.5, 0.5, "Pas de données de précipitations 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()
|
||||||
|
|
||||||
|
fig, ax1 = plt.subplots(figsize=(12, 5))
|
||||||
|
ax1.bar(
|
||||||
|
daily_rain.index,
|
||||||
|
daily_rain["daily_total"],
|
||||||
|
width=0.8,
|
||||||
|
color="tab:blue",
|
||||||
|
alpha=0.7,
|
||||||
|
label="Pluie quotidienne",
|
||||||
|
)
|
||||||
|
ax1.set_ylabel("Pluie quotidienne (mm)")
|
||||||
|
ax1.set_xlabel("Date")
|
||||||
|
ax1.grid(True, axis="y", linestyle=":", alpha=0.5)
|
||||||
|
|
||||||
|
ax2 = ax1.twinx()
|
||||||
|
ax2.plot(
|
||||||
|
daily_rain.index,
|
||||||
|
daily_rain["cumulative_total"],
|
||||||
|
color="tab:red",
|
||||||
|
linewidth=2,
|
||||||
|
label="Cumul annuel",
|
||||||
|
)
|
||||||
|
ax2.set_ylabel("Pluie cumulée (mm)")
|
||||||
|
|
||||||
|
locator = mdates.AutoDateLocator()
|
||||||
|
formatter = mdates.ConciseDateFormatter(locator)
|
||||||
|
ax1.xaxis.set_major_locator(locator)
|
||||||
|
ax1.xaxis.set_major_formatter(formatter)
|
||||||
|
|
||||||
|
lines_labels = [
|
||||||
|
(ax1.get_legend_handles_labels()),
|
||||||
|
(ax2.get_legend_handles_labels()),
|
||||||
|
]
|
||||||
|
lines, labels = [sum(lol, []) for lol in zip(*lines_labels)]
|
||||||
|
ax1.legend(lines, labels, loc="upper left")
|
||||||
|
|
||||||
|
fig.tight_layout()
|
||||||
|
fig.savefig(output_path, dpi=150)
|
||||||
|
plt.close(fig)
|
||||||
|
return output_path.resolve()
|
||||||
|
|||||||
46
scripts/plot_diurnal_cycle.py
Normal file
46
scripts/plot_diurnal_cycle.py
Normal file
@ -0,0 +1,46 @@
|
|||||||
|
# scripts/plot_diurnal_cycle.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_diurnal_cycle_statistics
|
||||||
|
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"]
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
variables = [VARIABLES_BY_KEY[key] for key in VARIABLE_KEYS]
|
||||||
|
stats = compute_diurnal_cycle_statistics(
|
||||||
|
df=df,
|
||||||
|
variables=variables,
|
||||||
|
quantiles=(0.25, 0.75),
|
||||||
|
)
|
||||||
|
|
||||||
|
output_path = plot_diurnal_cycle(
|
||||||
|
stats=stats,
|
||||||
|
variables=variables,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✔ Cycle diurne sauvegardé : {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
41
scripts/plot_rain_hyetograph.py
Normal file
41
scripts/plot_rain_hyetograph.py
Normal file
@ -0,0 +1,41 @@
|
|||||||
|
# scripts/plot_rain_hyetograph.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from meteo.dataset import load_raw_csv
|
||||||
|
from meteo.analysis import compute_daily_rainfall_totals
|
||||||
|
from meteo.plots import plot_daily_rainfall_hyetograph
|
||||||
|
|
||||||
|
|
||||||
|
CSV_PATH = Path("data/weather_minutely.csv")
|
||||||
|
OUTPUT_PATH = Path("figures/rainfall_hyetograph/daily_rainfall_hyetograph.png")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
daily_totals = compute_daily_rainfall_totals(df=df, rate_column="rain_rate")
|
||||||
|
|
||||||
|
if daily_totals.empty:
|
||||||
|
print("⚠ Aucune donnée de pluie cumule à afficher.")
|
||||||
|
return
|
||||||
|
|
||||||
|
output_path = plot_daily_rainfall_hyetograph(
|
||||||
|
daily_rain=daily_totals,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✔ Hyétographe quotidien exporté : {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
48
scripts/plot_wind_rose.py
Normal file
48
scripts/plot_wind_rose.py
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
# scripts/plot_wind_rose.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from meteo.dataset import load_raw_csv
|
||||||
|
from meteo.analysis import compute_wind_rose_distribution
|
||||||
|
from meteo.plots import plot_wind_rose
|
||||||
|
|
||||||
|
|
||||||
|
CSV_PATH = Path("data/weather_minutely.csv")
|
||||||
|
OUTPUT_PATH = Path("figures/wind_rose/wind_rose.png")
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
frequencies, 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("⚠ Pas assez de données pour construire une rose des vents.")
|
||||||
|
return
|
||||||
|
|
||||||
|
output_path = plot_wind_rose(
|
||||||
|
frequencies=frequencies,
|
||||||
|
speed_bin_labels=labels,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
sector_size_deg=sector_size,
|
||||||
|
cmap="plasma",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✔ Rose des vents exportée : {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
55
scripts/plot_wind_rose_rain.py
Normal file
55
scripts/plot_wind_rose_rain.py
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
# scripts/plot_wind_rose_rain.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
from meteo.dataset import load_raw_csv
|
||||||
|
from meteo.analysis import compute_wind_rose_distribution
|
||||||
|
from meteo.plots import plot_wind_rose
|
||||||
|
|
||||||
|
|
||||||
|
CSV_PATH = Path("data/weather_minutely.csv")
|
||||||
|
OUTPUT_PATH = Path("figures/wind_rose/wind_rose_during_rain.png")
|
||||||
|
RAIN_THRESHOLD = 0.2 # mm/h, pour considérer qu'il pleut réellement
|
||||||
|
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
rainy_df = df[df["rain_rate"].fillna(0.0) >= RAIN_THRESHOLD]
|
||||||
|
print(f"Lignes avec pluie ≥ {RAIN_THRESHOLD} mm/h : {len(rainy_df)}")
|
||||||
|
if rainy_df.empty:
|
||||||
|
print("⚠ Aucun événement pluvieux ne dépasse ce seuil, abandon.")
|
||||||
|
return
|
||||||
|
|
||||||
|
frequencies, labels, sector_size = compute_wind_rose_distribution(
|
||||||
|
df=rainy_df,
|
||||||
|
direction_sector_size=30,
|
||||||
|
speed_bins=(0, 5, 15, 30, 50, float("inf")),
|
||||||
|
)
|
||||||
|
|
||||||
|
if frequencies.empty:
|
||||||
|
print("⚠ Pas assez de données pour construire une rose des vents pendant la pluie.")
|
||||||
|
return
|
||||||
|
|
||||||
|
output_path = plot_wind_rose(
|
||||||
|
frequencies=frequencies,
|
||||||
|
speed_bin_labels=labels,
|
||||||
|
output_path=OUTPUT_PATH,
|
||||||
|
sector_size_deg=sector_size,
|
||||||
|
cmap="plasma",
|
||||||
|
)
|
||||||
|
|
||||||
|
print(f"✔ Rose des vents pendant la pluie exportée : {output_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Loading…
x
Reference in New Issue
Block a user