La grammatica dei grafici con ggplot2

Statistical Computing con R

Domenico Vistocco

28 ottobre 2026

Introduzione

Il piano di oggi

Blocco Contenuto
Apertura da R base alla grammatica dei grafici
Attività 1-4 dati, estetiche, geometrie, layer
Debug i tre errori più comuni
Attività 5-8 position, facet, scale, rifiniture e salvataggio
Sfida finale ricrea questo grafico
Chiusura riepilogo e prossimi passi

Materiale: questa presentazione, il file lezione_04-ggplot2-esercizi.qmd (su Teams) e i dati dell’esercitazione 1

Come lavoriamo oggi

Ogni concetto segue lo stesso ciclo, ispirato al modello PRIMM (Predict, Run, Investigate, Modify, Make; Sentance, Waite e Kallia, 2019):

  1. Osserva un esempio sui dati gapminder
  2. Anticipa che cosa produrrà il codice, prima di eseguirlo
  3. Esegui il codice e verifica se avevi ragione
  4. Modifica il codice per applicarlo ai dati delle biblioteche, con la scheda
  5. Confronta soluzioni, errori e sorprese con il resto della classe

Sbagliare l’anticipazione è utile: è il modo più rapido per capire come ragiona ggplot2. I punti Bonus sono per chi finisce prima.

Da R base a ggplot2

library(tidyverse)   # contiene anche ggplot2
library(janitor)
library(gapminder)
gap_data <- gapminder |>
  clean_names() |>
  rename(gdp_per_cap = gdp_percap)
gap_2007 <- gap_data |>
  filter(year == 2007)

R base: la penna sul foglio

plot(gap_2007$gdp_per_cap,
     gap_2007$life_exp,
     col = gap_2007$continent,
     pch = 19)
legend("bottomright",
       legend = levels(gap_2007$continent),
       col = 1:5, pch = 19)
  • ogni funzione disegna qualcosa sul grafico
  • colori, legenda, assi: li gestiamo noi

ggplot2: la grammatica

ggplot(gap_2007,
       aes(x = gdp_per_cap,
           y = life_exp,
           color = continent)) +
  geom_point()
  • descriviamo che cosa rappresentare
  • colori e legenda seguono dal mapping
  • il grafico è un oggetto che si costruisce a strati

La grammatica: i layer

Il risultato

ggplot(gapminder,
       aes(x = gdpPercap, y = lifeExp)) +
       geom_point(aes(color = continent)) +
       facet_wrap(~ continent) +
       geom_smooth(color = "black") +
       coord_cartesian(xlim = c(0, 40000)) +
       theme_minimal()

I layer si aggiungono con il +. La pipe |> serve solo per passare la tabella a ggplot().

1. Dati, estetiche, geometrie

Passo 1: i dati

ggplot(gap_2007)

Passo 2: le estetiche

ggplot(gap_2007,
       aes(x = gdp_per_cap,
           y = life_exp))

Passo 3: la geometria

ggplot(gap_2007,
       aes(x = gdp_per_cap,
           y = life_exp)) +
  geom_point()

E poi: altre estetiche

ggplot(gap_2007,
       aes(x = gdp_per_cap,
           y = life_exp,
           color = continent,
           size = pop)) +
  geom_point()

Che cosa succede?

Rispondi prima di eseguire il codice.

gap_2007 |>
  ggplot(aes(x = gdp_per_cap, y = life_exp)) |>
  geom_point()

Un errore: mapping must be created by aes(), con il suggerimento Did you use %>% or |> instead of +?

La pipe passa il grafico come primo argomento di geom_point(), che invece si aspetta un aes(). Tra i layer serve il +.

Attività 1

Modifica - Scheda, attività 1

Costruisci il grafico a barre del numero di utenti per categoria, con ggplot(), aes() e geom_bar().

  • Anticipa: che cosa succede con |> al posto del +?
  • Bonus: barre orizzontali

Attività 1: confronto

