Smettere di guardare i dati e iniziare a leggerli
I dati grezzi sono rumore. Una lista di temperature o un foglio Excel pieno di vendite non sono “informazioni”, sono solo spazio occupato su un disco fisso. La differenza tra chi subisce i numeri e chi li governa sta nella capacità di estrarre una storia da quella confusione.
Python, con la libreria Pandas, non serve solo a fare calcoli più velocemente di una calcolatrice; serve a trovare il momento esatto in cui un trend si spezza o in cui un’anomalia smette di essere un errore e diventa un segnale d’allarme. In questo percorso passeremo dal calcolo di una semplice media settimanale alla caccia agli outlier in una serie temporale complessa, affrontando i problemi reali che un analista incontra ogni lunedì mattina.
Esercizio 1 (Facile): Analisi Base delle Temperature Giornaliere
Testo:
Sei un volontario meteorologico e devi analizzare le temperature medie giornaliere di una settimana (da lunedì a domenica). I dati raccolti sono: [22.5, 23.0, 21.8, 20.5, 24.2, 25.0, 23.5] °C.
-
Calcola la temperatura media settimanale
-
Trova il giorno con temperatura massima e minima
-
Identifica quanti giorni hanno superato i 23°C
Soluzione:
import pandas as pd
import matplotlib.pyplot as plt
# Passo 1: Preparazione dei dati
# Creiamo una Serie pandas con indice temporale
temperature = [22.5, 23.0, 21.8, 20.5, 24.2, 25.0, 23.5]
giorni = ['Lun', 'Mar', 'Mer', 'Gio', 'Ven', 'Sab', 'Dom']
# Creiamo una Serie pandas (struttura simile a un array ma con etichette)
serie_temp = pd.Series(temperature, index=giorni)
# Passo 2: Calcolo della temperatura media
# La media è la somma di tutti i valori diviso il numero di valori
media = serie_temp.mean()
print(f"Temperatura media settimanale: {media:.2f}°C")
# Passo 3: Trovare temperatura massima e minima
# Usiamo i metodi .max() e .min() di pandas
temp_max = serie_temp.max()
temp_min = serie_temp.min()
giorno_max = serie_temp.idxmax() # idxmax restituisce l'indice del valore massimo
giorno_min = serie_temp.idxmin() # idxmin restituisce l'indice del valore minimo
print(f"Temperatura massima: {temp_max}°C il {giorno_max}")
print(f"Temperatura minima: {temp_min}°C il {giorno_min}")
# Passo 4: Contare giorni sopra 23°C
# Creiamo una maschera booleana (True per valori > 23, False altrimenti)
mask = serie_temp > 23
giorni_sopra_23 = serie_temp[mask]
num_giorni_sopra_23 = len(giorni_sopra_23)
print(f"Giorni con temperatura > 23°C: {num_giorni_sopra_23}")
print(f"Questi giorni sono: {list(giorni_sopra_23.index)}")
# Passo 5: Visualizzazione
plt.figure(figsize=(10, 5))
plt.plot(giorni, temperature, 'bo-', linewidth=2, markersize=8, label='Temperature')
plt.axhline(y=media, color='r', linestyle='--', label=f'Media ({media:.1f}°C)')
plt.axhline(y=23, color='g', linestyle=':', label='Soglia 23°C')
plt.title('Temperature Settimanali', fontsize=14)
plt.xlabel('Giorno della settimana', fontsize=12)
plt.ylabel('Temperatura (°C)', fontsize=12)
plt.legend()
plt.grid(True, alpha=0.3)
plt.show()
💡 Osservazioni:
-
Una Serie pandas è un array monodimensionale etichettato, perfetto per dati temporali semplici
-
Le maschere booleane (come
serie_temp > 23) sono uno strumento potente per filtrare dati -
La visualizzazione immediata aiuta a comprendere rapidamente l’andamento dei dati
Domanda di riflessione: Perché è importante usare .idxmax() invece di .max() quando vogliamo sapere quando si è verificato il valore massimo?
Esercizio 2 (Facile-Intermedio): Analisi delle Vendite Mensili
Testo:
Sei un analista di un negozio che vende gelati. Hai i dati delle vendite mensili (in migliaia di euro) per l’anno 2023:
[15, 18, 25, 30, 45, 60, 75, 80, 50, 35, 20, 15].
-
Calcola le vendite totali annuali
-
Trova il mese con picco massimo e minimo di vendite
-
Calcola la variazione percentuale mese per mese
-
Identifica in quale trimestre si sono concentrate le vendite maggiori
Soluzione:
import pandas as pd
import numpy as np
# Passo 1: Creazione di un DataFrame con dati temporali
# I DataFrame sono tabelle bidimensionali con righe e colonne etichettate
mesi = ['Gen', 'Feb', 'Mar', 'Apr', 'Mag', 'Giu',
'Lug', 'Ago', 'Set', 'Ott', 'Nov', 'Dic']
vendite = [15, 18, 25, 30, 45, 60, 75, 80, 50, 35, 20, 15]
# Creiamo un DataFrame (tabulato)
df_vendite = pd.DataFrame({
'Mese': mesi,
'Vendite': vendite
})
# Passo 2: Calcolo vendite totali
# Sommiamo tutti i valori della colonna 'Vendite'
vendite_totali = df_vendite['Vendite'].sum()
print(f"Vendite totali annuali: {vendite_totali} mila euro")
# Passo 3: Trovare mesi di picco
mese_max = df_vendite.loc[df_vendite['Vendite'].idxmax(), 'Mese']
mese_min = df_vendite.loc[df_vendite['Vendite'].idxmin(), 'Mese']
print(f"Mese di picco massimo: {mese_max}")
print(f"Mese di picco minimo: {mese_min}")
# Passo 4: Calcolo variazione percentuale mese per mese
# La variazione percentuale si calcola come: ((valore_corrente - valore_precedente) / valore_precedente) * 100
df_vendite['Variazione %'] = df_vendite['Vendite'].pct_change() * 100
print("\nVariazioni percentuali mensili:")
print(df_vendite[['Mese', 'Variazione %']].round(2))
# Passo 5: Analisi trimestrale
# Raggruppiamo i mesi in trimestri
trimestri = {
'Q1': ['Gen', 'Feb', 'Mar'],
'Q2': ['Apr', 'Mag', 'Giu'],
'Q3': ['Lug', 'Ago', 'Set'],
'Q4': ['Ott', 'Nov', 'Dic']
}
# Calcoliamo le vendite per trimestre
for trimestre, mesi_trim in trimestri.items():
vendite_trim = df_vendite[df_vendite['Mese'].isin(mesi_trim)]['Vendite'].sum()
print(f"{trimestre}: {vendite_trim} mila euro")
# Passo 6: Visualizzazione avanzata
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(15, 5))
# Grafico a barre delle vendite
ax1.bar(mesi, vendite, color='skyblue', edgecolor='black')
ax1.set_title('Vendite Mensili di Gelati 2023', fontsize=14)
ax1.set_xlabel('Mese')
ax1.set_ylabel('Vendite (migliaia di €)')
ax1.grid(True, alpha=0.3)
# Grafico a linee della variazione percentuale
ax2.plot(mesi[1:], df_vendite['Variazione %'].dropna(), 'ro-', linewidth=2)
ax2.axhline(y=0, color='black', linestyle='-', linewidth=0.5)
ax2.set_title('Variazione Percentuale Mensile', fontsize=14)
ax2.set_xlabel('Mese')
ax2.set_ylabel('Variazione %')
ax2.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
💡 Osservazioni:
-
pct_change()calcola automaticamente la variazione percentuale tra valori consecutivi -
Il raggruppamento per categorie (trimestri) permette analisi a più livelli
-
I subplot permettono di visualizzare più aspetti dei dati contemporaneamente
Domanda di riflessione: Perché la variazione percentuale di gennaio è NaN (Not a Number)?
Esercizio 3 (Intermedio): Analisi della Serie Temporale con Date Reali
Testo:
Analizza il consumo giornaliero di elettricità (in kWh) di un ufficio per 30 giorni consecutivi. I dati mostrano un pattern settimanale (lavorativo vs weekend) e alcune anomalie.
-
Crea una serie temporale con date reali
-
Identifica i weekend (sabato e domenica)
-
Calcola la media dei consumi nei giorni lavorativi vs weekend
-
Trova e analizza eventuali outlier (valori anomali)
Soluzione:
import pandas as pd
import numpy as np
from datetime import datetime, timedelta
# Passo 1: Creazione di una serie temporale con date reali
# Partiamo da una data specifica e creiamo 30 giorni consecutivi
data_inizio = datetime(2024, 1, 1)
date_range = [data_inizio + timedelta(days=i) for i in range(30)]
# Simuliamo dati di consumo con pattern settimanale + rumore + anomalie
np.random.seed(42) # Per risultati riproducibili
consumo_base = 100 # kWh base
# Pattern: più alto nei giorni lavorativi (lun-ven), più basso nei weekend
consumi = []
for i, data in enumerate(date_range):
giorno_settimana = data.weekday() # 0=lun, 1=mar, ..., 5=sab, 6=dom
if giorno_settimana < 5: # Giorno lavorativo
base = 150 + np.random.normal(0, 10) # Media 150, deviazione 10
else: # Weekend
base = 50 + np.random.normal(0, 5) # Media 50, deviazione 5
# Aggiungiamo alcune anomalie
if i == 7: # Un giorno molto alto
base = 300
elif i == 14: # Un giorno molto basso
base = 20
consumi.append(base)
# Passo 2: Creazione del DataFrame con DateTimeIndex
# Un DateTimeIndex è un indice specializzato per date/ore
df_consumo = pd.DataFrame({
'Data': date_range,
'Consumo_kWh': consumi,
'Giorno_Settimana': [d.weekday() for d in date_range],
'Nome_Giorno': [d.strftime('%A') for d in date_range]
})
# Impostiamo la colonna Data come indice
df_consumo.set_index('Data', inplace=True)
# Passo 3: Identificazione dei weekend
# Creiamo una colonna booleana per i weekend
df_consumo['Is_Weekend'] = df_consumo['Giorno_Settimana'].isin([5, 6]) # 5=sab, 6=dom
# Passo 4: Calcolo medie separate
media_lavorativi = df_consumo[~df_consumo['Is_Weekend']]['Consumo_kWh'].mean()
media_weekend = df_consumo[df_consumo['Is_Weekend']]['Consumo_kWh'].mean()
print(f"Consumo medio giorni lavorativi: {media_lavorativi:.2f} kWh")
print(f"Consumo medio weekend: {media_weekend:.2f} kWh")
print(f"Rapporto lavorativi/weekend: {media_lavorativi/media_weekend:.2f}")
# Passo 5: Identificazione outlier con metodo statistico
# Un outlier è un valore che si discosta significativamente dagli altri
Q1 = df_consumo['Consumo_kWh'].quantile(0.25) # Primo quartile (25%)
Q3 = df_consumo['Consumo_kWh'].quantile(0.75) # Terzo quartile (75%)
IQR = Q3 - Q1 # Intervallo interquartile
# Definizione outlier: valori sotto Q1 - 1.5*IQR o sopra Q3 + 1.5*IQR
limite_inferiore = Q1 - 1.5 * IQR
limite_superiore = Q3 + 1.5 * IQR
outliers = df_consumo[
(df_consumo['Consumo_kWh'] < limite_inferiore) | (df_consumo['Consumo_kWh'] > limite_superiore)
]
print(f"\nOutlier identificati ({len(outliers)}):")
print(outliers[['Consumo_kWh', 'Nome_Giorno']])
# Passo 6: Visualizzazione completa
fig, axes = plt.subplots(2, 2, figsize=(15, 10))
# Grafico 1: Serie temporale completa
axes[0, 0].plot(df_consumo.index, df_consumo['Consumo_kWh'], 'b-', linewidth=1, marker='o', markersize=4)
axes[0, 0].set_title('Consumo Elettrico Giornaliero', fontsize=14)
axes[0, 0].set_xlabel('Data')
axes[0, 0].set_ylabel('Consumo (kWh)')
axes[0, 0].grid(True, alpha=0.3)
# Evidenziamo i weekend
weekend_data = df_consumo[df_consumo['Is_Weekend']]
axes[0, 0].scatter(weekend_data.index, weekend_data['Consumo_kWh'],
color='red', s=50, label='Weekend', zorder=5)
# Grafico 2: Boxplot per confronto lavorativi/weekend
data_per_boxplot = [
df_consumo[~df_consumo['Is_Weekend']]['Consumo_kWh'],
df_consumo[df_consumo['Is_Weekend']]['Consumo_kWh']
]
axes[0, 1].boxplot(data_per_boxplot, labels=['Lavorativi', 'Weekend'])
axes[0, 1].set_title('Distribuzione Consumi: Lavorativi vs Weekend', fontsize=14)
axes[0, 1].set_ylabel('Consumo (kWh)')
axes[0, 1].grid(True, alpha=0.3)
# Grafico 3: Media per giorno della settimana
giorni_ordine = ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday']
media_per_giorno = df_consumo.groupby('Nome_Giorno')['Consumo_kWh'].mean()
media_per_giorno = media_per_giorno.reindex(giorni_ordine)
axes[1, 0].bar(media_per_giorno.index, media_per_giorno.values, color='lightgreen')
axes[1, 0].set_title('Consumo Medio per Giorno della Settimana', fontsize=14)
axes[1, 0].set_xlabel('Giorno')
axes[1, 0].set_ylabel('Consumo Medio (kWh)')
plt.setp(axes[1, 0].xaxis.get_majorticklabels(), rotation=45)
# Grafico 4: Istogramma della distribuzione
axes[1, 1].hist(df_consumo['Consumo_kWh'], bins=15, edgecolor='black', alpha=0.7)
axes[1, 1].axvline(x=media_lavorativi, color='blue', linestyle='--', label=f'Media lav.: {media_lavorativi:.1f}')
axes[1, 1].axvline(x=media_weekend, color='red', linestyle='--', label=f'Media week.: {media_weekend:.1f}')
axes[1, 1].set_title('Distribuzione dei Consumi', fontsize=14)
axes[1, 1].set_xlabel('Consumo (kWh)')
axes[1, 1].set_ylabel('Frequenza')
axes[1, 1].legend()
axes[1, 1].grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
💡 Osservazioni:
-
DateTimeIndexpermette operazioni temporali avanzate (raggruppamento per periodo, shift temporali, ecc.) -
Il metodo IQR (Interquartile Range) è robusto per identificare outlier
-
Il raggruppamento per categorie temporali (giorni della settimana) rivela pattern nascosti
Domanda di riflessione: Perché il metodo IQR è preferibile alla deviazione standard per identificare outlier in serie temporali?
Risposte alle Domande di Riflessione
Esercizio 1:
Domanda: Perché è importante usare .idxmax() invece di .max() quando vogliamo sapere quando si è verificato il valore massimo?
Risposta: .max() restituisce solo il valore numerico massimo (es. 25.0°C), mentre .idxmax() restituisce l’etichetta (index) associata a quel valore massimo (es. ‘Sab’). Questo è fondamentale nell’analisi temporale perché spesso ci interessa sapere non solo quanto ma anche quando si è verificato un evento. Nel contesto temporale, il “quando” è spesso più informativo del “quanto”.
Esercizio 2:
Domanda: Perché la variazione percentuale di gennaio è NaN (Not a Number)?
Risposta: La variazione percentuale per gennaio è NaN perché pct_change() calcola la variazione rispetto al valore precedente. Per il primo elemento della serie (gennaio) non esiste un “valore precedente” con cui confrontarlo. Questo è matematicamente corretto: la variazione percentuale è definita come ((valore_corrente - valore_precedente) / valore_precedente) × 100. Se non esiste un valore precedente, l’operazione non è definita. In analisi temporali, questo è comune per il primo punto dati.
Esercizio 3:
Domanda: Perché il metodo IQR è preferibile alla deviazione standard per identificare outlier in serie temporali?
Risposta: Il metodo IQR (Interquartile Range) è più robusto perché:
-
Non è influenzato da outlier estremi: La deviazione standard usa la media, che è sensibile a valori estremi. L’IQR usa i quartili (mediana), che sono più resistenti.
-
Funziona bene con distribuzioni non normali: Molte serie temporali non seguono una distribuzione normale, mentre l’IQR non assume normalità.
-
Soglie fisse: I limiti
Q1 - 1.5×IQReQ3 + 1.5×IQRsono standard e interpretabili. -
Preserva la struttura temporale: Identifica outlier senza distorcere la stima della variabilità.
⚠️ Errori Tipici nell’Analisi Dati con Pandas
Confondere .max() con .idxmax()
.max()restituisce il valore massimo → es.25.0.idxmax()restituisce l’etichetta dell’indice in cui si trova il massimo → es."Sab"
💡 Errore comune: usare .max() quando si vuole sapere quando è avvenuto il picco.
Gen → NaN
Feb → variazione rispetto a Gen
Mar → variazione rispetto a Feb
…
Usare liste Python invece di Serie Pandas per operazioni statistiche
Le liste sono contenitori generici. Le Serie Pandas sono strutture numeriche ottimizzate con metodi integrati:
.mean().sum().std().idxmax()- filtri booleani
- indicizzazione etichettata
💡 Errore comune: fare calcoli manuali su liste quando Pandas può farlo in un’unica riga.
Dimenticare che pct_change() produce NaN nel primo valore
pct_change() calcola la variazione rispetto all’elemento precedente. Il primo elemento non ha un precedente, quindi:
Analisi Tecnica e Applicativa
Esercizio 1: La base del monitoraggio
Perché è interessante: Qui non impariamo solo la sintassi. La distinzione tra .max() e .idxmax() è il primo passo verso l’automazione del reporting.
Il “Cosa” contro il “Quando”: .max() vs .idxmax()
In un contesto di monitoraggio professionale, sapere quanto è alto un picco è solo metà della storia. L’altra metà, spesso la più importante, è sapere quando è successo.
-
.max()(Il Quanto): È un valore statico. Ti dice che il server ha raggiunto i 95°C. È utile per la statistica descrittiva e per capire i limiti fisici di un sistema. -
.idxmax()(Il Quando): È un valore dinamico. Ti dice che il server ha toccato i 95°C martedì alle 14:30.
Perché fa la differenza?
Se sei un analista, il “quando” ti permette di fare correlazioni. Se il picco di calore avviene sempre alle 14:30, potresti scoprire che coincide con l’avvio di un backup pesante o con l’esposizione diretta del sensore al sole. .idxmax() trasforma un numero in un indizio investigativo.
Peculiarità: L’uso della maschera booleana è il “coltello svizzero” del Data Scientist. In un contesto reale, questo serve per i sistemi di allerta (es. invio notifica se la temperatura supera la soglia critica).
Efficienza e Intento
L’operazione serie_temp > 23 non è un semplice filtro, ma una vettorizzazione.
Come funziona “umanamente”:
Invece di scorrere i dati uno per uno (come farebbe un umano o un ciclo for in altri linguaggi), Pandas crea un “velo” (la maschera) della stessa dimensione dei tuoi dati. Dove la condizione è vera, il velo è trasparente; dove è falsa, il velo è coprente.
Perché è il “coltello svizzero”:
-
Velocità: Su milioni di righe, un ciclo
forimpiegherebbe secondi; la maschera booleana millisecondi. -
Pulizia: Permette di scrivere codice che sembra linguaggio naturale.
serie_temp[serie_temp > 23]si legge quasi come “Prendi le temperature dove la temperatura è maggiore di 23”. -
Versatilità: Puoi combinare più maschere (es. “temperature > 23” AND “giorno = weekend”) per isolare scenari complessi con una riga di codice.
Dall’Esercizio alla Realtà: I Sistemi di Allerta
L’esercizio 1 simula la logica di un SLA (Service Level Agreement) o di un monitoraggio industriale.
Esempio: Immagina di monitorare la temperatura di un frigorifero farmaceutico che conserva vaccini.
Non ti serve un grafico a fine mese che ti dica “è andato tutto bene”.
Ti serve uno script che ogni 5 minuti applichi una maschera booleana.
Se
len(giorni_sopra_soglia) > 0, il sistema non deve stampare un grafico, ma inviare un Telegram al tecnico di turno.
L’importanza della soglia (Thresholding)
Nell’esercizio abbiamo usato 23°C. In un sistema reale, identificare la soglia corretta è un’arte:
-
Troppo bassa: Generi “fatica da allerta” (troppe notifiche inutili, il tecnico inizia a ignorarle).
-
Troppo alta: Rilevi il problema quando è troppo tardi (i vaccini sono già degradati).
Il monitoraggio base è il primo gradino verso la Manutenzione Predittiva. Oggi usiamo
.max()per vedere cosa è successo. Domani useremo questi stessi dati storici per addestrare un modello che ci dica: “Ehi, basandomi sulla velocità con cui la temperatura è salita nelle ultime due ore, supereremo la soglia di 23°C tra circa 15 minuti. Intervieni ora.”
Esercizio 2: Il business che respira
Perché è interessante: Le aziende non vivono di numeri assoluti, ma di variazioni. Il concetto di “stagionalità” (il picco estivo dei gelati) unito alla variazione percentuale trasforma un database in una strategia di marketing.
Peculiarità: Il raggruppamento per trimestri (Q1-Q4) simula esattamente come vengono presentati i report agli investitori o ai titolari di azienda.
Esercizio 3: La realtà è sporca
Perché è interessante: Questo è l’esercizio più “adulto”. I dati reali contengono rumore, weekend e guasti ai sensori (outlier). Saper distinguere tra un lunedì produttivo e un picco energetico anomalo salva le aziende da costi inutili.
Peculiarità: L’uso dell’IQR (Interquartile Range) invece della deviazione standard mostra una maturità statistica: non ci fidiamo della media perché “un uomo con i piedi nel forno e la testa nel congelatore ha una temperatura media perfetta, ma è morto”. L’IQR ci dice cosa succede davvero nel cuore della distribuzione.
Pandas & serie temporali
👉 Python: la libreria Pandas, le Series e i DataFrame
👉 Pandas: statistiche con le Series
👉 Pandas Series vs NumPy array: la differenza pratica
👉 6 esercizi pratici di serie temporali con Pandas
👉 Introduzione al modello ARIMA per serie temporali
👉 Padroneggiare le serie temporali con Pandas: datetime e aggregazioni
👉 Guida alla decomposizione di serie temporali con STL (statsmodels)



