Temperature italiane, 2006-2023: una storia di dati climatici

Da un ampio file ISTAT a dati ordinati, stime di tendenza e grafici matplotlib coerenti con lo stile del blog per i capoluoghi di provincia italiani.
data science
python
clima
visualizzazione dati
Autore/Autrice

Federico Viscioletti

Data di Pubblicazione

2 luglio 2026

Introduzione

L’Italia concentra climi alpini, continentali e mediterranei in una penisola stretta. Questo la rende un buon caso di studio per osservare come il riscaldamento e i cambiamenti nelle precipitazioni si manifestino in contesti locali molto diversi.

In questo articolo uso il file meteoclimatico aggiornato dell’ISTAT sui capoluoghi di provincia per rispondere ad alcune domande semplici:

  • Quanto rapidamente si stanno riscaldando i capoluoghi di provincia italiani?
  • Quali luoghi sono, in media, i più caldi?
  • Le precipitazioni seguono la stessa tendenza?
  • Com’è stato il 2023 rispetto ai riferimenti recenti e storici?

Il file di origine è ricco, ma strutturato in modo scomodo. Come spesso accade con le statistiche ufficiali, la parte più difficile non è disegnare i grafici: è trasformare un layout Excel pensato per la lettura umana in dati ordinati.

Una nota per aspiranti data scientist

Una delle competenze più potenti che un data scientist può sviluppare è la capacità di trasformare dati grezzi in informazioni chiare e utilizzabili. Gli istituti statistici nazionali, come l’ISTAT in Italia, Eurostat o lo US Census Bureau, offrono molti dataset gratuiti e di alta qualità con cui sperimentare e produrre analisi.

Come vedrai in questa analisi, la barriera tecnica è in realtà piuttosto bassa. Una volta imparato a gestire file strutturati e a svolgere una semplice analisi esplorativa dei dati (EDA), puoi scoprire storie nei campi che ti appassionano: dal cambiamento climatico all’economia o alle tendenze sociali. Spero che questo notebook possa servirti da mappa nel tuo percorso di scoperta.

Questo articolo ricostruisce l’analisi originale in stile notebook usando il file ISTAT aggiornato per i capoluoghi di provincia, con temperatura e precipitazioni annuali dal 2006 al 2023, oltre alle tabelle delle anomalie per il 2023.

Se questo lavoro ti piace, lascia una stella al repository GitHub del progetto.

Caricamento del file

I fogli principali per l’analisi sono:

  • Tavola_1: temperatura media annuale per capoluogo di provincia, 2006-2023
  • Tavola_2: precipitazioni annuali per capoluogo di provincia, 2006-2023
  • Tavola_3: anomalie di temperatura del 2023 rispetto al 2006-2015 e alle normali climatologiche
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"
)

# --- Blog design system palette (see 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']

Dal formato wide ai dati ordinati

Le tabelle annuali sono archiviate in formato wide: una riga per città e una colonna per anno. Questo formato è comodo per l’ispezione visiva, ma scomodo per l’analisi. Un piccolo parser permette di trasformare ogni foglio in formato long gestendo anche il marcatore dei valori mancanti del file di origine (....).

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)

Un rapido controllo conferma che il dataset pulito copre l’intero periodo 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

Tendenze nazionali

Un primo sguardo naturale consiste nella media annuale su tutti i capoluoghi di provincia. Questa aggregazione condensa la variabilità locale in una sintesi nazionale e rende molto più chiara la direzione del cambiamento.

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)

Considerando i capoluoghi di provincia, il tasso di riscaldamento stimato è di circa 0,055 °C all’anno, cioè di circa 0,9–1,0 °C nell’intero periodo. Questo non significa che ogni città si riscaldi esattamente allo stesso ritmo, ma definisce il contesto nazionale per il resto dell’analisi.

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="Media annuale", zorder=3)
ax.plot(italy_mean["Year"], italy_mean["Trend"], color=TEXT, linewidth=1.8, linestyle=(0, (4, 4)), alpha=0.7, label="Tendenza lineare", zorder=2)