ggplot(utenti, aes(x = categoria)) +
  geom_bar()

Bonus: basta spostare la variabile sull’asse y

ggplot(utenti, aes(y = categoria)) +
  geom_bar()
  • l’asse y (count) non è una variabile della tabella: lo calcola geom_bar()
  • l’ordine delle barre è alfabetico

2. geom_bar() o geom_col()?

Contare o usare valori già calcolati?

geom_bar() conta le righe (dati grezzi)

ggplot(gap_2007, aes(x = continent)) +
  geom_bar()

geom_col() usa una y già calcolata

gap_2007 |>
  count(continent) |>
  ggplot(aes(x = continent, y = n)) +
  geom_col()

Attività 2

Modifica - Scheda, attività 2

Costruisci tab_categoria <- utenti |> tabyl(categoria) e rappresentala con geom_col().

  • Anticipa: che cosa succede con geom_bar() e la stessa aes(x = categoria, y = n)?
  • Bonus: ordina le barre dalla più alta alla più bassa (fct_infreq() o reorder())

Attività 2: confronto

ggplot(tab_categoria,
       aes(x = categoria, y = n)) +
  geom_col()

Bonus

ggplot(utenti,
       aes(x = fct_infreq(categoria))) +
  geom_bar()
ggplot(tab_categoria,
       aes(x = reorder(categoria, -n), y = n)) +
  geom_col()

geom_bar() con una y: errore stat_count() must only have an x or y aesthetic

3. Associare o fissare?

Mapping e setting

Mapping: dentro aes(), il colore dipende da una variabile

ggplot(gap_2007,
       aes(x = gdp_per_cap, y = life_exp)) +
  geom_point(aes(color = continent))

Setting: fuori da aes(), un valore fisso per tutti

ggplot(gap_2007,
       aes(x = gdp_per_cap, y = life_exp)) +
  geom_point(color = "steelblue")

Che cosa succede?

Di che colore saranno i punti? Ci sarà una legenda?

ggplot(gap_2007, aes(x = gdp_per_cap, y = life_exp)) +
  geom_point(aes(color = "steelblue"))

Attività 3

Modifica - Scheda, attività 3

Rappresenta eta (x) e anno_iscrizione (y) con geom_point() in tre versioni:

  1. geom_point(aes(color = "blue"))
  2. geom_point(color = "blue")
  3. color = categoria dentro aes()

Che cosa dice il messaggio di avvertimento? Bonus: in (c) fissa size = 3 e alpha = 0.6

Attività 3: confronto

  1. mapping di una costante

  1. setting

  1. mapping di una variabile

Removed 5 rows containing missing values: sono i 5 utenti senza anno di nascita. ggplot2 li esclude e lo segnala: è un avvertimento, non un errore.

4. I layer si sovrappongono

Più geometrie nello stesso grafico

ggplot(gap_2007,
       aes(x = continent,
           y = life_exp)) +
  geom_boxplot() +
  geom_jitter(width = 0.2,
              alpha = 0.5)

Più geometrie nello stesso grafico

Le estetiche in ggplot() valgono per tutti i layer, quelle in un geom_ solo per quel layer

ggplot(gap_2007,
       aes(x = gdp_per_cap, y = life_exp,
           color = continent)) +
  geom_point() +
  geom_smooth(se = FALSE)

ggplot(gap_2007,
       aes(x = gdp_per_cap, y = life_exp)) +
  geom_point(aes(color = continent)) +
  geom_smooth(se = FALSE)

Attività 4

Modifica - Scheda, attività 4

Distribuzione dell’eta per categoria: geom_boxplot() + geom_jitter(width = 0.2, alpha = 0.5).

  • Anticipa: che cosa cambia invertendo l’ordine dei due layer?
  • Bonus: geom_violin() al posto del boxplot

Attività 4: confronto

ggplot(utenti, aes(x = categoria, y = eta)) +
  geom_boxplot() +
  geom_jitter(width = 0.2, alpha = 0.5)

