La Distribuzione Empirica dei Dati: Rappresentazione Grafica
Parte (2/2)
Potrebbe interessarti:
Il progetto Python, che comprende gli script mostrati in questo post, e quelli relativi alla creazione dei grafici Matplotlib, è disponibile assieme al corso Fondamenti di Statistica per il ML.
Per comprendere pienamente un dataset, possiamo ricorrere a rappresentazioni grafiche, che trasformano i valori in una forma visiva interpretabile con maggior facilità. Esaminiamo ora i principali strumenti di visualizzazione, distinguendo tra variabili discrete e continue.
Distribuzioni Discrete
Per visualizzare una distribuzione di dati discreta, è necessario usare grafici che mettono in risalto la separazione tra le categorie o i singoli valori, evitando di suggerire una continuità che non esiste.
Grafico a Barre (Bar Chart)
Il grafico a barre è lo strumento principale per la rappresentazione di distribuzioni discrete. È particolarmente adatto per:
variabili nominali:
categorie distinte che non seguono un ordine logico (es. il colore degli occhi, o il sesso).variabili ordinali:
categorie ordinabili secondo una gerarchia o una classificazione (es.: livello di soddisfazione: basso, medio, alto).
In questo tipo di grafico:
- ogni barra corrisponde a uno specifico valore della variabile $x_i$;
- l’altezza della barra è proporzionale alla frequenza assoluta ($n_i$) oppure relativa ($f_i$) di quel valore;
- le barre sono separate da spazi, a indicare la natura discreta dei dati, ed il fatto che non esistono dati intermedi.
L’ordine delle barre è rilevante per variabili ordinali, mentre è arbitrario per variabili nominali.
Riprendiamo questo semplice dataset di esempio (già visto nella prima parte), contenente cinque punteggi di un test. Abbiamo 4 valori distinti: $18$, $20$, $22$ e $25$. Calcoliamo le frequenze assolute e relative:
| Valore (Classe) | Frequenza Assoluta | Frequenza Relativa |
|---|---|---|
| 18 | 1 | 0.2 |
| 20 | 2 | 0.4 |
| 22 | 1 | 0.2 |
| 25 | 1 | 0.2 |
| Totale | 5 | 1 |
Il relativo grafico a barre con le frequenze assolute è mostrato di seguito:
from collections import Counter
import numpy as np
import matplotlib.pyplot as plt
# mostra frequenze relative o assolute
SHOW_RELATIVE_FREQ = False
# Dataset
data = [18, 20, 20, 22, 25]
# Calcolo delle frequenze assolute.
# Counter è una sottoclasse della classe "dict" di Python, appartenente al modulo "collections".
# Viene utilizzata principalmente per contare la frequenza degli elementi
# presenti in un iterabile o in un dizionario. Per ogni valore distinto del dataset originale,
# Counter associa la sua frequenza assoluta. Ad esempio:
# Counter({18: 1, 20:2, 22:1, 25: 1})
freq = Counter(data)
# freq_keys è una view "dict_keys" delle chiavi di Counter.
# Gli oggetti “dict_keys” sono proxy di sola lettura degli oggetti dictionary sottostanti
# https://docs.python.org/3/library/stdtypes.html#dict-views
freq_keys_sorted = sorted(freq.keys())
y = [freq[freq_key] for freq_key in freq_keys_sorted]
y_label = "Frequenza Assoluta"
if SHOW_RELATIVE_FREQ:
y = np.array(y) / len(data)
y_label = "Frequenza Relativa"
plt.figure(figsize=(12, 5))
# Grafico a barre:
# La funzione bar() accetta argomenti che descrivono la disposizione delle barre.
# Le categorie e i relativi valori sono rappresentati dal primo e dal secondo argomento
# sotto forma di array (x e height)
plt.bar(x=freq_keys_sorted, height=y, width=0.8, color="C0")
plt.title("Distribuzione Discreta")
plt.xlabel("Valore")
plt.ylabel(y_label)
plt.xticks(freq_keys_sorted)
plt.grid(axis="y", ls="--", alpha=0.3)
plt.tight_layout()
plt.show()

Se fossimo interessati alle frequenze relative, possiamo riusare il codice precedente,
settando la variabile SHOW_RELATIVE_FREQ = True:

