Températures italiennes, 2006-2023 : une histoire de données climatiques

D’un classeur ISTAT au format large à des données au format long, des estimations de tendance et des graphiques Matplotlib adaptés au style du blog pour les chefs-lieux de province italiens.
data science
python
climat
visualisation de données
Auteur·rice

Federico Viscioletti

Date de publication

2 juillet 2026

Introduction

L’Italie rassemble des climats alpins, continentaux et méditerranéens sur une péninsule étroite. C’est donc un terrain idéal pour observer comment le réchauffement et l’évolution des précipitations se manifestent dans des contextes locaux très différents.

Dans cet article, j’utilise le classeur météoclimatique d’ISTAT, mis à jour pour les chefs-lieux de province, afin de répondre à quelques questions simples :

  • À quelle vitesse les chefs-lieux de province italiens se réchauffent-ils ?
  • Quels lieux sont les plus chauds en moyenne ?
  • Les précipitations évoluent-elles dans la même direction ?
  • Comment l’année 2023 se situe-t-elle par rapport aux références récentes et historiques ?

Le classeur source est riche, mais sa structure est peu commode. Comme souvent avec les statistiques officielles, le plus difficile n’est pas de tracer les graphiques : c’est de transformer une mise en page Excel pensée pour la lecture humaine en données exploitables au format long.

Une note pour les futurs spécialistes des données

L’une des compétences les plus utiles en science des données est la capacité à transformer des données brutes en informations claires et exploitables. Les instituts statistiques nationaux, comme ISTAT en Italie, Eurostat ou le US Census Bureau, proposent de nombreux jeux de données gratuits et de qualité pour expérimenter et produire des analyses.

Comme vous le verrez ici, la barrière technique est en réalité assez basse. Une fois que l’on sait manipuler des classeurs structurés et réaliser une analyse exploratoire de base, on peut faire émerger des histoires dans les domaines qui nous passionnent, du changement climatique à l’économie ou aux tendances sociales. J’espère que ce notebook servira de feuille de route pour vos propres explorations.

Cet article reconstruit l’analyse originale sous forme de notebook à partir du classeur ISTAT mis à jour pour les chefs-lieux de province. Il couvre les températures et précipitations annuelles de 2006 à 2023, ainsi que les tableaux d’anomalies pour 2023.

Si ce travail vous est utile, vous pouvez attribuer une étoile au dépôt GitHub associé.

Charger le classeur

Les feuilles principales pour l’analyse sont :

  • Tavola_1 : température moyenne annuelle par chef-lieu de province, 2006-2023
  • Tavola_2 : précipitations annuelles par chef-lieu de province, 2006-2023
  • Tavola_3 : anomalies de température de 2023 par rapport à 2006-2015 et aux normales climatologiques
import matplotlib
import numpy as np
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go
import plotly.io as pio
from pathlib import Path

pio.renderers.default = "notebook_connected"
pio.renderers["notebook_connected"].include_plotlyjs = False

FILE_PATH = (
    "Tavole-_Dati-Meteoclimatici_Capoluoghi-Provincia_Anno-2023-e-serie-2006-2023.xlsx"
)

# --- Palette du design system du blog (voir design-system.html) ----------
BLUE = "#2780e3"
CHARCOAL = "#343a40"
INK = "#1e1e1e"
TEXT = "#343a40"
MUTED = "rgba(52, 58, 64, 0.72)"
MUTED_HEX = "#6c757d"
LINE = "#dee2e6"
SOFT = "#f8f9fa"
SOFT_BLUE = "#d4e6f9"
CODE = "#7d12ba"

matplotlib.rcParams.update(
    {
        "font.family": "sans-serif",
        "font.size": 12,
        "text.color": TEXT,
        "axes.labelcolor": TEXT,
        "axes.edgecolor": LINE,
        "axes.titlecolor": TEXT,
        "axes.titleweight": "normal",
        "axes.titlesize": 16,
        "axes.spines.top": False,
        "axes.spines.right": False,
        "axes.grid": True,
        "grid.color": LINE,
        "grid.linewidth": 0.8,
        "grid.linestyle": "-",
        "axes.facecolor": "#ffffff",
        "figure.facecolor": "#ffffff",
        "xtick.color": MUTED_HEX,
        "ytick.color": MUTED_HEX,
        "axes.linewidth": 1.0,
        "legend.frameon": False,
        "legend.fontsize": 11,
        "figure.dpi": 130,
    }
)

xls = pd.ExcelFile(FILE_PATH)
xls.sheet_names
['Indice',
 'Tavola_1',
 'Tavola_2',
 'Tavola_3',
 'Tavola_4',
 'Tavola_5',
 'Tavola_6',
 'Tavola_7',
 'Tavola_8',
 'Tavola_9',
 'Tavola_10',
 'Tavola_11',
 'Tavola_12',
 'Tavola_13',
 'Tavola_14',
 'Tavola_14 segue',
 'Tavola_15']

