1

Détermination objective du meilleur jour de la semaine

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

4
.gitignore vendored
View File

@ -2,4 +2,6 @@
.env
data
__pycache__
.DS_Store
.DS_Store
blog/
scripts/build_blog_from_docs.py

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

After

Width:  |  Height:  |  Size: 245 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 110 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 93 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

View File

@ -55,7 +55,28 @@ Pour finir sur une note plus légère, un petit script de curiosité :
python "docs/12 - Conclusion/scripts/plot_weekday_profiles.py"
```
calcule les moyennes de température, dhumidité et de luminance pour chaque jour de la semaine et produit le graphique cidessous.
Dans mon jeu de données, cest le samedi qui ressort comme le jour « le plus agréable » : en moyenne un peu plus chaud, un peu plus lumineux, et moins humide que ses voisins… ce qui tombe plutôt bien pour organiser les activités du weekend.
calcule les moyennes de température, dhumidité, de pression atmosphérique, de luminance et de vitesse du vent pour chaque jour de la semaine, puis normalise ces valeurs pour attribuer un score de « confort » entre 0 et 1 à chaque jour : plus chaud et lumineux, moins humide et moins venteux, et une pression jugée « confortable » quand elle reste proche des valeurs habituelles plutôt quen situation très basse ou très élevée.
Il produit dabord le profil moyen par jour de semaine cidessous, puis un graphique de score global qui met en évidence le jour objectivement le plus favorable selon ces critères.
Enfin, une petite série de graphiques radar montre, pour chaque jour, comment se répartissent les scores des différentes variables (température, humidité, pression, lumière, vent).
![Profils moyens par jour de semaine](./figures/weekday_profiles.png)
![](./figures/weekday_radars/weekday_radar_0.png)
![](./figures/weekday_radars/weekday_radar_1.png)
![](./figures/weekday_radars/weekday_radar_2.png)
![](./figures/weekday_radars/weekday_radar_3.png)
![](./figures/weekday_radars/weekday_radar_4.png)
![](./figures/weekday_radars/weekday_radar_5.png)
![](./figures/weekday_radars/weekday_radar_6.png)
![](./figures/weekday_radar_all.png)
![Score global par jour de semaine](./figures/weekday_scores.png)
Objectivement, le meilleur jour de la semaine par chez moi est le vendredi !

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()