Grafico a Punti (Dot Plot)
Il grafico a punti è una rappresentazione semplice ed efficace, particolarmente adatta a dataset di piccole dimensioni. In questo grafico, ogni osservazione è rappresentata da un punto sull’asse orizzontale, e punti sovrapposti indicano frequenze maggiori. Permette di individuare facilmente la concentrazione e la dispersione dei dati, ed eventuali outlier. Consideriamo il seguente dataset di prezzi, in dollari, di 16 lettori DVD:
$$\{210, 219, 214, 197, 224, 219, 199, 199, 208, 209, 215, 199, 212, 212, 219, 210 \}$$import numpy as np
import matplotlib.pyplot as plt
# Dataset
x = np.array([210, 219, 214, 197, 224, 219, 199, 199, 208, 209, 215, 199, 212, 212, 219, 210])
# Ordiniamo i prezzi
x = np.sort(x)
# Calcoliamo la coordinata Y di ogni osservazione
y = np.zeros(len(x), dtype=int)
for i, x_i in enumerate(x):
# l'osservazione attuale quante volte è stata "incontrata" finora?
x_precedenti = x[:i]
conteggio = np.sum(x_precedenti == x_i) + 1
y[i] = conteggio
# Creazione del grafico
fig, ax = plt.subplots(figsize=(12, 4))
# Traccia un grafico a dispersione (scatterplot) di y in funzione di x
ax.scatter(x, y, color="C0", edgecolors="k", s=100)
# Etichette assi
ax.set_xlabel("Prezzo (€)")
ax.set_ylabel("Frequenza")
# Titolo
ax.set_title("Dot Plot dei prezzi dei lettori DVD")
# Setta tick e limiti per gli assi x e y
x_min, x_max = 190, 230
y_min, y_max = 0, 5
ax.set_xticks(np.arange(x_min, x_max + 1, 2))
ax.set_yticks(np.arange(y_min, y_max + 1, 1))
ax.set_xlim((x_min, x_max))
ax.set_ylim((y_min, y_max))
ax.grid(True, ls="--", alpha=0.3)
plt.tight_layout()
plt.show()

Distribuzioni Continue
Per variabili continue si utilizzano grafici che rappresentano i dati lungo un intervallo numerico continuo, evidenziandone la distribuzione complessiva.
Istogramma
L’istogramma è il grafico più utilizzato per rappresentare distribuzioni di dati continui. In una distribuzione continua, i valori possibili sono infiniti. Per questo motivo, a differenza del grafico a barre, l’istogramma raggruppa i dati in intervalli adiacenti (chiamati classi o bin); le barre risultano quindi unite tra loro per comunicare visivamente la natura continua della variabile.
Per costruire un istogramma con un certo numero di classi, dobbiamo:
- determinare il numero di osservazioni;
- calcolare l’intervallo dei dati (range);
- determinare l’ampiezza delle classi;
- definire gli intervalli dei valori (classi o “bin”);
- contare le osservazioni in ogni classe (quanti dati cadono in ogni intervallo);
- costruire la tabella delle frequenze, i cui valori verranno rappresentati con barre contigue.
Consideriamo ad esempio il seguente dataset:
$${0.2, 0.5, 1.2, 1.5, 1.7, 1.8, 2.0, 2.1, 2.2, 2.4, 2.5, 2.7, 2.8, 3.0, 3.1, 3.1, 3.2, 3.5, 4.0, 4.0, 4.2, 4.5, 5.5}$$Abbiamo $23$ osservazioni.
L’intervallo $R$ è uguale a:
$$R = x_{\text{max}} - x_{\text{min}} = 5.5 – 0.2 = 5.3$$Volendo costruire $k = 6$ classi, l’ampiezza teorica di ogni classe o bin è:
$$h = \frac{R}{k} = \frac{5.3}{6} \approx 0.9$$Partendo dal valore minimo $0.2$, otteniamo i seguenti estremi:
$$0.2, 1.1, 2.0, 2.9, 3.8, 4.7, 5.6$$Le 6 classi sono quindi:
$$ \begin{align*} [0.2; 1.1)\\ [1.1; 2.0)\\ [2.0; 2.9)\\ [2.9; 3.8)\\ [3.8; 4.7)\\ [4.7; 5.6] \end{align*} $$La notazione $[a; b)$ indica che l’estremo sinistro è incluso, mentre quello destro è escluso. Nell’ultima classe includiamo anche l’estremo destro, per essere certi di comprendere il valore massimo.
Contiamo ora le osservazioni che cadono in ogni classe, e costruiamo la seguente tabella di frequenza. La frequenza relativa è la frequenza assoluta divisa per il numero di osservazioni:
| Classe | Osservazioni Contenute | Frequenza assoluta | Frequenza relativa |
|---|---|---|---|
| [0.2; 1.1) | 0.2, 0.5 | 2 | 0.0870 |
| [1.1; 2.0) | 1.2, 1.5, 1.7, 1.8 | 4 | 0.1739 |
| [2.0; 2.9) | 2.0, 2.1, 2.2, 2.4, 2.5, 2.7, 2.8 | 7 | 0.3043 |
| [2.9; 3.8) | 3.0, 3.1, 3.1, 3.2, 3.5 | 5 | 0.2174 |
| [3.8; 4.7) | 4.0, 4.0, 4.2, 4.5 | 4 | 0.1739 |
| [4.7; 5.6] | 5.5 | 1 | 0.0435 |
| Totale | 23 | 1 |
Il relativo istogramma delle frequenze assolute è mostrato di seguito:
import numpy as np
import matplotlib.pyplot as plt
# mostra frequenze relative o assolute
SHOW_RELATIVE_FREQ = False
# Dataset
data = np.array([0.2, 0.5, 1.2, 1.5, 1.7, 1.8, 2.0, 2.1, 2.2, 2.4, 2.5, 2.7,
2.8, 3.0, 3.1, 3.1, 3.2, 3.5, 4.0, 4.0, 4.2, 4.5, 5.5])
# Numero di classi
bins = 6
y_label = "Frequenza Assoluta"
# "weights" è un array di pesi, della stessa forma di x.
# Ogni valore in x contribuisce al conteggio del bin solo con il peso a esso associato
weights = np.ones(len(data))
if SHOW_RELATIVE_FREQ:
y_label = "Frequenza Relativa"
weights = weights / len(data)
plt.figure(figsize=(12, 5))
counts, bin_edges, _ = plt.hist(data, bins=bins, weights=weights, color="C0", edgecolor='black', alpha=0.5)
# Etichette e titolo
plt.xlabel('Classi di Valori')
plt.ylabel(y_label)
plt.title('Istogramma')
plt.grid(True, ls="--", alpha=0.3)
plt.tight_layout()
plt.show()