Du format large au format long

Les tableaux annuels sont stockés au format large : une ligne par ville et une colonne par année. Ce format est pratique pour l’inspection visuelle, mais peu adapté à l’analyse. Un petit parseur permet de convertir chaque feuille au format long, tout en gérant le marqueur de valeurs manquantes du fichier source (....).

def parse_table(sheet_name, value_name):
    df = pd.read_excel(FILE_PATH, sheet_name=sheet_name, header=None)

    years = [int(x) for x in df.iloc[3, 1:19].tolist()]
    data = df.iloc[4:, :19].copy()
    data.columns = ["Province"] + years

    data = data[data["Province"].notna()]
    data = data[
        ~data["Province"]
        .astype(str)
        .str.contains(
            r"Fonte|Totale|Nota|Nelle tavole|^\s*$",
            regex=True,
            na=False,
        )
    ]

    long = data.melt(id_vars="Province", var_name="Year", value_name=value_name)
    long["Province"] = long["Province"].astype(str).str.strip()
    long["Year"] = pd.to_numeric(long["Year"], errors="coerce")
    long[value_name] = pd.to_numeric(
        long[value_name].replace("....", np.nan),
        errors="coerce",
    )
    return long


temperature_data = parse_table("Tavola_1", "Temperature")
precipitation_data = parse_table("Tavola_2", "Precipitation")

italy_weather_data = temperature_data.merge(
    precipitation_data,
    on=["Province", "Year"],
    how="inner",
)

italy_weather_data.head()
Province Year Temperature Precipitation
0 Torino 2006 15.0 697.2
1 Vercelli 2006 13.2 650.4
2 Novara 2006 14.3 664.0
3 Cuneo 2006 13.0 733.2
4 Asti 2006 13.7 517.8
import warnings
warnings.simplefilter(action='ignore', category=FutureWarning)

Une vérification rapide confirme que le jeu de données nettoyé couvre bien toute la période 2006-2023.

italy_weather_data.agg(
    provinces=("Province", "nunique"),
    start_year=("Year", "min"),
    end_year=("Year", "max"),
)
Province Year
provinces 110.0 NaN
start_year NaN 2006.0
end_year NaN 2023.0

Tendances nationales

Une première vue naturelle consiste à calculer la moyenne annuelle sur l’ensemble des chefs-lieux de province. Cela résume la variation locale en une synthèse nationale et rend la direction du changement beaucoup plus lisible.

def linear_slope(df, y_col):
    clean = df.dropna(subset=[y_col]).copy()
    x = clean["Year"].to_numpy(dtype=float)
    y = clean[y_col].to_numpy(dtype=float)
    if len(x) < 2:
        return np.nan
    return float(np.polyfit(x, y, 1)[0])


italy_temperature_trend = (
    italy_weather_data.groupby("Year", as_index=False)["Temperature"].mean().dropna()
)

temp_coef = np.polyfit(
    italy_temperature_trend["Year"].to_numpy(dtype=float),
    italy_temperature_trend["Temperature"].to_numpy(dtype=float),
    1,
)
italy_temperature_trend["Trend"] = (
    temp_coef[0] * italy_temperature_trend["Year"] + temp_coef[1]
)

national_temp_slope = linear_slope(italy_temperature_trend, "Temperature")
national_temp_change = national_temp_slope * (2023 - 2006)

national_temp_slope, national_temp_change
(0.05477762707301679, 0.9312196602412854)

Sur l’ensemble des chefs-lieux de province, le taux de réchauffement estimé est d’environ 0,055 °C par an, soit environ 0,9 à 1,0 °C sur toute la période. Cela ne signifie pas que chaque ville se réchauffe exactement au même rythme, mais cela pose le contexte national pour le reste de l’analyse.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
from matplotlib.ticker import MaxNLocator

def parse_table(sheet_name, value_name):
    df = pd.read_excel(FILE_PATH, sheet_name=sheet_name, header=None)
    years = [int(x) for x in df.iloc[3, 1:19].tolist()]
    data = df.iloc[4:, :19].copy()
    data.columns = ["Province"] + years
    data = data[data["Province"].notna()]
    data["Province"] = data["Province"].astype(str).str.strip()
    data = data[~data["Province"].str.contains(r"Fonte|Totale|Nota|Nelle tavole|^$", regex=True, na=False)]
    long = data.melt(id_vars="Province", var_name="Year", value_name=value_name)
    long["Year"] = pd.to_numeric(long["Year"], errors="coerce")
    long[value_name] = pd.to_numeric(long[value_name].replace("....", np.nan), errors="coerce")
    return long

italy_mean = italy_weather_data.groupby("Year", as_index=False)["Temperature"].mean().dropna()
coef = np.polyfit(italy_mean["Year"], italy_mean["Temperature"], 1)
italy_mean["Trend"] = coef[0] * italy_mean["Year"] + coef[1]