# Titolo con margine e sottotitolo
ax.set_title("Temperatura media annuale nei capoluoghi di provincia italiani", loc="left", pad=38)
ax.text(0, 1.02, "Temperatura media annuale per capoluogo di provincia, media nazionale, 2006–2023", transform=ax.transAxes, fontsize=10.5, color=MUTED)

ax.set_xlabel("Anno"); ax.set_ylabel("Temperatura (°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()
Figura 1: Temperatura media annuale nei capoluoghi di provincia italiani, 2006–2023.

Le precipitazioni sono molto più variabili. La media annuale può comunque essere riassunta con una semplice linea di tendenza, ma le oscillazioni da un anno all’altro sono molto più ampie rispetto a quelle della temperatura; la tendenza stimata va quindi interpretata con maggiore cautela. La banda ombreggiata nel grafico seguente rappresenta una dispersione descrittiva — una deviazione standard calcolata sulle medie nazionali annuali — ed è aggiunta solo come guida visiva per mostrare quanto le precipitazioni varino da un anno all’altro. Non è un intervallo di confidenza né una stima dell’incertezza intorno alla tendenza.

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 tendenza complessiva è {round(national_prec_slope, 2)} mm")
La tendenza complessiva è -4.57 mm
fig, ax = plt.subplots(figsize=(10.5, 5.8))

# Dispersione descrittiva: ±1 deviazione standard delle medie nazionali annuali.
# È una guida visiva alla variabilità da un anno all'altro, non un intervallo
# di confidenza o una stima dell'incertezza intorno alla linea di tendenza.
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

# Banda ombreggiata
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σ (variabilità da un anno all'altro)")

# Linee
ax.plot(italy_precipitation_trend["Year"], italy_precipitation_trend["Precipitation"], color=ACCENT, linewidth=2.8, marker="o", markersize=5.5, label="Media annuale", 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="Tendenza lineare", zorder=2)

# Titoli e sottotitoli
ax.set_title("Tendenza delle precipitazioni in Italia (2006–2023)", loc="left", pad=38)
ax.text(0, 1.02, "Precipitazioni annuali medie nei capoluoghi di provincia da ISTAT Tavola_2", transform=ax.transAxes, fontsize=10.5, color=MUTED)

ax.set_xlabel("Anno"); ax.set_ylabel("Precipitazioni (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()
Figura 2: Precipitazioni annuali medie nei capoluoghi di provincia italiani, 2006–2023.

I luoghi più caldi

Calcolare la temperatura media sull’intero periodo 2006–2023 produce una semplice classifica delle “città più calde”. Non sorprende che le prime posizioni si concentrino in Sicilia e nell’estremo Sud.

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

top_hottest
Tabella 1: I cinque capoluoghi di provincia più caldi per temperatura media, 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

Essere caldi non equivale a riscaldarsi rapidamente. Per stimare i tassi di riscaldamento, adatto una pendenza lineare per ogni città, limitando l’analisi alle località con almeno 15 osservazioni annuali valide. In questo modo riduco l’influenza delle serie incomplete.

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)

# Sottotitolo sotto il titolo
ax.set_title("Capoluoghi di provincia italiani con il riscaldamento più rapido", loc="left", pad=38)
ax.text(0, 1.02, "Pendenza lineare della temperatura media annuale, città con almeno 15 anni validi", transform=ax.transAxes, fontsize=10.5, color=MUTED_HEX)

ax.set_xlabel("Tendenza al riscaldamento (°C all'anno)", 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()
Figura 3: Capoluoghi di provincia con il riscaldamento più rapido, filtrati alle città con almeno 15 osservazioni annuali valide.

Anomalie del 2023

Le serie di lungo periodo sono utili, ma il file include anche una tabella specifica delle anomalie per il 2023. Tavola_3 confronta il 2023 con la media 2006-2015 e, dove disponibile, con le normali climatologiche 1981-2010 e 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

Capire il riferimento 1981-2010

Nella climatologia, un’anomalia è la differenza tra la temperatura osservata e una media di lungo periodo, chiamata normale climatologica. L’Organizzazione Meteorologica Mondiale (WMO) usa periodi di 30 anni per definire queste normali.

  • Anomalia di +0,0 °C: l’anno è stato caldo quanto la media del periodo 1981–2010.
  • Anomalia positiva, per esempio +2,66 °C: l’anno è stato più caldo del riferimento storico.

Quando nella tabella seguente compaiono valori superiori a +2,0 °C, significa che quelle città hanno vissuto un anno significativamente più caldo delle condizioni che caratterizzavano la fine del XX secolo.

Una nota sulla copertura: Tavola_3 non fornisce anomalie rispetto alle normali climatologiche per tutti i capoluoghi di provincia. Il foglio indica che questi riferimenti sono calcolati solo per i capoluoghi regionali con serie abbastanza lunghe; inoltre, alcune differenze tra il 2023 e il periodo 2006–2015 mancano quando le serie recenti sono incomplete.

La classifica seguente riguarda quindi i luoghi per i quali sono disponibili confronti climatologici, non tutti i capoluoghi di provincia. Mostra quali città emergono in questo sottoinsieme comparabile, non una lista esaustiva delle località più calde d’Italia. In questo gruppo, il 2023 spicca in particolare in città come Perugia, Bologna, Milano e 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
Tabella 2: Le maggiori anomalie di temperatura del 2023 rispetto alla climatologia 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)

# Sottotitolo sotto il titolo
ax.set_title("Le maggiori anomalie di temperatura del 2023", loc="left", pad=38)
ax.text(0, 1.02, "Rispetto alla climatologia 1981–2010 da ISTAT Tavola_3", transform=ax.transAxes, fontsize=10.5, color=MUTED_HEX)

ax.set_xlabel("Anomalia (°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()
Figura 4: Le maggiori anomalie di temperatura del 2023 rispetto alla climatologia 1981–2010.

Temperatura e precipitazioni

Una domanda naturale è se gli anni più caldi tendano anche a essere più umidi o più secchi. Mettere in relazione la temperatura media annuale nazionale con le precipitazioni totali offre un primo sguardo alla relazione tra i due aggregati.

import seaborn as sns

# Prepara i dati per la correlazione
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("Correlazione: temperatura annuale e precipitazioni", loc='left', pad=25)
ax.set_xlabel("Temperatura media (°C)")
ax.set_ylabel("Precipitazioni totali (mm)")

# Calcola il coefficiente di correlazione
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()
Figura 5: Correlazione: temperatura annuale e precipitazioni.

Lo scatterplot mostra una debole associazione negativa tra questi aggregati annuali: nei 18 anni considerati, gli anni più caldi hanno teso a coincidere con precipitazioni nazionali leggermente inferiori. Con un coefficiente di correlazione di Pearson di circa -0,34, la relazione è presente ma non forte: molti anni caldi non sono stati particolarmente secchi.

Questo risultato descrive soltanto come le due medie nazionali variano congiuntamente da un anno all’altro; non identifica il meccanismo fisico che guida l’una o l’altra serie, e un singolo anno può influenzare sensibilmente la relazione. Il forte calo del 2022, per esempio, riflette la grave siccità affrontata dall’Italia in quell’anno e abbassa visibilmente la linea di regressione.

Mappare il cambiamento

Un grafico temporale mostra quando aumentano le temperature; una mappa mostra dove si concentra il caldo. Per geocodificare una sola volta i capoluoghi di provincia, memorizzo le coordinate localmente e le riutilizzo nei render successivi.

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

# Ridefinisce il parser per garantire che i dati siano disponibili
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

# Carica i dati
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")

# Dizionario di coordinate per i capoluoghi di provincia italiani
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],
}

# Il workbook usa nomi brevi per alcuni capoluoghi (per esempio "Verbania",
# "Monza", "Pesaro Urbino", "Forlì") che non corrispondono alle chiavi
# amministrative più lunghe in city_coords. Normalizza prima del lookup per
# evitare che un punto scompaia quando la chiave usa una grafia diversa.
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])

