diff --git a/figures/pairwise_scatter/scatter_humidity_vs_illuminance.png b/figures/pairwise_scatter/scatter_humidity_vs_illuminance.png index b346fc0..0cd5eae 100644 Binary files a/figures/pairwise_scatter/scatter_humidity_vs_illuminance.png and b/figures/pairwise_scatter/scatter_humidity_vs_illuminance.png differ diff --git a/figures/pairwise_scatter/scatter_humidity_vs_pressure.png b/figures/pairwise_scatter/scatter_humidity_vs_pressure.png index 79193d0..5c04a1d 100644 Binary files a/figures/pairwise_scatter/scatter_humidity_vs_pressure.png and b/figures/pairwise_scatter/scatter_humidity_vs_pressure.png differ diff --git a/figures/pairwise_scatter/scatter_humidity_vs_rain_rate.png b/figures/pairwise_scatter/scatter_humidity_vs_rain_rate.png index ecb4854..c90c120 100644 Binary files a/figures/pairwise_scatter/scatter_humidity_vs_rain_rate.png and b/figures/pairwise_scatter/scatter_humidity_vs_rain_rate.png differ diff --git a/figures/pairwise_scatter/scatter_humidity_vs_wind_direction.png b/figures/pairwise_scatter/scatter_humidity_vs_wind_direction.png index 9fec78a..01c436e 100644 Binary files a/figures/pairwise_scatter/scatter_humidity_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_humidity_vs_wind_direction.png differ diff --git a/figures/pairwise_scatter/scatter_humidity_vs_wind_speed.png b/figures/pairwise_scatter/scatter_humidity_vs_wind_speed.png index 10d2584..a80e632 100644 Binary files a/figures/pairwise_scatter/scatter_humidity_vs_wind_speed.png and b/figures/pairwise_scatter/scatter_humidity_vs_wind_speed.png differ diff --git a/figures/pairwise_scatter/scatter_illuminance_vs_wind_direction.png b/figures/pairwise_scatter/scatter_illuminance_vs_wind_direction.png index 659d96e..2f4afa9 100644 Binary files a/figures/pairwise_scatter/scatter_illuminance_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_illuminance_vs_wind_direction.png differ diff --git a/figures/pairwise_scatter/scatter_illuminance_vs_wind_speed.png b/figures/pairwise_scatter/scatter_illuminance_vs_wind_speed.png index 41065e9..e0d2cc4 100644 Binary files a/figures/pairwise_scatter/scatter_illuminance_vs_wind_speed.png and b/figures/pairwise_scatter/scatter_illuminance_vs_wind_speed.png differ diff --git a/figures/pairwise_scatter/scatter_pressure_vs_illuminance.png b/figures/pairwise_scatter/scatter_pressure_vs_illuminance.png index 9bc3097..3de3375 100644 Binary files a/figures/pairwise_scatter/scatter_pressure_vs_illuminance.png and b/figures/pairwise_scatter/scatter_pressure_vs_illuminance.png differ diff --git a/figures/pairwise_scatter/scatter_pressure_vs_rain_rate.png b/figures/pairwise_scatter/scatter_pressure_vs_rain_rate.png index f50c109..94a7603 100644 Binary files a/figures/pairwise_scatter/scatter_pressure_vs_rain_rate.png and b/figures/pairwise_scatter/scatter_pressure_vs_rain_rate.png differ diff --git a/figures/pairwise_scatter/scatter_pressure_vs_wind_direction.png b/figures/pairwise_scatter/scatter_pressure_vs_wind_direction.png index 410256e..8fd4401 100644 Binary files a/figures/pairwise_scatter/scatter_pressure_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_pressure_vs_wind_direction.png differ diff --git a/figures/pairwise_scatter/scatter_pressure_vs_wind_speed.png b/figures/pairwise_scatter/scatter_pressure_vs_wind_speed.png index 6f7872b..79832aa 100644 Binary files a/figures/pairwise_scatter/scatter_pressure_vs_wind_speed.png and b/figures/pairwise_scatter/scatter_pressure_vs_wind_speed.png differ diff --git a/figures/pairwise_scatter/scatter_rain_rate_vs_illuminance.png b/figures/pairwise_scatter/scatter_rain_rate_vs_illuminance.png index 9853e56..d9c6f58 100644 Binary files a/figures/pairwise_scatter/scatter_rain_rate_vs_illuminance.png and b/figures/pairwise_scatter/scatter_rain_rate_vs_illuminance.png differ diff --git a/figures/pairwise_scatter/scatter_rain_rate_vs_wind_direction.png b/figures/pairwise_scatter/scatter_rain_rate_vs_wind_direction.png index da27658..10c3fff 100644 Binary files a/figures/pairwise_scatter/scatter_rain_rate_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_rain_rate_vs_wind_direction.png differ diff --git a/figures/pairwise_scatter/scatter_rain_rate_vs_wind_speed.png b/figures/pairwise_scatter/scatter_rain_rate_vs_wind_speed.png index 5237543..473f7c0 100644 Binary files a/figures/pairwise_scatter/scatter_rain_rate_vs_wind_speed.png and b/figures/pairwise_scatter/scatter_rain_rate_vs_wind_speed.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_humidity.png b/figures/pairwise_scatter/scatter_temperature_vs_humidity.png index 1362a44..da22c4d 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_humidity.png and b/figures/pairwise_scatter/scatter_temperature_vs_humidity.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_illuminance.png b/figures/pairwise_scatter/scatter_temperature_vs_illuminance.png index a606f88..649795b 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_illuminance.png and b/figures/pairwise_scatter/scatter_temperature_vs_illuminance.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_pressure.png b/figures/pairwise_scatter/scatter_temperature_vs_pressure.png index 2a6d4c0..351d124 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_pressure.png and b/figures/pairwise_scatter/scatter_temperature_vs_pressure.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_rain_rate.png b/figures/pairwise_scatter/scatter_temperature_vs_rain_rate.png index 41bbb18..23c86c5 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_rain_rate.png and b/figures/pairwise_scatter/scatter_temperature_vs_rain_rate.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_wind_direction.png b/figures/pairwise_scatter/scatter_temperature_vs_wind_direction.png index 490da3b..58e5277 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_temperature_vs_wind_direction.png differ diff --git a/figures/pairwise_scatter/scatter_temperature_vs_wind_speed.png b/figures/pairwise_scatter/scatter_temperature_vs_wind_speed.png index 32fbe5f..731ca4b 100644 Binary files a/figures/pairwise_scatter/scatter_temperature_vs_wind_speed.png and b/figures/pairwise_scatter/scatter_temperature_vs_wind_speed.png differ diff --git a/figures/pairwise_scatter/scatter_wind_speed_vs_wind_direction.png b/figures/pairwise_scatter/scatter_wind_speed_vs_wind_direction.png index cc60804..9a08627 100644 Binary files a/figures/pairwise_scatter/scatter_wind_speed_vs_wind_direction.png and b/figures/pairwise_scatter/scatter_wind_speed_vs_wind_direction.png differ diff --git a/meteo/plots.py b/meteo/plots.py index 88d2851..fa70fa3 100644 --- a/meteo/plots.py +++ b/meteo/plots.py @@ -2,8 +2,10 @@ from __future__ import annotations from pathlib import Path +from typing import Sequence import matplotlib.pyplot as plt +from matplotlib.colors import Normalize import numpy as np import pandas as pd @@ -17,12 +19,19 @@ def plot_scatter_pair( output_path: str | Path, *, sample_step: int = 10, + color_by_time: bool = True, + cmap: str = "viridis", ) -> Path: """ Trace un nuage de points (scatter) pour une paire de variables. - On sous-échantillonne les données avec `sample_step` (par exemple, 1 point sur 10) pour éviter un graphique illisible. + - Si `color_by_time` vaut True et que l'index est temporel, les points + sont colorés du plus ancien (sombre) au plus récent (clair). + - Lorsque l'axe Y correspond à la direction du vent, on bascule sur + un graphique polaire plus adapté (0° = Nord, sens horaire) avec + un rayon normalisé : centre = valeur minimale, bord = maximale. """ output_path = Path(output_path) output_path.parent.mkdir(parents=True, exist_ok=True) @@ -33,14 +42,128 @@ def plot_scatter_pair( if sample_step > 1: df_pair = df_pair.iloc[::sample_step, :] - plt.figure() - plt.scatter(df_pair[var_x.column], df_pair[var_y.column], s=5, alpha=0.5) - plt.xlabel(f"{var_x.label} ({var_x.unit})") - plt.ylabel(f"{var_y.label} ({var_y.unit})") - plt.title(f"{var_y.label} en fonction de {var_x.label}") - plt.tight_layout() - plt.savefig(output_path, dpi=150) - plt.close() + use_polar = var_y.key == "wind_direction" + + if use_polar: + fig, ax = plt.subplots(subplot_kw={"projection": "polar"}) + else: + fig, ax = plt.subplots() + + scatter_kwargs: dict = {"s": 5, "alpha": 0.5} + colorbar_meta: dict | None = None + + if color_by_time and isinstance(df_pair.index, pd.DatetimeIndex): + idx = df_pair.index + timestamps = idx.view("int64") + time_span = np.ptp(timestamps) + norm = ( + Normalize(vmin=timestamps.min(), vmax=timestamps.max()) + if time_span > 0 + else None + ) + scatter_kwargs |= {"c": timestamps, "cmap": cmap} + if norm is not None: + scatter_kwargs["norm"] = norm + colorbar_meta = { + "index": idx, + "timestamps": timestamps, + "time_span": time_span, + } + + 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) + + if radius_raw.size == 0: + radius = radius_raw + value_min = value_max = float("nan") + else: + value_min = float(np.min(radius_raw)) + value_max = float(np.max(radius_raw)) + if np.isclose(value_min, value_max): + radius = np.zeros_like(radius_raw) + else: + radius = (radius_raw - value_min) / (value_max - value_min) + + scatter = ax.scatter(theta, radius, **scatter_kwargs) + + cardinal_angles = np.deg2rad(np.arange(0, 360, 45)) + cardinal_labels = ["N", "NE", "E", "SE", "S", "SO", "O", "NO"] + ax.set_theta_zero_location("N") + ax.set_theta_direction(-1) + ax.set_xticks(cardinal_angles) + ax.set_xticklabels(cardinal_labels) + + if radius_raw.size > 0: + if np.isclose(value_min, value_max): + radial_positions = [0.0] + else: + radial_positions = np.linspace(0.0, 1.0, num=5).tolist() + if np.isclose(value_min, value_max): + actual_values = [value_min] + else: + actual_values = [ + value_min + pos * (value_max - value_min) + for pos in radial_positions + ] + ax.set_yticks(radial_positions) + ax.set_yticklabels([f"{val:.1f}" for val in actual_values]) + ax.set_rlabel_position(225) + ax.set_ylim(0.0, 1.0) + + unit_suffix = f" {var_x.unit}" if var_x.unit else "" + ax.text( + 0.5, + -0.1, + f"Centre = {value_min:.1f}{unit_suffix}, bord = {value_max:.1f}{unit_suffix}", + transform=ax.transAxes, + ha="center", + va="top", + fontsize=8, + ) + + radial_label = f"{var_x.label} ({var_x.unit})" if var_x.unit else var_x.label + ax.set_ylabel(radial_label, labelpad=20) + else: + scatter = ax.scatter( + df_pair[var_x.column], + df_pair[var_y.column], + **scatter_kwargs, + ) + + if colorbar_meta is not None: + cbar = fig.colorbar(scatter, ax=ax) + idx = colorbar_meta["index"] + timestamps = colorbar_meta["timestamps"] + time_span = colorbar_meta["time_span"] + + def _format_tick_label(ts: pd.Timestamp) -> str: + base = f"{ts.strftime('%Y-%m-%d')}\n{ts.strftime('%H:%M')}" + tz_name = ts.tzname() + return f"{base} ({tz_name})" if tz_name else base + + if time_span > 0: + tick_datetimes = pd.date_range(start=idx.min(), end=idx.max(), periods=5) + tick_positions = tick_datetimes.view("int64") + tick_labels = [_format_tick_label(ts) for ts in tick_datetimes] + cbar.set_ticks(tick_positions) + cbar.set_ticklabels(tick_labels) + else: + cbar.set_ticks([timestamps[0]]) + ts = idx[0] + cbar.set_ticklabels([_format_tick_label(ts)]) + + cbar.set_label("Temps (ancien → récent)") + + if use_polar: + ax.set_title(f"{var_y.label} en fonction de {var_x.label}") + else: + ax.set_xlabel(f"{var_x.label} ({var_x.unit})") + ax.set_ylabel(f"{var_y.label} ({var_y.unit})") + ax.set_title(f"{var_y.label} en fonction de {var_x.label}") + fig.tight_layout() + fig.savefig(output_path, dpi=150) + plt.close(fig) return output_path.resolve() @@ -146,4 +269,4 @@ def plot_correlation_heatmap( plt.savefig(output_path, dpi=150) plt.close(fig) - return output_path.resolve() \ No newline at end of file + return output_path.resolve()