BG = "#f7f6f2"; PANEL = "#f9f8f5"; TEXT = "#28251d"; MUTED = "#7a7974"; GRID = "#dcd9d5"; ACCENT = "#01696f"

plt.rcParams.update({
    "figure.facecolor": BG, "axes.facecolor": PANEL, "axes.edgecolor": GRID, "axes.labelcolor": TEXT,
    "axes.titlecolor": TEXT, "xtick.color": MUTED, "ytick.color": MUTED, "text.color": TEXT,
    "font.size": 11, "axes.titlesize": 18, "axes.titleweight": "bold", "legend.frameon": False
})

fig, ax = plt.subplots(figsize=(10.5, 5.8))
ax.plot(italy_mean["Year"], italy_mean["Temperature"], color=ACCENT, linewidth=2.8, marker="o", markersize=5.5, label="Moyenne annuelle", zorder=3)
ax.plot(italy_mean["Year"], italy_mean["Trend"], color=TEXT, linewidth=1.8, linestyle=(0, (4, 4)), alpha=0.7, label="Tendance linéaire", zorder=2)

# Titre avec marge, puis sous-titre juste en dessous
ax.set_title("Température annuelle moyenne des chefs-lieux de province italiens", loc="left", pad=38)
ax.text(0, 1.02, "Moyenne nationale des températures annuelles par chef-lieu de province, 2006-2023", transform=ax.transAxes, fontsize=10.5, color=MUTED)

ax.set_xlabel("Année"); ax.set_ylabel("Température (°C)")
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.set_xticks(italy_mean["Year"][::2])
ax.grid(axis="y", color=GRID, linewidth=0.8, alpha=0.85)
ax.grid(axis="x", visible=False)
for spine in ["top", "right"]: ax.spines[spine].set_visible(False)
ax.spines["left"].set_color(GRID); ax.spines["bottom"].set_color(GRID)
ax.legend(loc="upper left", ncol=2)

plt.tight_layout()
plt.show()
Figure 1: Température annuelle moyenne dans les chefs-lieux de province italiens, 2006-2023.

Les précipitations sont beaucoup plus bruitées. La moyenne annuelle peut toujours être résumée par une ligne simple, mais les variations d’une année à l’autre sont nettement plus fortes que pour la température ; la tendance linéaire estimée doit donc être lue avec prudence. La bande ombrée du graphique ci-dessous est une dispersion descriptive — un écart type des moyennes nationales annuelles — ajoutée uniquement pour visualiser l’ampleur des variations annuelles. Ce n’est pas un intervalle de confiance ni une estimation de l’incertitude autour de la tendance.

italy_precipitation_trend = (
    italy_weather_data.groupby("Year", as_index=False)["Precipitation"]
    .mean()
    .dropna()
)

prec_coef = np.polyfit(
    italy_precipitation_trend["Year"].to_numpy(dtype=float),
    italy_precipitation_trend["Precipitation"].to_numpy(dtype=float),
    1,
)

italy_precipitation_trend["Trend"] = (
    prec_coef[0] * italy_precipitation_trend["Year"] + prec_coef[1]
)

national_prec_slope = linear_slope(
    italy_precipitation_trend,
    "Precipitation"
)
print(f"La tendance globale est de {round(national_prec_slope, 2)} mm")
La tendance globale est de -4.57 mm
fig, ax = plt.subplots(figsize=(10.5, 5.8))

# Dispersion descriptive : ±1 écart type des moyennes nationales annuelles.
# Il s’agit d’un repère visuel pour la variabilité annuelle, PAS d’un
# intervalle de confiance ni d’une incertitude autour de la tendance.
prec_std = italy_precipitation_trend["Precipitation"].std()
italy_precipitation_trend["Lower"] = italy_precipitation_trend["Precipitation"] - prec_std
italy_precipitation_trend["Upper"] = italy_precipitation_trend["Precipitation"] + prec_std

# Bande ombrée
ax.fill_between(italy_precipitation_trend["Year"], italy_precipitation_trend["Lower"], italy_precipitation_trend["Upper"], facecolor=ACCENT, alpha=0.12, linewidth=0, zorder=1, label="±1σ (dispersion annuelle)")

# Lignes
ax.plot(italy_precipitation_trend["Year"], italy_precipitation_trend["Precipitation"], color=ACCENT, linewidth=2.8, marker="o", markersize=5.5, label="Moyenne annuelle", zorder=3)
ax.plot(italy_precipitation_trend["Year"], italy_precipitation_trend["Trend"], color=TEXT, linewidth=1.8, linestyle=(0, (4, 4)), alpha=0.7, label="Tendance linéaire", zorder=2)

# Titre et sous-titre
ax.set_title("Tendance des précipitations en Italie (2006-2023)", loc="left", pad=38)
ax.text(0, 1.02, "Précipitations annuelles moyennes des chefs-lieux de province, d’après ISTAT Tavola_2", transform=ax.transAxes, fontsize=10.5, color=MUTED)