ggplot(utenti, aes(x = categoria, y = eta)) +
  geom_jitter(width = 0.2, alpha = 0.5) +
  geom_boxplot()

Debug

Trova l’errore

Modifica - Prima di eseguire, individua l’errore. Poi esegui, leggi il messaggio e correggi.

# D1
ggplot(utenti, aes(x = categoria)) +
  geom_bar

# D2
ggplot(utenti, aes(x = categoria)) +
  geom_bar(fill = genere)

# D3
ggplot(utenti, aes(x = eta))
  + geom_histogram()

Trova l’errore: confronto

Errore Che cosa succede
D1 geom_bar senza () Can’t add geom_bar to a ggplot object. Did you forget to add parentheses?
D2 fill = genere fuori da aes() R cerca un oggetto genere nell’ambiente di lavoro: object ‘genere’ not found. Serve aes(fill = genere)
D3 + all’inizio della riga la prima riga è già un’istruzione completa (grafico vuoto); la seconda dà errore. Il + va alla fine della riga

In D2, se nell’ambiente esistesse un oggetto genere, R lo userebbe senza dare errore: un risultato sbagliato è più pericoloso di un errore.

5. Barre impilate, affiancate, normalizzate

L’argomento position

gap_classi <- gap_2007 |>
  mutate(aspettativa = cut(life_exp, breaks = c(0, 60, 75, Inf),
                           labels = c("< 60", "60-75", "> 75")))
g <- ggplot(gap_classi, aes(x = continent, fill = aspettativa))
g + geom_bar()

g + geom_bar(position = "dodge")

g + geom_bar(position = "fill")

Attività 5

Modifica - Scheda, attività 5

Con prestiti_validi (prestiti con genere letterario noto) e g <- ggplot(prestiti_validi, aes(x = categoria, fill = genere_letterario)), prova geom_bar() con le tre position.

  • Anticipa: quale grafico serve per confrontare la composizione per genere letterario dei prestiti di studenti e pensionati?
  • A quale tabella tabyl() + adorn_*() corrisponde position = "fill"?

Attività 5: confronto

position = "fill" disegna i profili riga, cioè tabyl(categoria, genere_letterario) |> adorn_percentages("row"): confronta la composizione di gruppi di numerosità molto diversa (139 prestiti degli studenti, 28 dei docenti), ma nasconde quanti sono.

6. Un grafico per ogni gruppo

I facet

gap_data |>
  filter(year %in% c(1957, 1982, 2007)) |>
  ggplot(aes(x = gdp_per_cap, y = life_exp, color = continent)) +
  geom_point() +
  facet_wrap(~ year)

Attività 6

Modifica - Scheda, attività 6

Istogramma dell’eta degli utenti (geom_histogram(binwidth = 5, boundary = 10)) con un pannello per categoria (facet_wrap(~ categoria)).

  • Anticipa: i pannelli hanno lo stesso asse y? È un vantaggio?
  • Bonus: scales = "free_y"; Bonus 2: facet_grid(genere ~ categoria)

Attività 6: confronto

ggplot(utenti, aes(x = eta)) +
  geom_histogram(binwidth = 5, boundary = 10) +
  facet_wrap(~ categoria)

  facet_wrap(~ categoria, scales = "free_y")

7. Scale e colori

Le scale: dai valori alle posizioni (e ai colori)

p_gap <- ggplot(gap_2007,
                aes(x = gdp_per_cap, y = life_exp,
                    color = continent)) +
  geom_point()
p_gap

p_gap +
  scale_x_log10() +
  scale_color_brewer(palette = "Set1")

Famiglia scale_<estetica>_<tipo>(): scale_x_continuous(), scale_x_log10(), scale_fill_brewer(), scale_color_manual(), scale_color_viridis_d(), …

Attività 7

Modifica - Scheda, attività 7

