1

Détermination objective du meilleur jour de la semaine

This commit is contained in:
2025-11-26 22:57:15 +01:00
parent f4bdbe2c7f
commit d7f61ca93a
13 changed files with 329 additions and 10 deletions

View File

@@ -1,8 +1,10 @@
from __future__ import annotations
from pathlib import Path
import sys
from pathlib import Path
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
PROJECT_ROOT = Path(__file__).resolve().parents[3]
@@ -10,16 +12,32 @@ if str(PROJECT_ROOT) not in sys.path:
sys.path.insert(0, str(PROJECT_ROOT))
from meteo.dataset import load_raw_csv
from meteo.plots import export_plot_dataset, plot_weekday_profiles
from meteo.variables import VARIABLES_BY_KEY, Variable
from meteo.plots import plot_weekday_profiles
CSV_PATH = PROJECT_ROOT / "data" / "weather_minutely.csv"
DOC_DIR = Path(__file__).resolve().parent.parent
OUTPUT_PATH = DOC_DIR / "figures" / "weekday_profiles.png"
OUTPUT_PROFILES_PATH = DOC_DIR / "figures" / "weekday_profiles.png"
OUTPUT_SCORES_PATH = DOC_DIR / "figures" / "weekday_scores.png"
OUTPUT_RADAR_DIR = DOC_DIR / "figures" / "weekday_radars"
# On se concentre sur le ressenti "agréable" : température, humidité, lumière.
VARIABLE_KEYS = ["temperature", "humidity", "illuminance"]
# On se concentre sur le ressenti "agréable" :
# - température (plus élevée = meilleur score),
# - humidité (plus faible = meilleur score),
# - pression atmosphérique (score maximal pour une plage "confortable"),
# - luminosité (plus élevée = meilleur score),
# - vent (plus faible = meilleur score).
VARIABLE_KEYS = ["temperature", "humidity", "pressure", "illuminance", "wind_speed"]
# Type de scoring par variable (clé = Variable.key)
COMFORT_SCORING: dict[str, str] = {
"temperature": "higher_better",
"humidity": "lower_better",
"pressure": "midrange_better",
"illuminance": "higher_better",
"wind_speed": "lower_better",
}
def compute_weekday_means(df: pd.DataFrame, variables: list[Variable]) -> pd.DataFrame:
@@ -39,6 +57,234 @@ def compute_weekday_means(df: pd.DataFrame, variables: list[Variable]) -> pd.Dat
return weekday_means
def compute_weekday_scores(weekday_means: pd.DataFrame, variables: list[Variable]) -> pd.DataFrame:
"""
À partir des moyennes par jour, calcule un score normalisé (01) par variable
en fonction d'un critère de confort, puis un score global moyen.
"""
if weekday_means.empty:
return pd.DataFrame(index=range(7))
scores = pd.DataFrame(index=weekday_means.index)
def _normalize_monotonic(series: pd.Series, *, higher_is_better: bool) -> pd.Series:
vmin = float(series.min(skipna=True))
vmax = float(series.max(skipna=True))
if np.isclose(vmax, vmin):
return pd.Series(1.0, index=series.index)
norm = (series - vmin) / (vmax - vmin)
if not higher_is_better:
norm = 1.0 - norm
return norm
def _normalize_midrange(series: pd.Series) -> pd.Series:
"""
Score maximal pour des valeurs proches de la moyenne,
plus faible pour des pressions très basses ou très élevées.
"""
vmin = float(series.min(skipna=True))
vmax = float(series.max(skipna=True))
if np.isclose(vmax, vmin):
return pd.Series(1.0, index=series.index)
mid = 0.5 * (vmin + vmax)
half_range = 0.5 * (vmax - vmin)
# 1 au centre, 0 aux extrêmes (vmin/vmax), valeur >0 dans l'intervalle.
norm = 1.0 - (series - mid).abs() / half_range
norm = norm.clip(lower=0.0, upper=1.0)
return norm
for var in variables:
col = var.column
if col not in weekday_means.columns:
continue
series = weekday_means[col]
if series.isna().all():
continue
scoring = COMFORT_SCORING.get(var.key, "higher_better")
if scoring == "higher_better":
norm = _normalize_monotonic(series, higher_is_better=True)
elif scoring == "lower_better":
norm = _normalize_monotonic(series, higher_is_better=False)
elif scoring == "midrange_better":
norm = _normalize_midrange(series)
else:
# Fallback : plus élevé = meilleur score.
norm = _normalize_monotonic(series, higher_is_better=True)
scores[col] = norm
if scores.empty:
return scores
scores["overall_score"] = scores.mean(axis=1, skipna=True)
scores.index.name = "weekday"
return scores
def plot_overall_weekday_score(
scores: pd.DataFrame,
weekday_labels: list[str],
output_path: Path,
) -> Path | None:
"""
Trace un graphique synthétique du score global par jour de la semaine.
"""
if "overall_score" not in scores.columns or scores["overall_score"].isna().all():
return None
output_path.parent.mkdir(parents=True, exist_ok=True)
export_plot_dataset(scores, output_path)
overall = scores["overall_score"]
x = np.arange(len(weekday_labels))
fig, ax = plt.subplots(figsize=(8, 4))
values = overall.to_numpy(dtype=float)
best_idx = int(np.nanargmax(values))
colors = ["#9ecae1"] * len(values)
colors[best_idx] = "#08519c"
ax.bar(x, values, color=colors)
ax.set_xticks(x)
ax.set_xticklabels(weekday_labels)
ax.set_ylabel("Score global (01)")
ax.set_ylim(0, 1.05)
ax.set_title("Score global d'agrément par jour de semaine")
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()
def plot_weekday_radars(
scores: pd.DataFrame,
variables: list[Variable],
weekday_labels_long: list[str],
output_dir: Path,
) -> list[Path]:
"""
Produit un graphique radar par jour de la semaine, avec un axe par variable.
"""
paths: list[Path] = []
if scores.empty:
return paths
output_dir.mkdir(parents=True, exist_ok=True)
var_cols = [v.column for v in variables if v.column in scores.columns]
if not var_cols:
return paths
labels = [v.label for v in variables if v.column in scores.columns]
n_vars = len(labels)
if n_vars == 0:
return paths
angles = np.linspace(0, 2 * np.pi, n_vars, endpoint=False)
angles = np.concatenate([angles, angles[:1]])
for weekday in scores.index:
day_scores = scores.loc[weekday, var_cols]
if day_scores.isna().all():
continue
values = day_scores.to_numpy(dtype=float)
values = np.nan_to_num(values, nan=0.0)
values = np.concatenate([values, values[:1]])
fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(6, 6))
ax.plot(angles, values, marker="o")
ax.fill(angles, values, alpha=0.25)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(labels)
ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.set_ylim(0, 1.05)
day_label = weekday_labels_long[int(weekday)] if 0 <= int(weekday) < len(weekday_labels_long) else str(weekday)
ax.set_title(f"Profil radar des scores {day_label}")
fig.tight_layout()
filename = f"weekday_radar_{int(weekday)}.png"
output_path = output_dir / filename
# Export des données brutes associées à ce radar
export_plot_dataset(day_scores.to_frame().T, output_path)
fig.savefig(output_path, dpi=150)
plt.close(fig)
paths.append(output_path.resolve())
return paths
def plot_weekday_radar_all(
scores: pd.DataFrame,
variables: list[Variable],
weekday_labels_long: list[str],
output_path: Path,
) -> Path | None:
"""
Produit un seul graphique radar superposant tous les jours de la semaine.
"""
if scores.empty:
return None
output_path.parent.mkdir(parents=True, exist_ok=True)
var_cols = [v.column for v in variables if v.column in scores.columns]
if not var_cols:
return None
labels = [v.label for v in variables if v.column in scores.columns]
n_vars = len(labels)
if n_vars == 0:
return None
angles = np.linspace(0, 2 * np.pi, n_vars, endpoint=False)
angles = np.concatenate([angles, angles[:1]])
fig, ax = plt.subplots(subplot_kw={"projection": "polar"}, figsize=(7, 7))
cmap = plt.get_cmap("tab10")
for idx, weekday in enumerate(scores.index):
day_scores = scores.loc[weekday, var_cols]
if day_scores.isna().all():
continue
values = day_scores.to_numpy(dtype=float)
values = np.nan_to_num(values, nan=0.0)
values = np.concatenate([values, values[:1]])
color = cmap(idx % 10)
day_label = (
weekday_labels_long[int(weekday)]
if 0 <= int(weekday) < len(weekday_labels_long)
else str(weekday)
)
ax.plot(angles, values, marker="o", color=color, label=day_label)
ax.fill(angles, values, color=color, alpha=0.15)
ax.set_xticks(angles[:-1])
ax.set_xticklabels(labels)
ax.set_yticks([0.25, 0.5, 0.75, 1.0])
ax.set_ylim(0, 1.05)
ax.set_title("Profils radar des scores tous les jours")
ax.grid(True, linestyle=":", alpha=0.4)
ax.legend(loc="upper right", bbox_to_anchor=(1.35, 1.05), borderaxespad=0.0)
# Export des données brutes utilisées pour ce radar global
export_plot_dataset(scores[var_cols], output_path)
fig.tight_layout()
fig.savefig(output_path, dpi=150)
plt.close(fig)
return output_path.resolve()
def main() -> None:
if not CSV_PATH.exists():
print(f"⚠ Fichier introuvable : {CSV_PATH}")
@@ -56,7 +302,7 @@ def main() -> None:
output_path = plot_weekday_profiles(
weekday_df=weekday_means,
variables=variables,
output_path=OUTPUT_PATH,
output_path=OUTPUT_PROFILES_PATH,
title="Moyennes par jour de semaine",
)
@@ -73,7 +319,57 @@ def main() -> None:
unit = f" {var.unit}" if var.unit else ""
print(f"{var.label} maximale en moyenne le {best_label} (≈{best_value:.2f}{unit})")
# Calcul des scores normalisés et du score global.
scores = compute_weekday_scores(weekday_means, variables)
if not scores.empty and "overall_score" in scores.columns:
print()
print("Scores globaux (01) par jour de semaine :")
overall = scores["overall_score"]
for idx, label in enumerate(weekday_labels_long):
value = overall.get(idx)
if pd.isna(value):
continue
print(f" - {label:<9} : {value:.3f}")
best_idx = int(overall.idxmax())
best_label = weekday_labels_long[best_idx]
best_score = overall.max()
print()
print(
f"⇒ Jour le plus « agréable » au sens de ce score normalisé : "
f"{best_label} (score global ≈{best_score:.3f})."
)
# Graphique synthétique des scores globaux.
weekday_labels_short = ["Lun", "Mar", "Mer", "Jeu", "Ven", "Sam", "Dim"]
scores_path = plot_overall_weekday_score(
scores=scores,
weekday_labels=weekday_labels_short,
output_path=OUTPUT_SCORES_PATH,
)
if scores_path is not None:
print(f"✔ Graphique des scores globaux exporté : {scores_path}")
# Graphiques radar par jour.
radar_paths = plot_weekday_radars(
scores=scores,
variables=variables,
weekday_labels_long=weekday_labels_long,
output_dir=OUTPUT_RADAR_DIR,
)
if radar_paths:
print(f"✔ Graphiques radar exportés ({len(radar_paths)}) dans : {OUTPUT_RADAR_DIR}")
# Graphique radar global superposant tous les jours.
radar_all_path = plot_weekday_radar_all(
scores=scores,
variables=variables,
weekday_labels_long=weekday_labels_long,
output_path=DOC_DIR / "figures" / "weekday_radar_all.png",
)
if radar_all_path is not None:
print(f"✔ Graphique radar global exporté : {radar_all_path}")
if __name__ == "__main__":
main()