ax.set_xlabel("Année"); ax.set_ylabel("Précipitations (mm)")
ax.xaxis.set_major_locator(MaxNLocator(integer=True))
ax.set_xticks(italy_precipitation_trend["Year"][::2])
ax.grid(axis="y", color=GRID, linewidth=0.8, alpha=0.85)
ax.grid(axis="x", visible=False)
for spine in ["top", "right"]: ax.spines[spine].set_visible(False)
ax.spines["left"].set_color(GRID); ax.spines["bottom"].set_color(GRID)
ax.legend(loc="lower right")

plt.tight_layout()
plt.show()
Figure 2: Précipitations annuelles moyennes dans les chefs-lieux de province italiens, 2006-2023.

Les lieux les plus chauds

La moyenne des températures sur toute la période 2006-2023 donne un classement simple des « villes les plus chaudes ». Sans surprise, les premières places se concentrent en Sicile et dans l’extrême sud.

top_hottest = (
    italy_weather_data.groupby("Province", as_index=False)["Temperature"]
    .mean()
    .sort_values("Temperature", ascending=False)
    .head(5)
)

top_hottest
Table 1: Cinq chefs-lieux de province les plus chauds par température moyenne, 2006-2023.
Province Temperature
56 Messina 19.655556
24 Catania 19.650000
79 Reggio di Calabria 19.300000
0 Agrigento 19.244444
91 Taranto 19.177778

Être une ville chaude n’est pas la même chose que se réchauffer rapidement. Pour estimer les taux de réchauffement, j’ajuste une pente linéaire pour chaque ville après avoir conservé uniquement les lieux ayant au moins 15 observations annuelles valides, ce qui réduit l’influence des séries incomplètes.

def slope_per_city(group, value_col):
    clean = group.dropna(subset=[value_col])
    x = clean["Year"].to_numpy(dtype=float)
    y = clean[value_col].to_numpy(dtype=float)
    if len(x) < 2:
        return np.nan
    return round(float(np.polyfit(x, y, 1)[0]), 3)

warming_base = italy_weather_data.groupby("Province").filter(
    lambda g: g["Temperature"].notna().sum() >= 15
)

warming_rates = (
    warming_base.groupby("Province")
    .apply(lambda g: slope_per_city(g, "Temperature"))
    .rename("Trend (°C/year)")
    .reset_index()
    .sort_values("Trend (°C/year)", ascending=False)
)


warming_rates.head(10)
Province Trend (°C/year)
87 Sondrio 0.150
53 Massa Carrara 0.132
81 Roma 0.122
15 Bologna 0.118
29 Cremona 0.117
0 Agrigento 0.110
24 Catania 0.109
57 Modena 0.107
84 Sassari 0.099
52 Mantova 0.098
top_warming = warming_rates.head(10)
fig, ax = plt.subplots(figsize=(10.5, 6.5))

bars = ax.barh(top_warming["Province"][::-1], top_warming["Trend (°C/year)"][::-1], color=ACCENT, alpha=0.8, edgecolor="none", height=0.65)

# Sous-titre sous le titre
ax.set_title("Chefs-lieux de province italiens au réchauffement le plus rapide", loc="left", pad=38)
ax.text(0, 1.02, "Pente linéaire des températures annuelles moyennes, villes avec au moins 15 années valides", transform=ax.transAxes, fontsize=10.5, color=MUTED_HEX)

ax.set_xlabel("Tendance au réchauffement (°C par an)", color=MUTED_HEX, fontsize=11, labelpad=10)
for spine in ["top", "right"]: ax.spines[spine].set_visible(False)
ax.spines["left"].set_color(GRID); ax.spines["bottom"].set_color(GRID)
ax.grid(axis="x", linestyle="-", color=GRID, alpha=0.8)
ax.grid(axis="y", visible=False)

for bar in bars:
    width = bar.get_width()
    ax.text(width + 0.003, bar.get_y() + bar.get_height()/2, f"+{width:.3f}", va="center", color=TEXT, fontsize=10, fontweight="bold")

plt.tight_layout()
plt.show()
Figure 3: Chefs-lieux de province au réchauffement le plus rapide, filtrés aux villes ayant au moins 15 observations annuelles valides.

Anomalies de 2023

Les séries longues sont utiles, mais le classeur contient aussi un tableau d’anomalies ciblé pour 2023. Tavola_3 compare 2023 à la moyenne 2006-2015 et, lorsque c’est disponible, aux normales climatologiques 1981-2010 et 1971-2000.

anomaly_sheet = pd.read_excel(FILE_PATH, sheet_name="Tavola_3", header=None)

anomaly_data = anomaly_sheet.iloc[4:114, :7].copy()
anomaly_data.columns = [
    "Province",
    "Diff_2023_vs_2006_2015",
    "Mean_2006_2015",
    "Anom_1981_2010",
    "Clim_1981_2010",
    "Anom_1971_2000",
    "Clim_1971_2000",
]