In matplotlib.pyplot.hist, un istogramma di frequenza assoluta mostra il conteggio grezzo delle osservazioni in ciascun intervallo, mentre un istogramma di frequenza relativa mostra la proporzione (o percentuale) del dataset che rientra in ciascun intervallo.
Di default, plt.hist() genera un grafico a frequenza assoluta.
Per tracciare un grafico a frequenza relativa, in cui l’altezza complessiva di tutte le barre
sia pari a 1 (o al 100%), è necessario assegnare pesi frazionari ai punti dati utilizzando il parametro weights.
Si noti che l’uso di density=True viene spesso confuso con la frequenza relativa,
ma in realtà traccia la densità di probabilità
(in cui l’area totale delle barre è pari a 1, non alla somma delle loro altezze).
Per ottenere la vera frequenza relativa, l’approccio più accurato consiste nell’utilizzare l’argomento weights.
In questo istogramma, l’asse y rappresenta il numero di osservazioni presenti in un dato bin. Ad esempio: “nel terzo bin sono presenti 7 osservazioni”. L’istogramma delle frequenze relative è molto simile:

In questo caso, l’asse y rappresenta la frazione del campione che cade in un dato bin, rispetto alla totalità del campione. Ad esempio: “circa il $30%$ delle osservazioni appartiene al terzo bin”. L’ istogramma permette di osservare facilmente la forma della distribuzione, ed individuare simmetria o asimmetria, picchi della distribuzione, e dispersione dei dati.
Poligono di Frequenza
Per rappresentare in modo più chiaro l’andamento di una distribuzione empirica, si può affiancare all’istogramma il poligono di frequenza, una linea spezzata che ne approssima la forma. Il poligono di frequenza si costruisce partendo dallo stesso istogramma: si costruisce calcolando il punto medio di ogni classe $(m_j, n_j)$ oppure $(m_j, f_j)$ dove:
$$m_j = \frac{a_j + a_{j + 1}}{2}$$dove:
- $a_j$ è l’estremo inferiore dell’intervallo della classe $j$ (cioè l’inizio della barra sull’asse $x$);
- $a_{j+1}$ è l’estremo superiore della stessa classe (cioè la fine della barra);
- $m_j$ è il punto centrale di questo intervallo.
Unendo questi punti con segmenti consecutivi, si ottiene una linea che segue il profilo delle barre dell’istogramma. Per il dataset precedente, possiamo disegnare il seguente poligono di frequenza:
import numpy as np
import matplotlib.pyplot as plt
# Dataset
data = np.array([0.2, 0.5, 1.2, 1.5, 1.7, 1.8, 2.0, 2.1, 2.2, 2.4, 2.5, 2.7,
2.8, 3.0, 3.1, 3.1, 3.2, 3.5, 4.0, 4.0, 4.2, 4.5, 5.5])
# Numero di classi (puoi modificarlo)
bins = 6
plt.figure(figsize=(12, 5))
# Istogramma
counts, bin_edges, _ = plt.hist(data, bins=bins, color="C0", alpha=0.5, edgecolor='black',
label="Istogramma")
# Calcolo dei punti centrali delle classi
bin_centers = 0.5 * (bin_edges[1:] + bin_edges[:-1])
# Poligono di frequenza
plt.plot(bin_centers, counts, marker='o', color="orangered", linestyle='-', linewidth=3,
label="Poligono di Frequenza")
# Etichette e titolo
plt.xlabel('Classi di Valori')
plt.ylabel('Frequenza Assoluta')
plt.title('Istogramma e Poligono di Frequenza')
plt.legend()
plt.grid(True, ls="--", alpha=0.3)
plt.tight_layout()
plt.show()

