Juin 2026

Un épisode caniculaire précoce et intense

Date de publication

1 juillet 2026

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.

Indicateur Pièce principale Chambre
T maximale 31.7 °C — 28 juin 2026, 19:12 31.2 °C — 29 juin 2026, 16:26
T minimale 18.8 °C — 11 juin 2026, 06:17 19.3 °C — 11 juin 2026, 06:17
Jours T max ≥ 26 °C 16 jours 14 jours
Jours T max ≥ 28 °C 13 jours 11 jours
T extérieure max (GVE) 36.7 °C — 27 juin 2026, 18:00
T extérieure min (GVE) 9.4 °C
Nuits tropicales ext. (T min ≥ 20 °C) 5 nuits
Canicule officielle 18 au 29 juin 2026
CO₂ maximal mesuré 1054 ppm 2172 ppm

Comparaison température intérieure et extérieure

Afficher le code
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()
Figure 1: 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.

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.

Afficher le code
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()
Figure 2: 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).