anomaly_data = anomaly_data[anomaly_data["Province"].notna()]
anomaly_data = anomaly_data[
    ~anomaly_data["Province"].astype(str).str.contains(
        r"Fonte|^\s*$",
        regex=True,
        na=False,
    )
]
anomaly_data["Province"] = anomaly_data["Province"].astype(str).str.strip()

for col in anomaly_data.columns[1:]:
    anomaly_data[col] = pd.to_numeric(
        anomaly_data[col].replace("....", np.nan),
        errors="coerce",
    )

anomaly_data.head()
Province Diff_2023_vs_2006_2015 Mean_2006_2015 Anom_1981_2010 Clim_1981_2010 Anom_1971_2000 Clim_1971_2000
4 Torino 1.0 15.0 2.01 13.9 2.47 13.44
5 Vercelli 0.9 13.5 NaN NaN NaN NaN
6 Novara 1.1 14.4 NaN NaN NaN NaN
7 Cuneo 0.9 12.9 NaN NaN NaN NaN
8 Asti 1.1 13.3 NaN NaN NaN NaN

Comprendre la référence 1981-2010

En climatologie, une anomalie est l’écart entre la température observée et une moyenne de long terme, appelée normale climatologique. L’Organisation météorologique mondiale (OMM) utilise des périodes de 30 ans pour établir ces normales.

  • Anomalie de +0,0 °C : l’année a été exactement aussi chaude que la moyenne de 1981-2010.
  • Anomalie positive, par exemple +2,66 °C : l’année a été plus chaude que la référence historique.

Lorsque les valeurs dépassent +2,0 °C dans le tableau ci-dessous, cela indique que ces villes ont connu une année nettement plus chaude que les conditions qui caractérisaient la fin du XXe siècle.

Une réserve s’impose quant à la couverture des données : Tavola_3 ne fournit pas d’anomalies par rapport aux normales climatologiques pour tous les chefs-lieux de province. La feuille précise que ces références ne sont calculées que pour les chefs-lieux de région disposant de séries suffisamment longues. Certaines différences entre 2023 et la période 2006-2015 manquent également lorsque les séries récentes sont incomplètes.

Le classement ci-dessous porte donc sur les lieux pour lesquels les comparaisons climatologiques sont disponibles, et non sur tous les chefs-lieux de province. Il montre les villes qui ressortent dans ce sous-ensemble comparable, et non une liste exhaustive des villes les plus chaudes d’Italie. Dans ce sous-ensemble, 2023 se distingue particulièrement à Perugia, Bologna, Milano et Torino.

top_anomalies = (
    anomaly_data.dropna(subset=["Anom_1981_2010"])
    .sort_values("Anom_1981_2010", ascending=False)
    [["Province", "Anom_1981_2010"]]
    .head(10)
)

top_anomalies
Table 2: Plus fortes anomalies de température en 2023 par rapport à la climatologie 1981-2010.
Province Anom_1981_2010
61 Perugia 2.95
46 Bologna 2.56
16 Milano 2.38
4 Torino 2.01
26 Trento 1.84
70 Roma 1.84
31 Venezia 1.74
36 Trieste 1.74
25 Bolzano 1.72
12 Aosta 1.71
top_anomalies_plot = anomaly_data.dropna(subset=["Anom_1981_2010"]).sort_values("Anom_1981_2010", ascending=False).head(10)

fig, ax = plt.subplots(figsize=(10.5, 6.5))
bars = ax.barh(top_anomalies_plot["Province"][::-1], top_anomalies_plot["Anom_1981_2010"][::-1], color=ACCENT, alpha=0.8, edgecolor="none", height=0.65)

# Sous-titre sous le titre
ax.set_title("Plus fortes anomalies de température en 2023", loc="left", pad=38)
ax.text(0, 1.02, "Par rapport à la climatologie 1981-2010, d’après ISTAT Tavola_3", transform=ax.transAxes, fontsize=10.5, color=MUTED_HEX)

ax.set_xlabel("Anomalie (°C)", color=MUTED_HEX, fontsize=11, labelpad=10)
for spine in ["top", "right"]: ax.spines[spine].set_visible(False)
ax.spines["left"].set_color(GRID); ax.spines["bottom"].set_color(GRID)
ax.grid(axis="x", linestyle="-", color=GRID, alpha=0.8)
ax.grid(axis="y", visible=False)

for bar in bars:
    width = bar.get_width()
    ax.text(width + 0.05, bar.get_y() + bar.get_height()/2, f"+{width:.2f}", va="center", color=TEXT, fontsize=10, fontweight="bold")

plt.tight_layout()
plt.show()
Figure 4: Plus fortes anomalies de température en 2023 par rapport à la climatologie 1981-2010.

Corrélation : chaleur et pluie