# Mappa le coordinate e segnala eventuali province ancora non mappate.
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"Province non mappate (senza coordinate): {list(unmapped)}")
map_df = italy_weather_data.dropna(subset=['Lat', 'Lon', 'Temperature']).copy()

# Prepara la mappa Mapbox
fig = px.scatter_mapbox(
    map_df,
    lat="Lat", lon="Lon", color="Temperature",
    hover_name="Province", animation_frame="Year",
    title="Evoluzione delle temperature annuali in Italia (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
Province non mappate (senza coordinate): ['Imperia', 'Savona', 'Genova', 'a) I dati meteoclimatici delle stazioni esaminate e gli indicatori statistici calcolati forniscono misure riferite alle aree monitorate.']
Figura 6: Temperatura media annuale per capoluogo di provincia negli anni selezionati, 2006–2023.

Una vista complementare calcola la media di ogni capoluogo sull’intera finestra 2006–2023, rendendo più facile leggere a colpo d’occhio il persistente gradiente termico nord-sud: Sicilia ed estremo Sud più caldi, Alpi più fresche.

# Calcola la media per città per la mappa statica
mean_per_city = (
    map_df.groupby("Province", as_index=False)
    .agg(
        Lat=("Lat", "first"),
        Lon=("Lon", "first"),
        Temperature=("Temperature", "mean"),
    )
)

# Crea una mappa Mapbox statica coerente con l'aspetto interattivo
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>Temperatura media per capoluogo di provincia (2006–2023)</b><br><span style='font-size:12px; color:#7a7974'>Temperatura media sull'intera serie da 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
Figura 7: Temperatura media annuale per capoluogo di provincia, media 2006–2023.

Cosa portarsi a casa

Il file aggiornato permette di leggere con chiarezza il quadro nazionale: nel periodo 2006–2023 i capoluoghi di provincia italiani mostrano una chiara tendenza all’aumento della temperatura, mentre le precipitazioni sono molto più variabili e meno uniformi.

Emergono alcune conclusioni generali:

  • La media dei capoluoghi di provincia indica un riscaldamento costante nel periodo.
  • Le città più calde sono concentrate in Sicilia e nell’estremo Sud.
  • La classifica del “riscaldamento più rapido” dipende molto dalla completezza dei dati; filtrare le serie poco complete è quindi importante.
  • La tabella delle anomalie del 2023 aggiunge un importante contesto storico, mostrando che diverse città non sono state soltanto calde in termini assoluti, ma insolitamente calde rispetto ai riferimenti climatologici.

Un’estensione naturale dell’analisi sarebbe includere i fogli sui capoluoghi regionali (Tavola_9 e Tavola_10) e le tabelle degli estremi (Tavola_7, Tavola_8, Tavola_11Tavola_14) in un secondo articolo dedicato agli estremi di caldo, alle notti calde e agli indicatori di pioggia intensa.

Riproduci l’analisi

Preferisci eseguirla tu? Il notebook, il dataset ordinato e il repository sorgente sono disponibili per controllare i passaggi di pulizia, riutilizzare i dati o estendere i grafici.

Notebook

Esegui l’analisi completa in locale e riproduci le figure dell’articolo.

Scarica .ipynb

Dataset ordinato

Usa la tabella pulita provincia-anno con colonne di temperatura e precipitazioni.

Scarica .csv

Repository sorgente

Lascia una stella o fai fork del progetto GitHub collegato per continuare l’analisi.

Vedi su GitHub

Share this article