You've already forked donnees_meteo
Exportation des CSV pour chaque graphique
This commit is contained in:
156
meteo/plots.py
156
meteo/plots.py
@@ -3,7 +3,7 @@ from __future__ import annotations
|
||||
|
||||
import calendar
|
||||
from pathlib import Path
|
||||
from typing import Callable, Sequence
|
||||
from typing import Any, Callable, Sequence
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.colors import Normalize
|
||||
@@ -17,6 +17,44 @@ from .season import SEASON_LABELS
|
||||
from .variables import Variable
|
||||
|
||||
|
||||
def export_plot_dataset(data: Any, output_path: str | Path, *, suffix: str = ".csv") -> Path | None:
|
||||
"""Persist the dataset used for a figure next to the exported image."""
|
||||
|
||||
if data is None:
|
||||
return None
|
||||
|
||||
output_path = Path(output_path)
|
||||
dataset_path = output_path.with_suffix(suffix)
|
||||
dataset_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
def _normalize(value: Any, *, default_name: str = "value") -> pd.DataFrame:
|
||||
if isinstance(value, pd.DataFrame):
|
||||
return value.copy()
|
||||
if isinstance(value, pd.Series):
|
||||
return value.to_frame(name=value.name or default_name)
|
||||
if isinstance(value, np.ndarray):
|
||||
return pd.DataFrame(value)
|
||||
return pd.DataFrame(value)
|
||||
|
||||
if isinstance(data, dict):
|
||||
frames: list[pd.DataFrame] = []
|
||||
for key, value in data.items():
|
||||
if value is None:
|
||||
continue
|
||||
frame = _normalize(value, default_name=str(key))
|
||||
# Prefix columns with the key to retain provenance when merging
|
||||
frame = pd.concat({str(key): frame}, axis=1)
|
||||
frames.append(frame)
|
||||
if not frames:
|
||||
return None
|
||||
export_df = pd.concat(frames, axis=1)
|
||||
else:
|
||||
export_df = _normalize(data)
|
||||
|
||||
export_df.to_csv(dataset_path)
|
||||
return dataset_path
|
||||
|
||||
|
||||
def plot_scatter_pair(
|
||||
df: pd.DataFrame,
|
||||
var_x: Variable,
|
||||
@@ -47,7 +85,25 @@ def plot_scatter_pair(
|
||||
if sample_step > 1:
|
||||
df_pair = df_pair.iloc[::sample_step, :]
|
||||
|
||||
use_polar = var_y.key == "wind_direction"
|
||||
export_plot_dataset(df_pair, output_path)
|
||||
|
||||
direction_var: Variable | None = None
|
||||
radial_var: Variable | None = None
|
||||
direction_series: pd.Series | None = None
|
||||
radial_series: pd.Series | None = None
|
||||
|
||||
if var_y.key == "wind_direction" and var_x.key != "wind_direction":
|
||||
direction_var = var_y
|
||||
direction_series = df_pair[var_y.column]
|
||||
radial_var = var_x
|
||||
radial_series = df_pair[var_x.column]
|
||||
elif var_x.key == "wind_direction" and var_y.key != "wind_direction":
|
||||
direction_var = var_x
|
||||
direction_series = df_pair[var_x.column]
|
||||
radial_var = var_y
|
||||
radial_series = df_pair[var_y.column]
|
||||
|
||||
use_polar = direction_var is not None and radial_var is not None
|
||||
|
||||
if use_polar:
|
||||
fig, ax = plt.subplots(subplot_kw={"projection": "polar"})
|
||||
@@ -76,8 +132,11 @@ def plot_scatter_pair(
|
||||
}
|
||||
|
||||
if use_polar:
|
||||
theta = np.deg2rad(df_pair[var_y.column].to_numpy(dtype=float) % 360.0)
|
||||
radius_raw = df_pair[var_x.column].to_numpy(dtype=float)
|
||||
assert direction_series is not None and radial_series is not None
|
||||
assert direction_var is not None and radial_var is not None
|
||||
|
||||
theta = np.deg2rad(direction_series.to_numpy(dtype=float) % 360.0)
|
||||
radius_raw = radial_series.to_numpy(dtype=float)
|
||||
|
||||
if radius_raw.size == 0:
|
||||
radius = radius_raw
|
||||
@@ -116,7 +175,7 @@ def plot_scatter_pair(
|
||||
ax.set_rlabel_position(225)
|
||||
ax.set_ylim(0.0, 1.0)
|
||||
|
||||
unit_suffix = f" {var_x.unit}" if var_x.unit else ""
|
||||
unit_suffix = f" {radial_var.unit}" if radial_var.unit else ""
|
||||
ax.text(
|
||||
0.5,
|
||||
-0.1,
|
||||
@@ -127,7 +186,7 @@ def plot_scatter_pair(
|
||||
fontsize=8,
|
||||
)
|
||||
|
||||
radial_label = f"{var_x.label} ({var_x.unit})" if var_x.unit else var_x.label
|
||||
radial_label = f"{radial_var.label} ({radial_var.unit})" if radial_var.unit else radial_var.label
|
||||
ax.set_ylabel(radial_label, labelpad=20)
|
||||
else:
|
||||
scatter = ax.scatter(
|
||||
@@ -161,7 +220,8 @@ def plot_scatter_pair(
|
||||
cbar.set_label("Temps (ancien → récent)")
|
||||
|
||||
if use_polar:
|
||||
ax.set_title(f"{var_y.label} en fonction de {var_x.label}")
|
||||
assert direction_var is not None and radial_var is not None
|
||||
ax.set_title(f"{radial_var.label} en fonction de {direction_var.label}")
|
||||
else:
|
||||
ax.set_xlabel(f"{var_x.label} ({var_x.unit})")
|
||||
ax.set_ylabel(f"{var_y.label} ({var_y.unit})")
|
||||
@@ -195,6 +255,7 @@ def plot_hexbin_with_third_variable(
|
||||
reduce_func = reduce_func or np.mean
|
||||
|
||||
df_xyz = df[[var_x.column, var_y.column, var_color.column]].dropna()
|
||||
export_plot_dataset(df_xyz, output_path)
|
||||
if df_xyz.empty:
|
||||
fig, ax = plt.subplots()
|
||||
ax.text(
|
||||
@@ -250,6 +311,8 @@ def plot_lagged_correlation(
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
export_plot_dataset(lag_df, output_path)
|
||||
|
||||
plt.figure()
|
||||
plt.plot(lag_df.index, lag_df["correlation"])
|
||||
plt.axvline(0, linestyle="--") # lag = 0
|
||||
@@ -295,6 +358,7 @@ def plot_correlation_heatmap(
|
||||
|
||||
# On aligne la matrice sur l'ordre désiré
|
||||
corr = corr.loc[columns, columns]
|
||||
export_plot_dataset(corr, output_path)
|
||||
|
||||
data = corr.to_numpy()
|
||||
|
||||
@@ -357,6 +421,8 @@ def plot_rolling_correlation_heatmap(
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
export_plot_dataset(rolling_corr, output_path)
|
||||
|
||||
if rolling_corr.empty:
|
||||
fig, ax = plt.subplots()
|
||||
ax.text(0.5, 0.5, "Aucune donnée de corrélation glissante.", ha="center", va="center")
|
||||
@@ -442,6 +508,16 @@ def plot_event_composite(
|
||||
quantile_low = group.quantile(q_low) if q_low is not None else None
|
||||
quantile_high = group.quantile(q_high) if q_high is not None else None
|
||||
|
||||
export_plot_dataset(
|
||||
{
|
||||
"mean": mean_df,
|
||||
"median": median_df,
|
||||
"quantile_low": quantile_low,
|
||||
"quantile_high": quantile_high,
|
||||
},
|
||||
output_path,
|
||||
)
|
||||
|
||||
offsets = mean_df.index.to_numpy(dtype=float)
|
||||
n_vars = len(variables)
|
||||
fig, axes = plt.subplots(n_vars, 1, figsize=(10, 3 * n_vars), sharex=True)
|
||||
@@ -502,6 +578,11 @@ def plot_wind_rose(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
dataset = frequencies.copy()
|
||||
dataset.insert(0, "sector_start_deg", frequencies.index)
|
||||
dataset.insert(1, "sector_center_deg", frequencies.index + sector_size_deg / 2.0)
|
||||
export_plot_dataset(dataset, output_path)
|
||||
|
||||
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)))
|
||||
@@ -560,6 +641,16 @@ def plot_diurnal_cycle(
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
export_plot_dataset(
|
||||
{
|
||||
"mean": stats.mean,
|
||||
"median": stats.median,
|
||||
"quantile_low": stats.quantile_low,
|
||||
"quantile_high": stats.quantile_high,
|
||||
},
|
||||
output_path,
|
||||
)
|
||||
|
||||
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)
|
||||
@@ -630,6 +721,9 @@ def plot_seasonal_boxplots(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
dataset_columns = [season_column] + [var.column for var in variables]
|
||||
export_plot_dataset(df[dataset_columns], output_path)
|
||||
|
||||
n_vars = len(variables)
|
||||
fig, axes = plt.subplots(n_vars, 1, figsize=(10, 3 * n_vars), sharex=True)
|
||||
if n_vars == 1:
|
||||
@@ -687,6 +781,11 @@ def plot_monthly_boxplots(
|
||||
if not isinstance(df.index, pd.DatetimeIndex):
|
||||
raise TypeError("plot_monthly_boxplots nécessite un DatetimeIndex.")
|
||||
|
||||
value_columns = [var.column for var in variables]
|
||||
dataset = df[value_columns].copy()
|
||||
dataset.insert(0, "month", df.index.month)
|
||||
export_plot_dataset(dataset, output_path)
|
||||
|
||||
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)
|
||||
@@ -756,6 +855,25 @@ def plot_binned_profiles(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
bin_summary = pd.DataFrame(
|
||||
{
|
||||
"bin_left": stats.intervals.left,
|
||||
"bin_right": stats.intervals.right,
|
||||
"center": stats.centers,
|
||||
}
|
||||
)
|
||||
export_plot_dataset(
|
||||
{
|
||||
"bins": bin_summary,
|
||||
"counts": stats.counts,
|
||||
"mean": stats.mean,
|
||||
"median": stats.median,
|
||||
"quantile_low": stats.quantile_low,
|
||||
"quantile_high": stats.quantile_high,
|
||||
},
|
||||
output_path,
|
||||
)
|
||||
|
||||
base_axes = len(variables)
|
||||
total_axes = base_axes + (1 if show_counts else 0)
|
||||
fig, axes = plt.subplots(
|
||||
@@ -839,6 +957,8 @@ def plot_daily_rainfall_hyetograph(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(daily_rain, output_path)
|
||||
|
||||
fig, ax1 = plt.subplots(figsize=(12, 5))
|
||||
ax1.bar(
|
||||
daily_rain.index,
|
||||
@@ -900,6 +1020,8 @@ def plot_rainfall_by_season(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(rainfall_df, output_path)
|
||||
|
||||
seasons = rainfall_df.index.tolist()
|
||||
x = np.arange(len(seasons))
|
||||
totals = rainfall_df["total_rain_mm"].to_numpy(dtype=float)
|
||||
@@ -968,6 +1090,8 @@ def plot_monthly_anomalies(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_frames: list[pd.DataFrame] = []
|
||||
|
||||
n_vars = len(variables)
|
||||
fig, axes = plt.subplots(n_vars, 1, figsize=(12, 3 * n_vars), sharex=True)
|
||||
if n_vars == 1:
|
||||
@@ -987,6 +1111,11 @@ def plot_monthly_anomalies(
|
||||
clim = climatology.loc[months, var.column].to_numpy(dtype=float)
|
||||
anomaly = actual.to_numpy(dtype=float) - clim
|
||||
|
||||
clim_series = pd.Series(clim, index=actual.index, name="climatology")
|
||||
frame = pd.DataFrame({"actual": actual, "climatology": clim_series})
|
||||
frame["anomaly"] = frame["actual"] - frame["climatology"]
|
||||
export_frames.append(pd.concat({var.column: frame}, axis=1))
|
||||
|
||||
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(
|
||||
@@ -1012,6 +1141,9 @@ def plot_monthly_anomalies(
|
||||
ax.xaxis.set_major_locator(locator)
|
||||
ax.xaxis.set_major_formatter(formatter)
|
||||
|
||||
if export_frames:
|
||||
export_plot_dataset(pd.concat(export_frames, axis=1), output_path)
|
||||
|
||||
axes[-1].set_xlabel("Date")
|
||||
axes[0].legend(loc="upper right")
|
||||
fig.suptitle(title)
|
||||
@@ -1041,6 +1173,8 @@ def plot_wind_vector_series(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(vector_df, output_path)
|
||||
|
||||
times = vector_df.index
|
||||
x = mdates.date2num(times)
|
||||
u = vector_df["u"].to_numpy(dtype=float)
|
||||
@@ -1089,6 +1223,8 @@ def plot_calendar_heatmap(
|
||||
output_path = Path(output_path)
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
export_plot_dataset(matrix, output_path)
|
||||
|
||||
if matrix.empty:
|
||||
fig, ax = plt.subplots()
|
||||
ax.text(0.5, 0.5, "Pas de données pour la heatmap.", ha="center", va="center")
|
||||
@@ -1141,6 +1277,8 @@ def plot_weekday_profiles(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(weekday_df, output_path)
|
||||
|
||||
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)
|
||||
@@ -1192,6 +1330,8 @@ def plot_seasonal_hourly_profiles(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(profile_df, output_path)
|
||||
|
||||
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]))
|
||||
@@ -1229,6 +1369,8 @@ def plot_daylight_hours(
|
||||
plt.close(fig)
|
||||
return output_path.resolve()
|
||||
|
||||
export_plot_dataset(monthly_series, output_path)
|
||||
|
||||
months = monthly_series.index
|
||||
fig, ax = plt.subplots(figsize=(10, 4))
|
||||
ax.bar(months, monthly_series.values, color="goldenrod", alpha=0.8)
|
||||
|
||||
Reference in New Issue
Block a user