Une question naturelle est de savoir si les années plus chaudes ont aussi tendance à être plus humides ou plus sèches. Le nuage de points entre température moyenne annuelle nationale et précipitation totale donne un premier aperçu de leur évolution conjointe.

import seaborn as sns

# Préparer les données de corrélation
corr_df = italy_temperature_trend[['Year', 'Temperature']].merge(
    italy_precipitation_trend[['Year', 'Precipitation']], 
    on='Year'
)

fig, ax = plt.subplots(figsize=(8, 6))
sns.regplot(data=corr_df, x='Temperature', y='Precipitation', 
            scatter_kws={'color': ACCENT, 's': 60}, 
            line_kws={'color': TEXT, 'linestyle': '--', 'linewidth': 1.5}, ax=ax)

ax.set_title("Corrélation : température annuelle et précipitations", loc='left', pad=25)
ax.set_xlabel("Température moyenne (°C)")
ax.set_ylabel("Précipitations totales (mm)")

# Calculer le coefficient de corrélation
r_val = corr_df['Temperature'].corr(corr_df['Precipitation'])
ax.text(0.05, 0.05, f"Pearson r = {r_val:.2f}", transform=ax.transAxes, 
        fontsize=12, fontweight='bold', color=TEXT)

plt.tight_layout()
plt.show()
Figure 5: Corrélation entre température annuelle et précipitations.

Le nuage de points montre une association négative faible à modérée dans ces agrégats annuels : sur 18 ans, les années plus chaudes ont eu tendance à coïncider avec des précipitations nationales un peu plus faibles. Avec un r de Pearson d’environ -0,34, la relation existe mais reste loin d’être stricte : beaucoup d’années chaudes n’ont pas été particulièrement sèches. Cela décrit seulement la co-variation annuelle des deux moyennes nationales ; cela ne dit rien du mécanisme physique qui pilote l’une ou l’autre série, et une seule année peut déplacer fortement la relation. Le creux marqué de 2022, par exemple, reflète la forte sécheresse que l’Italie a connue cette année-là et tire visiblement la droite de régression vers le bas.

Cartographier le changement

Une série temporelle montre quand les températures augmentent. Une carte montre se concentre la chaleur. Pour géocoder une seule fois les chefs-lieux de province, je mets les coordonnées en cache localement et les réutilise lors des rendus suivants.

import numpy as np
import pandas as pd
import plotly.express as px
import plotly.io as pio

# Redéfinir le parseur pour garantir la disponibilité des données
def parse_table(sheet_name, value_name):
    df = pd.read_excel(FILE_PATH, sheet_name=sheet_name, header=None)
    years = [int(x) for x in df.iloc[3, 1:19].tolist()]
    data = df.iloc[4:, :19].copy()
    data.columns = ["Province"] + years
    data = data[data["Province"].notna()]
    data["Province"] = data["Province"].astype(str).str.strip()
    data = data[~data["Province"].str.contains(r"Fonte|Totale|Nota|Nelle tavole|^$", regex=True, na=False)]
    long = data.melt(id_vars="Province", var_name="Year", value_name=value_name)
    long["Year"] = pd.to_numeric(long["Year"], errors="coerce")
    long[value_name] = pd.to_numeric(long[value_name].replace("....", np.nan), errors="coerce")
    return long

# Charger les données
temp_df = parse_table("Tavola_1", "Temperature")
precip_df = parse_table("Tavola_2", "Precipitation")
italy_weather_data = temp_df.merge(precip_df, on=["Province", "Year"], how="inner")