Rispetto all’istogramma, il poligono di frequenza offre una rappresentazione più sintetica e continua (anche se ancora composta da segmenti), simile a una curva, che aiuta a cogliere rapidamente l’andamento globale dei dati. Permette inoltre di confrontare più distribuzioni sullo stesso grafico, in modo più semplice rispetto agli istogrammi sovrapposti.
Curva KDE
Per descrivere in modo chiaro l’aspetto geometrico di una distribuzione empirica, può essere utile affiancare all’istogramma una curva “liscia” che ne riassuma l’andamento complessivo. Una tecnica particolarmente efficace a questo scopo è la Kernel Density Estimation (KDE).
In parole semplici, il metodo KDE costruisce una curva continua che approssima la distribuzione dei dati senza suddividerli in classi rigide, come avviene nell’istogramma. Mentre l’istogramma rappresenta i dati tramite barre (la cui forma dipende dalla scelta dei bin), la KDE associa a ciascun punto osservato una piccola “campana” (detta kernel), e poi somma tutte queste contribuzioni. Il risultato è una curva fluida che segue l’andamento generale dei dati.
Considerando il dataset precedente, possiamo disegnare la seguente curva KDE:
import numpy as np
import matplotlib.pyplot as plt
from scipy.stats import gaussian_kde
data = np.array([
0.2, 0.5, 1.2, 1.5, 1.7, 1.8, 2.0, 2.1, 2.2, 2.4, 2.5,
2.7, 2.8, 3.0, 3.1, 3.1, 3.2, 3.5, 4.0, 4.0, 4.2, 4.5, 5.5
])
# Numero di bin
bins = 6
plt.figure(figsize=(12, 5))
# Istogramma (frequenze assolute)
counts, bin_edges, _ = plt.hist(data, bins=bins, alpha=0.5, edgecolor='black', label='Istogramma')
# KDE (densità)
kde = gaussian_kde(data)
x_vals = np.linspace(data.min() - 0.5, data.max() + 0.5, 500)
y_vals = kde(x_vals)
# Scala la KDE sulle frequenze:
# Moltiplicando y_vals per (n * ⋅bin_width),
# trasformiamo la KDE in una curva che approssima le frequenze attese per classe,
# rendendola direttamente confrontabile con l’istogramma
n = len(data)
bin_width = bin_edges[1] - bin_edges[0]
y_vals_scaled = y_vals * n * bin_width
# Plot KDE scalata
plt.plot(x_vals, y_vals_scaled, color="orangered", linewidth=3, label='KDE scalata (frequenze)')
# Etichette e titolo
plt.xlabel('Classi di Valori')
plt.ylabel('Frequenza Assoluta')
plt.title('Istogramma con KDE scalata sulle frequenze')
plt.legend()
plt.grid(True, ls="--", alpha=0.3)
plt.tight_layout()
plt.show()

Dal punto di vista geometrico, tale curva svolge un ruolo simile al poligono di frequenza: entrambi forniscono una rappresentazione più regolare rispetto all’istogramma, e mettono in evidenza la forma globale della distribuzione (simmetria, asimmetria, presenza di più picchi).
Tuttavia, il metodo KDE presenta un vantaggio importante: non dipende da una suddivisione arbitraria in classi, ma produce una curva continua che varia in modo graduale.