projet: commit initial
- Ajoute les données intérieures (chambre et pièce principale). - Ajoute les données de MétéoSuisse. - Crée l'environnement python et ajoute les dépendances. - Crée le site web avec Quarto. - Écrit la page qui analyse et présente les données pour le mois de juin 2026 qui comprend la canicule du 18 au 29 juin. Co-Authored-by: iGor milhit <igor@milhit.ch>main
commit
ef79de8c74
|
|
@ -0,0 +1,14 @@
|
||||||
|
# Python-generated files
|
||||||
|
__pycache__/
|
||||||
|
*.py[oc]
|
||||||
|
build/
|
||||||
|
dist/
|
||||||
|
wheels/
|
||||||
|
*.egg-info
|
||||||
|
|
||||||
|
# Virtual environments
|
||||||
|
.venv
|
||||||
|
|
||||||
|
/.quarto/
|
||||||
|
**/*.quarto_ipynb
|
||||||
|
_site
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
3.13
|
||||||
|
|
@ -0,0 +1,423 @@
|
||||||
|
---
|
||||||
|
title: Juin 2026
|
||||||
|
subtitle: Un épisode caniculaire précoce et intense
|
||||||
|
lang: fr
|
||||||
|
creation_date: 2026-06-27T08:20:03+0200
|
||||||
|
date: today
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
code-fold: true
|
||||||
|
code-summary: "Afficher le code"
|
||||||
|
execute:
|
||||||
|
echo: true
|
||||||
|
warning: false
|
||||||
|
cache: false
|
||||||
|
id: 20260627082003
|
||||||
|
tags: ["canicule", "température"]
|
||||||
|
---
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
#| label: setup
|
||||||
|
#| include: false
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import numpy as np
|
||||||
|
import matplotlib.pyplot as plt
|
||||||
|
import matplotlib.dates as mdates
|
||||||
|
import matplotlib.patches as mpatches
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
APPART_DATA_PIECE_PRINCIPALE = Path("data/2026-06-appart-data-piece-principale.csv")
|
||||||
|
APPART_DATA_CHAMBRE = Path("data/2026-06-appart-data-chambre.csv")
|
||||||
|
GVE_LOCAL = Path("data/gve_h_recent.csv")
|
||||||
|
|
||||||
|
def load_aranet(path):
|
||||||
|
"""Charge un CSV Aranet4 (2 lignes de header, decimal=',', champs vides tolérés)."""
|
||||||
|
raw = pd.read_csv(
|
||||||
|
path,
|
||||||
|
skiprows=2,
|
||||||
|
header=None,
|
||||||
|
names=["time", "co2", "temperature", "humidity", "pressure"],
|
||||||
|
quotechar='"',
|
||||||
|
dtype=str,
|
||||||
|
)
|
||||||
|
raw = raw.dropna(how="all")
|
||||||
|
raw["time"] = pd.to_datetime(raw["time"], format="%d/%m/%Y %H:%M:%S")
|
||||||
|
for col in ["co2", "temperature", "humidity", "pressure"]:
|
||||||
|
raw[col] = pd.to_numeric(
|
||||||
|
raw[col].str.replace(",", ".", regex=False),
|
||||||
|
errors="coerce", # champs vides → NaN, pas d'erreur
|
||||||
|
)
|
||||||
|
return raw.sort_values("time").reset_index(drop=True)
|
||||||
|
|
||||||
|
df = load_aranet(APPART_DATA_PIECE_PRINCIPALE)
|
||||||
|
df_c = load_aranet(APPART_DATA_CHAMBRE)
|
||||||
|
|
||||||
|
# Filtrage sur la période commune
|
||||||
|
fig_start = max(df["time"].min(), df_c["time"].min())
|
||||||
|
fig_end = min(df["time"].max(), df_c["time"].max())
|
||||||
|
|
||||||
|
# Dates en fr sans la locale
|
||||||
|
MOIS_FR = {
|
||||||
|
1: "janvier", 2: "février", 3: "mars", 4: "avril",
|
||||||
|
5: "mai", 6: "juin", 7: "juillet", 8: "août",
|
||||||
|
9: "septembre", 10: "octobre", 11: "novembre", 12: "décembre",
|
||||||
|
}
|
||||||
|
|
||||||
|
def fmt_datetime_fr(ts):
|
||||||
|
return f"{ts.day:02d} {MOIS_FR[ts.month]} {ts.year}, {ts.strftime('%H:%M')}"
|
||||||
|
|
||||||
|
# Période commune aux deux capteurs
|
||||||
|
## Période en chaîne de caractères pour le texte en Markdown.
|
||||||
|
def fmt_date_fr(ts):
|
||||||
|
"""Retourne '3 juin 2026' (sans zéro initial, sans heure)."""
|
||||||
|
return f"{ts.day} {MOIS_FR[ts.month]} {ts.year}"
|
||||||
|
|
||||||
|
period_start_str = fmt_date_fr(fig_start)
|
||||||
|
period_end_str = fmt_date_fr(fig_end)
|
||||||
|
|
||||||
|
## Période dans des variables pour les figures
|
||||||
|
period_start = max(df["time"].min(), df_c["time"].min())
|
||||||
|
period_end = min(df["time"].max(), df_c["time"].max())
|
||||||
|
|
||||||
|
ext = pd.read_csv(
|
||||||
|
GVE_LOCAL,
|
||||||
|
sep=";",
|
||||||
|
encoding="windows-1252",
|
||||||
|
dtype=str,
|
||||||
|
)
|
||||||
|
ext = ext.rename(columns={"reference_timestamp": "time"})
|
||||||
|
ext["time"] = (
|
||||||
|
pd.to_datetime(ext["time"], format="%d.%m.%Y %H:%M", utc=True)
|
||||||
|
.dt.tz_convert("Europe/Zurich")
|
||||||
|
.dt.tz_localize(None)
|
||||||
|
)
|
||||||
|
for col in ["tre200h0", "ure200h0"]:
|
||||||
|
if col in ext.columns:
|
||||||
|
ext[col] = pd.to_numeric(ext[col], errors="coerce")
|
||||||
|
|
||||||
|
rename_map = {"tre200h0": "temp_ext", "ure200h0": "rh_ext"}
|
||||||
|
ext = ext[["time"] + [c for c in rename_map if c in ext.columns]].rename(columns=rename_map)
|
||||||
|
|
||||||
|
# Filtre sur la période couverte par au moins df (pièce principale)
|
||||||
|
ext = ext[
|
||||||
|
(ext["time"] >= df["time"].min()) &
|
||||||
|
(ext["time"] <= df["time"].max())
|
||||||
|
].reset_index(drop=True)
|
||||||
|
|
||||||
|
print(f"Pièce principale : {len(df)} mesures — {df['time'].min().date()} → {df['time'].max().date()}")
|
||||||
|
print(f"Chambre : {len(df_c)} mesures — {df_c['time'].min().date()} → {df_c['time'].max().date()}")
|
||||||
|
print(f"Période commune : {period_start.date()} → {period_end.date()}")
|
||||||
|
print(f"MétéoSuisse GVE : {len(ext)} mesures — {ext['time'].min().date()} → {ext['time'].max().date()}")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Contexte
|
||||||
|
|
||||||
|
En 2026, il y avait déjà eu un épisode chaud en mai. Celui de juin a été plus
|
||||||
|
intense et plus long également. Officiellement, il a duré du 18 au 29 juin
|
||||||
|
2026.
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
#| label: stats
|
||||||
|
#| echo: false
|
||||||
|
|
||||||
|
from IPython.display import Markdown
|
||||||
|
|
||||||
|
# ── Pièce principale ─────────────────────────────────────────────────────────
|
||||||
|
tmax_val = df["temperature"].max()
|
||||||
|
tmax_time = fmt_datetime_fr(df.loc[df["temperature"].idxmax(), "time"])
|
||||||
|
tmin_val = df["temperature"].min()
|
||||||
|
tmin_time = fmt_datetime_fr(df.loc[df["temperature"].idxmin(), "time"])
|
||||||
|
jours_26 = df.groupby(df["time"].dt.date)["temperature"].max().ge(26).sum()
|
||||||
|
jours_28 = df.groupby(df["time"].dt.date)["temperature"].max().ge(28).sum()
|
||||||
|
|
||||||
|
# ── Chambre ──────────────────────────────────────────────────────────────────
|
||||||
|
c_tmax_val = df_c["temperature"].max()
|
||||||
|
c_tmax_time = fmt_datetime_fr(df_c.loc[df_c["temperature"].idxmax(), "time"])
|
||||||
|
c_tmin_val = df_c["temperature"].min()
|
||||||
|
c_tmin_time = fmt_datetime_fr(df_c.loc[df_c["temperature"].idxmin(), "time"])
|
||||||
|
c_jours_26 = df_c.groupby(df_c["time"].dt.date)["temperature"].max().ge(26).sum()
|
||||||
|
c_jours_28 = df_c.groupby(df_c["time"].dt.date)["temperature"].max().ge(28).sum()
|
||||||
|
|
||||||
|
# ── Extérieur MétéoSuisse ────────────────────────────────────────────────────
|
||||||
|
ext_tmax_val = ext["temp_ext"].max()
|
||||||
|
ext_tmax_time = fmt_datetime_fr(ext.loc[ext["temp_ext"].idxmax(), "time"])
|
||||||
|
ext_tmin_val = ext["temp_ext"].min()
|
||||||
|
|
||||||
|
nuits_trop = (
|
||||||
|
ext.groupby(ext["time"].dt.date)["temp_ext"]
|
||||||
|
.min()
|
||||||
|
.ge(20)
|
||||||
|
.sum()
|
||||||
|
)
|
||||||
|
|
||||||
|
Markdown(f"""
|
||||||
|
| Indicateur | Pièce principale | Chambre |
|
||||||
|
|---|---|---|
|
||||||
|
| **T maximale** | **{tmax_val:.1f} °C** — {tmax_time} | **{c_tmax_val:.1f} °C** — {c_tmax_time} |
|
||||||
|
| T minimale | {tmin_val:.1f} °C — {tmin_time} | {c_tmin_val:.1f} °C — {tmin_time} |
|
||||||
|
| Jours T max ≥ 26 °C | {jours_26} jours | {c_jours_26} jours |
|
||||||
|
| Jours T max ≥ 28 °C | {jours_28} jours | {c_jours_28} jours |
|
||||||
|
| **T extérieure max** (GVE) | **{ext_tmax_val:.1f} °C** — {ext_tmax_time} | — |
|
||||||
|
| T extérieure min (GVE) | {ext_tmin_val:.1f} °C | — |
|
||||||
|
| Nuits tropicales ext. (T min ≥ 20 °C) | {nuits_trop} nuits | — |
|
||||||
|
| Canicule officielle | 18 au 29 juin 2026 | — |
|
||||||
|
| CO₂ maximal mesuré | {df['co2'].max():.0f} ppm | {df_c['co2'].max():.0f} ppm |
|
||||||
|
""")
|
||||||
|
```
|
||||||
|
|
||||||
|
## Comparaison température intérieure et extérieure
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
#| label: fig-int-vs-ext
|
||||||
|
#| fig-cap: "Comparaison des températures intérieure et extérieure du 3 au 30 juin 2026. Zone orange : période de canicule officielle du 18 au 29 juin). Bandes violettes : nuits tropicales (T ext min ≥ 20 °C, 20h–6h). Source données météo : MétéoSuisse OGD."
|
||||||
|
|
||||||
|
RED_DARK = "#c62828" # pièce principale
|
||||||
|
BROWN = "#e65100" # chambre
|
||||||
|
BLUE = "#1565c0" # extérieur
|
||||||
|
CANICULE = "#ffe0b2"
|
||||||
|
NUIT_TROP = "#ede7f6"
|
||||||
|
|
||||||
|
df_fig = df[(df["time"] >= fig_start) & (df["time"] <= fig_end)]
|
||||||
|
dfc_fig = df_c[(df_c["time"] >= fig_start) & (df_c["time"] <= fig_end)]
|
||||||
|
ext_fig = ext[(ext["time"] >= fig_start) & (ext["time"] <= fig_end)]
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(14, 5))
|
||||||
|
fig.patch.set_facecolor("white")
|
||||||
|
|
||||||
|
# ── 1. Période de canicule officielle ────────────────────────────────────────
|
||||||
|
canicule_start = pd.Timestamp("2026-06-18 00:00")
|
||||||
|
canicule_end = pd.Timestamp("2026-06-29 23:59")
|
||||||
|
ax.axvspan(canicule_start, canicule_end,
|
||||||
|
color=CANICULE, alpha=0.55, zorder=0,
|
||||||
|
label="Période de canicule (plan cantonal GE, du 18 au 29 juin)")
|
||||||
|
|
||||||
|
# ── 2. Nuits tropicales ──────────────────────────────────────────────────────
|
||||||
|
ext_tmp = ext_fig.copy()
|
||||||
|
ext_tmp["night_date"] = ext_tmp["time"].dt.date
|
||||||
|
mask_ev = ext_tmp["time"].dt.hour >= 20
|
||||||
|
ext_tmp.loc[mask_ev, "night_date"] = (
|
||||||
|
ext_tmp.loc[mask_ev, "time"] + pd.Timedelta(days=1)
|
||||||
|
).dt.date
|
||||||
|
mask_n = (ext_tmp["time"].dt.hour >= 20) | (ext_tmp["time"].dt.hour < 6)
|
||||||
|
|
||||||
|
# Date maximale couverte par les données extérieures
|
||||||
|
ext_last_date = ext_fig["time"].max().date()
|
||||||
|
|
||||||
|
nuits_dates = (
|
||||||
|
ext_tmp[mask_n]
|
||||||
|
.groupby("night_date")["temp_ext"]
|
||||||
|
.min()
|
||||||
|
.pipe(lambda s: s[s >= 20])
|
||||||
|
# Exclure les nuits dont la tranche matinale (0h–6h) n'est pas couverte :
|
||||||
|
# night_date correspond au matin de la nuit → doit être ≤ dernière date disponible
|
||||||
|
.pipe(lambda s: s[pd.to_datetime(s.index) <= pd.Timestamp(ext_last_date)])
|
||||||
|
.index
|
||||||
|
)
|
||||||
|
|
||||||
|
first_nuit = True
|
||||||
|
for nuit in nuits_dates:
|
||||||
|
nuit_ts = pd.Timestamp(nuit)
|
||||||
|
span_start = nuit_ts - pd.Timedelta(hours=4)
|
||||||
|
span_end = nuit_ts + pd.Timedelta(hours=6)
|
||||||
|
label = "Nuit tropicale (T ext min ≥ 20 °C)" if first_nuit else ""
|
||||||
|
ax.axvspan(span_start, span_end,
|
||||||
|
color=NUIT_TROP, alpha=0.85, zorder=1, label=label)
|
||||||
|
first_nuit = False
|
||||||
|
|
||||||
|
# ── 3. Courbes de température ────────────────────────────────────────────────
|
||||||
|
ax.plot(dfc_fig["time"], dfc_fig["temperature"],
|
||||||
|
color=BROWN, linewidth=0.8, alpha=0.75, zorder=3,
|
||||||
|
label="T intérieure chambre (Aranet4, 5 min)")
|
||||||
|
ax.plot(df_fig["time"], df_fig["temperature"],
|
||||||
|
color=RED_DARK, linewidth=0.8, alpha=0.9, zorder=4,
|
||||||
|
label="T intérieure pièce principale (Aranet4, 5 min)")
|
||||||
|
ax.plot(ext_fig["time"], ext_fig["temp_ext"],
|
||||||
|
color=BLUE, linewidth=1.3, zorder=5,
|
||||||
|
marker="o", markersize=1.8, alpha=0.85,
|
||||||
|
label="T extérieure (MétéoSuisse GVE, horaire)")
|
||||||
|
ax.set_xlim(fig_start, fig_end)
|
||||||
|
|
||||||
|
# ── 4. Seuil nuit tropicale ──────────────────────────────────────────────────
|
||||||
|
ax.axhline(20, color="#7b1fa2", lw=0.8, ls=":", alpha=0.7,
|
||||||
|
label="Seuil nuit tropicale (20 °C)")
|
||||||
|
|
||||||
|
# ── 5. Habillage ─────────────────────────────────────────────────────────────
|
||||||
|
ax.yaxis.set_ticks_position("both")
|
||||||
|
ax.tick_params(axis="y", right=True, labelright=True, labelsize=9)
|
||||||
|
ax.set_ylabel("Température (°C)", fontsize=10)
|
||||||
|
ax.set_ylim(10, 38)
|
||||||
|
ax.legend(loc="upper left", fontsize=8, framealpha=0.9)
|
||||||
|
ax.set_title(
|
||||||
|
"T intérieure (8e étage, exp. SSE) vs T extérieure (GVE – Cointrin)\n"
|
||||||
|
"Source : Aranet4 / MétéoSuisse OGD — 3–30 juin 2026",
|
||||||
|
fontsize=10
|
||||||
|
)
|
||||||
|
ax.grid(axis="y", ls=":", alpha=0.4)
|
||||||
|
|
||||||
|
ax.xaxis.set_major_locator(mdates.DayLocator(interval=2))
|
||||||
|
ax.xaxis.set_minor_locator(mdates.DayLocator(interval=1))
|
||||||
|
ax.xaxis.set_major_formatter(mdates.DateFormatter("%-d %b"))
|
||||||
|
plt.setp(ax.xaxis.get_majorticklabels(), rotation=0, ha="center", fontsize=9)
|
||||||
|
|
||||||
|
fig.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
## Pratique d'aération et limites physiques
|
||||||
|
|
||||||
|
La stratégie appliquée est optimale dans les contraintes du logement :
|
||||||
|
les fenêtres sont ouvertes **le soir dès que la température extérieure
|
||||||
|
rejoint la température intérieure** (croisement des courbes), et maintenues
|
||||||
|
ouvertes pendant la nuit.
|
||||||
|
|
||||||
|
Le matin, dès que le rayonnement solaire direct atteint les vitrages (vers
|
||||||
|
6h–7h en juin pour une exposition Sud-Sud-Est), fenêtres et stores sont
|
||||||
|
fermés. Cette fermeture est visible sur le graphique : la courbe intérieure
|
||||||
|
repart à la hausse alors que la courbe extérieure est encore basse ou en
|
||||||
|
descente.
|
||||||
|
|
||||||
|
**Les stores ne constituent pas une protection suffisante.** De couleur
|
||||||
|
sombre et en métal, ils absorbent et accumulent le rayonnement solaire.
|
||||||
|
Cette chaleur est ensuite diffusée vers l'intérieur par conduction à travers
|
||||||
|
les cadres de fenêtres et les vitrages, dont l'isolation thermique est
|
||||||
|
insuffisante. La fermeture des stores ralentit la montée en température
|
||||||
|
mais ne l'empêche pas.
|
||||||
|
|
||||||
|
|
||||||
|
```{python}
|
||||||
|
#| label: fig-zoom-croisements
|
||||||
|
#| fig-cap: "Zoom sur la fin de la canicule, 26–28 juin 2026. Croisement vespéral : la température extérieure (en baisse) rejoint la température intérieure → ouverture des fenêtres et stores. Divergence matinale : la température intérieure repart à la hausse sous l'effet du rayonnement solaire sur les vitrages et stores métalliques, alors que la température extérieure est encore basse. Zones grises : tranches nocturnes (20h–6h)."
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
from matplotlib.ticker import FuncFormatter
|
||||||
|
|
||||||
|
JOURS_FR = {0: "Lun", 1: "Mar", 2: "Mer", 3: "Jeu",
|
||||||
|
4: "Ven", 5: "Sam", 6: "Dim"}
|
||||||
|
|
||||||
|
def fmt_date_fr_court(ts):
|
||||||
|
return f"{JOURS_FR[ts.weekday()]} {ts.day} {MOIS_FR[ts.month]}"
|
||||||
|
|
||||||
|
zoom_start = pd.Timestamp("2026-06-26 00:00")
|
||||||
|
zoom_end = pd.Timestamp("2026-06-28 23:59")
|
||||||
|
|
||||||
|
dz = df[(df["time"] >= zoom_start) & (df["time"] <= zoom_end)].copy().reset_index(drop=True)
|
||||||
|
dz_c = df_c[(df_c["time"] >= zoom_start) & (df_c["time"] <= zoom_end)].copy()
|
||||||
|
ez = ext[(ext["time"] >= zoom_start) & (ext["time"] <= zoom_end)].copy()
|
||||||
|
|
||||||
|
# Interpolation T extérieure sur la grille 5 min de l'Aranet
|
||||||
|
temp_ext_interp = (
|
||||||
|
ez.set_index("time")["temp_ext"]
|
||||||
|
.reindex(ez.set_index("time")["temp_ext"].index.union(dz["time"]))
|
||||||
|
.interpolate("index")
|
||||||
|
.reindex(dz["time"])
|
||||||
|
.values
|
||||||
|
)
|
||||||
|
|
||||||
|
diff = dz["temperature"].values - temp_ext_interp
|
||||||
|
sign_changes = np.where(np.diff(np.sign(diff)))[0]
|
||||||
|
|
||||||
|
# Détection des fermetures : minimum local de T intérieure entre 5h et 10h
|
||||||
|
# (on cherche le moment où la courbe intérieure cesse de descendre et repart)
|
||||||
|
dz["hour"] = dz["time"].dt.hour
|
||||||
|
dz["diff_temp"] = dz["temperature"].diff() # dérivée discrète
|
||||||
|
|
||||||
|
fig, ax = plt.subplots(figsize=(14, 5))
|
||||||
|
fig.patch.set_facecolor("white")
|
||||||
|
|
||||||
|
# Fond canicule
|
||||||
|
ax.axvspan(zoom_start, zoom_end, color="#ffe0b2", alpha=0.35, zorder=0)
|
||||||
|
|
||||||
|
# Zones nocturnes (20h–6h)
|
||||||
|
for day_offset in range(4):
|
||||||
|
night_s = zoom_start + pd.Timedelta(days=day_offset, hours=20)
|
||||||
|
night_e = night_s + pd.Timedelta(hours=10)
|
||||||
|
ax.axvspan(night_s, night_e, color="#eceff1", alpha=0.6, zorder=0)
|
||||||
|
|
||||||
|
# Courbes
|
||||||
|
ax.plot(dz_c["time"], dz_c["temperature"],
|
||||||
|
color=BROWN, linewidth=0.8, alpha=0.75, zorder=3,
|
||||||
|
linestyle="--", label="T intérieure chambre (Aranet4)")
|
||||||
|
ax.plot(dz["time"], dz["temperature"],
|
||||||
|
color=RED_DARK, linewidth=1.3, zorder=4,
|
||||||
|
label="T intérieure pièce principale (Aranet4)")
|
||||||
|
ax.plot(ez["time"], ez["temp_ext"],
|
||||||
|
color=BLUE, linewidth=1.5, zorder=5,
|
||||||
|
marker="o", markersize=2.5, label="T extérieure (MétéoSuisse GVE)")
|
||||||
|
ax.set_xlim(zoom_start, zoom_end)
|
||||||
|
|
||||||
|
# ── Annotations ouvertures (croisement ext descend sous int, soirée) ─────────
|
||||||
|
annotated_open = 0
|
||||||
|
for idx in sign_changes:
|
||||||
|
if idx + 1 >= len(dz):
|
||||||
|
continue
|
||||||
|
s_before = np.sign(diff[idx])
|
||||||
|
s_after = np.sign(diff[idx + 1])
|
||||||
|
hour = dz["time"].iloc[idx].hour
|
||||||
|
if s_before < 0 and s_after > 0 and 17 <= hour <= 23 and annotated_open < 4:
|
||||||
|
ax.annotate(
|
||||||
|
"ouverture",
|
||||||
|
xy=(dz["time"].iloc[idx], dz["temperature"].iloc[idx]),
|
||||||
|
xytext=(0, 25), textcoords="offset points",
|
||||||
|
fontsize=7.5, color=BLUE, ha="center",
|
||||||
|
arrowprops=dict(arrowstyle="->", color=BLUE, lw=0.8),
|
||||||
|
)
|
||||||
|
annotated_open += 1
|
||||||
|
|
||||||
|
# ── Annotations fermetures : minimum local de T int entre 5h et 10h ──────────
|
||||||
|
# Par jour, on cherche l'indice du minimum de T intérieure dans la fenêtre 5h–10h
|
||||||
|
annotated_solar = 0
|
||||||
|
for day_offset in range(4):
|
||||||
|
day = zoom_start.date() + pd.Timedelta(days=day_offset)
|
||||||
|
mask = (
|
||||||
|
(dz["time"].dt.date == day) &
|
||||||
|
(dz["hour"] >= 5) &
|
||||||
|
(dz["hour"] <= 10)
|
||||||
|
)
|
||||||
|
subset = dz[mask]
|
||||||
|
if subset.empty:
|
||||||
|
continue
|
||||||
|
# minimum de T intérieure dans la fenêtre → point de divergence
|
||||||
|
idx_min = subset["temperature"].idxmin()
|
||||||
|
t_close = dz.loc[idx_min, "time"]
|
||||||
|
y_close = dz.loc[idx_min, "temperature"]
|
||||||
|
if annotated_solar < 4:
|
||||||
|
ax.annotate(
|
||||||
|
"fermeture",
|
||||||
|
xy=(t_close, y_close),
|
||||||
|
xytext=(0, -32), textcoords="offset points",
|
||||||
|
fontsize=7.5, color=RED_DARK, ha="center",
|
||||||
|
arrowprops=dict(arrowstyle="->", color=RED_DARK, lw=0.8),
|
||||||
|
)
|
||||||
|
annotated_solar += 1
|
||||||
|
|
||||||
|
# ── Habillage ─────────────────────────────────────────────────────────────────
|
||||||
|
ax.yaxis.set_ticks_position("both")
|
||||||
|
ax.tick_params(axis="y", right=True, labelright=True, labelsize=9)
|
||||||
|
ax.set_ylabel("Température (°C)", fontsize=10)
|
||||||
|
ax.set_ylim(19, 37)
|
||||||
|
ax.legend(loc="upper left", fontsize=8, framealpha=0.9)
|
||||||
|
ax.set_title(
|
||||||
|
"Zoom canicule 26–28 juin 2026 — croisements T intérieure / T extérieure\n"
|
||||||
|
"Zones grises : tranches nocturnes (20h–6h)",
|
||||||
|
fontsize=10
|
||||||
|
)
|
||||||
|
ax.grid(axis="y", ls=":", alpha=0.4)
|
||||||
|
|
||||||
|
# Axe X : jours en français, heures intermédiaires avec label
|
||||||
|
ax.xaxis.set_major_locator(mdates.DayLocator(interval=1))
|
||||||
|
ax.xaxis.set_major_formatter(FuncFormatter(
|
||||||
|
lambda x, _: fmt_date_fr_court(mdates.num2date(x))
|
||||||
|
))
|
||||||
|
ax.xaxis.set_minor_locator(mdates.HourLocator(byhour=[6, 12, 18]))
|
||||||
|
ax.xaxis.set_minor_formatter(mdates.DateFormatter("%Hh"))
|
||||||
|
plt.setp(ax.xaxis.get_minorticklabels(), fontsize=7, color="#888888")
|
||||||
|
plt.setp(ax.xaxis.get_majorticklabels(), rotation=0, ha="center", fontsize=9)
|
||||||
|
|
||||||
|
fig.tight_layout()
|
||||||
|
plt.show()
|
||||||
|
```
|
||||||
|
|
||||||
|
<!-- références -->
|
||||||
|
|
||||||
|
[1]: https://aranet.com/en/home/products/aranet4-home "Appareil sur le site du fournisseur"
|
||||||
|
|
@ -0,0 +1,24 @@
|
||||||
|
project:
|
||||||
|
type: website
|
||||||
|
|
||||||
|
website:
|
||||||
|
title: "Appartemp"
|
||||||
|
navbar:
|
||||||
|
left:
|
||||||
|
- href: index.qmd
|
||||||
|
text: Accueil
|
||||||
|
- href: 2026-06.qmd
|
||||||
|
text: "Juin 2026"
|
||||||
|
- href: about.qmd
|
||||||
|
text: À propos
|
||||||
|
|
||||||
|
format:
|
||||||
|
html:
|
||||||
|
theme:
|
||||||
|
- cosmo
|
||||||
|
- brand
|
||||||
|
css: styles.css
|
||||||
|
toc: true
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 115 KiB |
|
|
@ -0,0 +1,64 @@
|
||||||
|
---
|
||||||
|
title: Un appartement, des températures
|
||||||
|
subtitle: Contexte et mesure
|
||||||
|
lang: fr
|
||||||
|
creation_date: 2026-06-27T08:20:00+0200
|
||||||
|
date: today
|
||||||
|
id: 20260627082000
|
||||||
|
---
|
||||||
|
|
||||||
|
## Appartement
|
||||||
|
|
||||||
|
Il s'agit d'un site web qui cherche à documenter comment se comporte la
|
||||||
|
température dans un appartement. Il s'agit d'un appartement d'environ 45 m^2^,
|
||||||
|
au 8^e^ étage, exposé approximativement Sud-Sud-Est. Il n'est pas traversant et
|
||||||
|
la grande partie de la façade est constituée de fenêtres relativement peu
|
||||||
|
isolées. C'est bien un double vitrage, mais le cadre des fenêtre laisse passer
|
||||||
|
l'air et le bruit.
|
||||||
|
|
||||||
|
Le plan de l'appartement :
|
||||||
|
|
||||||
|
![Plan de l'appartement][2]
|
||||||
|
|
||||||
|
## Mesures
|
||||||
|
|
||||||
|
Les mesures intérieures sont prises par deux appareils [Aranet4][3], chaque 5
|
||||||
|
minutes, un appareil par pièce. Ils sont posés sur des étagères, environ à
|
||||||
|
1.20 m du sol, plutôt vers le mur intérieur de l'appartement, et non exposé au
|
||||||
|
soleil direct. J'utilise la fonction d'export au format CSV des données,
|
||||||
|
lorsque je veux traiter celles-ci. Ce n'est pas fait automatiquement.
|
||||||
|
|
||||||
|
Ces appareils mesurent :
|
||||||
|
|
||||||
|
- La teneur en CO^2^.
|
||||||
|
- La température.
|
||||||
|
- L'humidité relative.
|
||||||
|
- La pression atmosphérique.
|
||||||
|
|
||||||
|
Lorsque j'utilise des données de température extérieure, je me base sur celles
|
||||||
|
mises à disposition par MétéoSuisse, pour la station de Genève, à savoir
|
||||||
|
l'aéroport Cornavin. Ce sont des données officielles, fiables. Mais c'est assez
|
||||||
|
éloigné de l'appartement. Mon hypothèse non vérifiée est que la température
|
||||||
|
autour de l'appartement est vraisemblablement un peu moins élevée lors des
|
||||||
|
grandes chaleurs. Je n'ai aucune idée s'il y a vraiment une différence, et de
|
||||||
|
son importance éventuelle.
|
||||||
|
|
||||||
|
Mais les données de MétéoSuisse me semblent une bonne base. Je les récupère
|
||||||
|
avec la commande suivante, `gve` étant le code de la station :
|
||||||
|
|
||||||
|
```bash
|
||||||
|
curl -o gve_h_recent.csv https://data.geo.admin.ch/ch.meteoschweiz.ogd-smn/gve/ogd-smn_gve_h_recent.csv
|
||||||
|
```
|
||||||
|
|
||||||
|
## Périodes traitées
|
||||||
|
|
||||||
|
Pour le moment, il n'y a qu'un seul épisode qui est documenté :
|
||||||
|
|
||||||
|
- [Juin 2026][1].
|
||||||
|
|
||||||
|
<!-- références -->
|
||||||
|
|
||||||
|
[1]: 2026-06.qmd
|
||||||
|
[2]: ./img/appart.jpg
|
||||||
|
[3]: https://aranet.com/en/home/products/aranet4-home "Appareil sur le site du fournisseur"
|
||||||
|
|
||||||
|
|
@ -0,0 +1,14 @@
|
||||||
|
[project]
|
||||||
|
name = "appartemp"
|
||||||
|
version = "0.1.0"
|
||||||
|
description = "Add your description here"
|
||||||
|
readme = "README.md"
|
||||||
|
requires-python = ">=3.13"
|
||||||
|
dependencies = [
|
||||||
|
"jupyter>=1.1.1",
|
||||||
|
"jupyter-cache>=1.0.1",
|
||||||
|
"matplotlib>=3.11.0",
|
||||||
|
"numpy>=2.5.0",
|
||||||
|
"pandas>=3.0.3",
|
||||||
|
"scipy>=1.18.0",
|
||||||
|
]
|
||||||
|
|
@ -0,0 +1 @@
|
||||||
|
/* css styles */
|
||||||
Loading…
Reference in New Issue