# Dictionnaire de coordonnées pour les chefs-lieux de province italiens
city_coords = {
    'Torino': [45.0703, 7.6869], 'Vercelli': [45.3238, 8.4232], 'Novara': [45.4468, 8.6212],
    'Cuneo': [44.3833, 7.5500], 'Asti': [44.9005, 8.2069], 'Alessandria': [44.9129, 8.6154],
    'Biella': [45.5630, 8.0579], 'Verbano-Cusio-Ossola': [45.9220, 8.5516], 'Aosta': [45.7371, 7.3201],
    'Milano': [45.4642, 9.1900], 'Lodi': [45.3139, 9.5032], 'Monza e della Brianza': [45.5845, 9.2744],
    'Bergamo': [45.6983, 9.6773], 'Brescia': [45.5398, 10.2181], 'Pavia': [45.1850, 9.1546],
    'Como': [45.8081, 9.0852], 'Varese': [45.8167, 8.8333], 'Sondrio': [46.1690, 9.8731],
    'Lecco': [45.8559, 9.3977], 'Mantova': [45.1564, 10.7911], 'Cremona': [45.1333, 10.0333],
    'Bolzano': [46.4983, 11.3548], 'Trento': [46.0667, 11.1167], 'Verona': [45.4384, 10.9916],
    'Vicenza': [45.5479, 11.5446], 'Belluno': [46.1408, 12.2161], 'Treviso': [45.6667, 12.2450],
    'Venezia': [45.4408, 12.3155], 'Padova': [45.4064, 11.8768], 'Rovigo': [45.0711, 11.7905],
    'Pordenone': [45.9569, 12.6605], 'Udine': [46.0625, 13.2346], 'Gorizia': [45.9409, 13.6222],
    'Trieste': [45.6495, 13.7768], 'Piacenza': [45.0526, 9.6930], 'Parma': [44.8015, 10.3279],
    'Reggio nell\'Emilia': [44.6982, 10.6312], 'Modena': [44.6471, 10.9252], 'Bologna': [44.4949, 11.3426],
    'Ferrara': [44.8381, 11.6198], 'Ravenna': [44.4183, 12.2035], 'Forlì-Cesena': [44.2227, 12.0409],
    'Rimini': [44.0594, 12.5684], 'Massa Carrara': [44.0375, 10.1417], 'Lucca': [43.8429, 10.5027],
    'Pistoia': [43.9333, 10.9167], 'Firenze': [43.7696, 11.2558], 'Livorno': [43.5485, 10.3106],
    'Pisa': [43.7085, 10.4036], 'Arezzo': [43.4631, 11.8781], 'Siena': [43.3186, 11.3306],
    'Grosseto': [42.7667, 11.1167], 'Prato': [43.8777, 11.1022], 'Perugia': [43.1107, 12.3908],
    'Terni': [42.5639, 12.6427], 'Ancona': [43.6158, 13.5189], 'Pesaro e Urbino': [43.9100, 12.9133],
    'Macerata': [43.3003, 13.4531], 'Ascoli Piceno': [42.8536, 13.5768], 'Fermo': [43.1610, 13.7183],
    'Viterbo': [42.4191, 12.1051], 'Rieti': [42.4000, 12.8667], 'Roma': [41.9028, 12.4964],
    'Latina': [41.4676, 12.9036], 'Frosinone': [41.6394, 13.3411], 'L\'Aquila': [42.3489, 13.3980],
    'Teramo': [42.6589, 13.7044], 'Pescara': [42.4618, 14.2142], 'Chieti': [42.3511, 14.1675],
    'Campobasso': [41.5603, 14.6584], 'Isernia': [41.5947, 14.2342], 'Caserta': [41.0736, 14.3347],
    'Benevento': [41.1297, 14.7821], 'Napoli': [40.8518, 14.2681], 'Avellino': [40.9144, 14.7936],
    'Salerno': [40.6779, 14.7658], 'Foggia': [41.4622, 15.5446], 'Bari': [41.1171, 16.8719],
    'Taranto': [40.4677, 17.2433], 'Brindisi': [40.6321, 17.9361], 'Lecce': [40.3515, 18.1750],
    'Barletta': [41.3197, 16.2768], 'Potenza': [40.6404, 15.8051], 'Matera': [40.6664, 16.6043],
    'Cosenza': [39.2983, 16.2537], 'Catanzaro': [38.9098, 16.5877], 'Reggio di Calabria': [38.1105, 15.6434],
    'Crotone': [39.0808, 17.1273], 'Vibo Valentia': [38.6753, 16.1011], 'Messina': [38.1938, 15.5540],
    'Palermo': [38.1157, 13.3615], 'Trapani': [38.0175, 12.5150], 'Agrigento': [37.3111, 13.5765],
    'Caltanissetta': [37.4903, 14.0622], 'Enna': [37.5671, 14.2750], 'Catania': [37.5079, 15.0830],
    'Ragusa': [36.9269, 14.7231], 'Siracusa': [37.0755, 15.2866], 'Sassari': [40.7259, 8.5615],
    'Nuoro': [40.3231, 9.3303], 'Cagliari': [39.2238, 9.1217], 'Oristano': [39.9056, 8.5911],
    'Carbonia': [39.1671, 8.5222],
    'La Spezia': [44.2384, 9.6912],
    'Andria': [41.2276, 16.2955],
    'Trani': [41.2782, 16.4186],
}

# Le classeur utilise des noms courts de chefs-lieux (par exemple "Verbania",
# "Monza", "Pesaro Urbino", "Forlì") qui ne correspondent pas toujours aux
# clés administratives plus longues ci-dessus. On normalise donc avant la
# recherche pour éviter qu’un point disparaisse silencieusement.
COORD_NAME_ALIASES = {
    'Verbania': 'Verbano-Cusio-Ossola',
    'Monza': 'Monza e della Brianza',
    'Pesaro Urbino': 'Pesaro e Urbino',
    'Forlì': 'Forlì-Cesena',
}

def lookup_coords(province):
    key = COORD_NAME_ALIASES.get(province, province)
    return city_coords.get(key, [None, None])