Con prestiti_mese <- prestiti |> count(biblioteca, mese = month(data_prestito)), costruisci un grafico a linee (geom_line() + geom_point()) dei prestiti per mese, un colore per biblioteca. Aggiungi scale_x_continuous(breaks = 1:12) e scale_color_brewer(palette = "Dark2").

  • Controlla: che cosa mostra il grafico ad agosto per B01, B02 e B03? Che cosa è successo davvero?
  • Bonus: labels = month.abb

Attività 7: confronto

Ad agosto B01, B02 e B03 sono chiuse: count() non crea righe con n = 0 e la linea unisce luglio e settembre

prestiti_mese |>
  complete(biblioteca, mese = 1:12,
           fill = list(n = 0))

Codice corretto, grafico fuorviante: controlla sempre i dati da cui parte.

8. Rifinire e salvare

Etichette e temi

p_gap <- p_gap +
  scale_x_log10() +
  labs(title = "Ricchezza e salute (2007)",
       x = "PIL pro capite (log)",
       y = "Aspettativa di vita",
       color = "Continente",
       caption = "Fonte: gapminder")
p_gap + theme_minimal()

Temi predefiniti e ritocchi

p_gap + theme_economist()   # ggthemes

p_gap + theme_minimal() +
  theme(legend.position = "bottom")

ggsave("ricchezza_salute.png", p_gap, width = 8, height = 5)

Attività 8

Modifica - Scheda, attività 8

Salva in p il grafico dell’attività 7 e aggiungi un pezzo alla volta: labs() (titolo, sottotitolo, assi, legenda, fonte), theme_minimal(), la legenda in basso. Salvalo con ggsave().

  • Anticipa: che cosa succede mettendo theme(legend.position = "bottom") prima di theme_minimal()?

Attività 8: confronto

p <- p +
  labs(title = "Prestiti per mese e biblioteca",
       subtitle = "Sistema bibliotecario, 2025",
       x = NULL,
       y = "Numero di prestiti",
       color = "Biblioteca",
       caption = "Fonte: esercitazione 1") +
  theme_minimal() +
  theme(legend.position = "bottom")
ggsave("prestiti_mese_biblioteca.png", p,
       width = 8, height = 4.5)

Con theme() prima di theme_minimal() la legenda torna a destra: il tema completo annulla le modifiche precedenti.

Sfida finale

Ricrea questo grafico

Modifica - dati utenti

  • 145 utenti, non 150: chi manca?
  • categorie ordinate per età mediana: fct_reorder()
  • colori "#E6A817" e "#003087", trasparenza 0.7
  • poi chiedi lo stesso grafico a un assistente AI partendo dall’immagine e confronta

Sfida: una soluzione

utenti |>
  filter(!is.na(eta)) |>
  ggplot(aes(x = eta, y = fct_reorder(categoria, eta), fill = genere)) +
  geom_boxplot(alpha = 0.7) +
  scale_fill_manual(values = c("Femmina" = "#E6A817", "Maschio" = "#003087")) +
  labs(title = "Età degli utenti per categoria e genere",
       subtitle = "Categorie ordinate per età mediana",
       x = "Età (anni)", y = NULL, fill = NULL) +
  theme_minimal() +
  theme(legend.position = "top")
  • Data: filter() prima di ggplot()
  • Aesthetics: tre mapping, uno con una variabile trasformata
  • Geometries, Scales, Theme: nell’ordine visto oggi

Chiusura

Che cosa portiamo a casa

  • ggplot(dati, aes(...)) + geom_...()
  • + tra i layer, |> solo prima di ggplot()
  • geom_bar() conta, geom_col() usa una y
  • mapping (dentro aes()) e setting (fuori)
  • i layer si disegnano nell’ordine in cui li scrivi
  • position: "stack", "dodge", "fill"
  • facet_wrap() e facet_grid()
  • scale_<estetica>_<tipo>()
  • labs(), theme_...(), poi theme()
  • ggsave()

Per approfondire

Nel prossimo compito: i grafici dell’HW05 rifatti con ggplot2