# Associer les coordonnées et signaler tout lieu encore non cartographié.
italy_weather_data['Lat'] = italy_weather_data['Province'].map(lambda x: lookup_coords(x)[0])
italy_weather_data['Lon'] = italy_weather_data['Province'].map(lambda x: lookup_coords(x)[1])
unmapped = italy_weather_data.loc[italy_weather_data['Lat'].isna(), 'Province'].unique()
if len(unmapped) > 0:
    print(f"Provinces non cartographiées (coordonnées manquantes) : {list(unmapped)}")
map_df = italy_weather_data.dropna(subset=['Lat', 'Lon', 'Temperature']).copy()

# Préparer la carte Mapbox
fig = px.scatter_mapbox(
    map_df,
    lat="Lat", lon="Lon", color="Temperature",
    hover_name="Province", animation_frame="Year",
    title="Évolution des températures annuelles en Italie (2006-2023)",
    color_continuous_scale=px.colors.sequential.YlOrRd,
    range_color=[map_df['Temperature'].min(), map_df['Temperature'].max()],
    mapbox_style="open-street-map",
    zoom=4.5, center=dict(lat=42.0, lon=12.5)
)

fig.update_layout(height=700, margin=dict(r=10, t=80, b=40, l=10), paper_bgcolor=globals().get('BG', '#f7f6f2'))
fig
Provinces non cartographiées (coordonnées manquantes) : ['Imperia', 'Savona', 'Genova', 'a) I dati meteoclimatici delle stazioni esaminate e gli indicatori statistici calcolati forniscono misure riferite alle aree monitorate.']
Figure 6: Température annuelle moyenne par chef-lieu de province pour certaines années, 2006-2023.

Une vue complémentaire moyenne chaque chef-lieu sur toute la période 2006-2023, ce qui rend le gradient nord-sud persistant facile à lire d’un coup d’œil : la Sicile et l’extrême sud apparaissent les plus chauds, les Alpes les plus fraîches.

# Calculer la moyenne par ville pour la carte statique
mean_per_city = (
    map_df.groupby("Province", as_index=False)
    .agg(
        Lat=("Lat", "first"),
        Lon=("Lon", "first"),
        Temperature=("Temperature", "mean"),
    )
)

# Créer une carte Mapbox statique cohérente avec la version interactive
fig_static = px.scatter_mapbox(
    mean_per_city,
    lat="Lat",
    lon="Lon",
    color="Temperature",
    hover_name="Province",
    size_max=15,
    color_continuous_scale=px.colors.sequential.YlOrRd,
    range_color=[map_df['Temperature'].min(), map_df['Temperature'].max()],
    mapbox_style="open-street-map",
    zoom=4.5,
    center=dict(lat=42.0, lon=12.5),
    title="<b>Température moyenne par chef-lieu de province (2006-2023)</b><br><span style='font-size:12px; color:#7a7974'>Température moyenne sur l’ensemble de la série ISTAT Tavola_1</span>"
)

fig_static.update_layout(
    height=700,
    margin=dict(r=10, t=80, b=40, l=10),
    paper_bgcolor=BG,
    font=dict(color=TEXT)
)

fig_static
Figure 7: Température annuelle moyenne par chef-lieu de province, moyenne sur 2006-2023.

Points à retenir

Le classeur mis à jour fait clairement apparaître le profil national : les chefs-lieux de province italiens présentent une nette tendance au réchauffement entre 2006 et 2023, tandis que les précipitations sont beaucoup plus variables et moins homogènes.

Quelques constats généraux se dégagent :

  • La moyenne des chefs-lieux de province indique un réchauffement régulier sur la période.
  • Les villes les plus chaudes se concentrent en Sicile et dans l’extrême sud.
  • Le classement des villes au réchauffement le plus rapide dépend fortement de l’exhaustivité des données ; il est donc important de filtrer les séries lacunaires.
  • Le tableau des anomalies de 2023 apporte un contexte historique important : plusieurs villes n’étaient pas seulement chaudes en valeur absolue, mais aussi exceptionnellement chaudes par rapport aux références climatologiques.

La suite naturelle consisterait à intégrer les feuilles consacrées aux chefs-lieux de région (Tavola_9 et Tavola_10) ainsi que les tableaux d’extrêmes (Tavola_7, Tavola_8, Tavola_11 à Tavola_14) dans un second article consacré aux extrêmes de chaleur, aux nuits chaudes et aux indicateurs de fortes précipitations.

Reproduire l’analyse

Vous préférez l’exécuter vous-même ? Le notebook, le jeu de données au format long et le dépôt source sont disponibles pour inspecter les étapes de nettoyage, réutiliser les données ou prolonger les graphiques.

Notebook

Exécutez l’analyse complète localement et reproduisez les figures de l’article.

Télécharger .ipynb

Jeu de données au format long

Utilisez le tableau nettoyé province-année avec les colonnes de température et de précipitation.

Télécharger .csv

Dépôt source

Attribuez une étoile au projet GitHub associé, ou forkez-le pour prolonger l’analyse.

Voir sur GitHub

Share this article