La grafica di base in R

Author

Domenico Vistocco

Prepariamo l’ambiente di lavoro

Le funzioni della grafica di base appartengono al package graphics, che viene caricato automaticamente all’avvio di R: per usarle non serve caricare alcun package. Carico comunque il tidyverse e janitor per preparare i dati, e il package gapminder, che contiene la tabella usata negli esempi.

# carico le librerie di lavoro --------------------------------------------
library(tidyverse)          # per le funzioni di dplyr (preparazione dei dati)
library(janitor)            # per la funzione clean_names
library(gapminder)          # per la tabella dati utilizzata
# NOTA: le funzioni grafiche usate nel tutorial appartengono al package
#       graphics, caricato automaticamente all'avvio di R

La tabella dati gapminder

Come nel tutorial su ggplot2, uso la tabella gapminder, resa famosa dalla presentazione di Hans Rosling: “The best stats you’ve ever seen”. La tabella riporta, per 142 paesi e per gli anni dal 1952 al 2007 (ogni 5 anni), il continente, la speranza di vita alla nascita, la popolazione e il PIL pro capite. Usare gli stessi dati nei due tutorial permette di confrontare direttamente i due modi di costruire i grafici.

help("gapminder")
gap_data <- gapminder |>
  clean_names() |>
  rename(gdp_per_cap = gdp_percap)
gap_data
# A tibble: 1,704 × 6
   country     continent  year life_exp      pop gdp_per_cap
   <fct>       <fct>     <int>    <dbl>    <int>       <dbl>
 1 Afghanistan Asia       1952     28.8  8425333        779.
 2 Afghanistan Asia       1957     30.3  9240934        821.
 3 Afghanistan Asia       1962     32.0 10267083        853.
 4 Afghanistan Asia       1967     34.0 11537966        836.
 5 Afghanistan Asia       1972     36.1 13079460        740.
 6 Afghanistan Asia       1977     38.4 14880372        786.
 7 Afghanistan Asia       1982     39.9 12881816        978.
 8 Afghanistan Asia       1987     40.8 13867957        852.
 9 Afghanistan Asia       1992     41.7 16317921        649.
10 Afghanistan Asia       1997     41.8 22227415        635.
# ℹ 1,694 more rows
gap_data_2007 <- gap_data |>
  filter(year == 2007)
gap_data_1987_2007 <- gap_data |>
  filter(year >= 1987)

La logica della grafica di base

La grafica di base funziona come un pittore che dipinge su una tela: ogni istruzione disegna immediatamente qualcosa sulla finestra grafica, e ciò che è stato disegnato non può essere modificato, ma solo coperto da nuove “pennellate”. Le funzioni grafiche si dividono in due famiglie:

  • le funzioni di alto livello (plot, barplot, hist, boxplot, pie, curve, …) creano un nuovo grafico: preparano la tela, disegnano gli assi e la rappresentazione dei dati, e sostituiscono l’eventuale grafico precedente
  • le funzioni di basso livello (points, lines, abline, segments, text, legend, title, axis, rug, …) aggiungono elementi al grafico già presente

L’aspetto del grafico si controlla con i parametri grafici, che si passano come argomenti alle funzioni (ad esempio col per il colore o pch per il simbolo dei punti) oppure si impostano per tutti i grafici successivi con la funzione par.

NoteGrafica di base e ggplot2

In ggplot2 un grafico è un oggetto che si costruisce sommando strati (layer) e che viene disegnato solo quando lo si stampa: si può salvare, modificare e ridisegnare. Nella grafica di base, invece, il grafico non è un oggetto: ogni istruzione disegna direttamente sulla finestra grafica, e per cambiare qualcosa bisogna rieseguire tutte le istruzioni. In compenso la logica è semplice e immediata, e molti package statistici producono i loro grafici con la grafica di base.

Funzioni di alto livello

Una funzione di alto livello produce da sola un grafico completo. Qui plot, a cui passo due vettori numerici, disegna un diagramma di dispersione:

# le funzioni "di alto livello" (plot, barplot, hist, boxplot, ...) aprono
# un nuovo grafico: disegnano gli assi, i punti (o le barre, ...) e le
# etichette, sostituendo l'eventuale grafico precedente
plot(gap_data_2007$gdp_per_cap, gap_data_2007$life_exp)

Funzioni di basso livello

Le funzioni di basso livello aggiungono elementi a un grafico già presente. Nell’esempio aggiungo una linea orizzontale in corrispondenza della media, un’etichetta e un titolo. Se si prova a usare una funzione di basso livello senza un grafico aperto, R restituisce un errore (plot.new has not been called yet).

# le funzioni "di basso livello" (points, lines, abline, text, legend, ...)
# aggiungono elementi al grafico gia' presente, come pennellate successive
# su una tela: non possono essere usate se non c'e' un grafico aperto
plot(gap_data_2007$gdp_per_cap, gap_data_2007$life_exp)
# linea orizzontale in corrispondenza della speranza di vita media
abline(h = mean(gap_data_2007$life_exp), lty = 2, col = "red")
# testo in una posizione indicata dalle coordinate (x, y) del grafico
text(x = 40000, y = 70, labels = "media", col = "red")
# titolo aggiunto in un secondo momento
title(main = "Speranza di vita e PIL pro capite (2007)")

Gli argomenti delle funzioni di alto livello

La maggior parte delle personalizzazioni si ottiene direttamente con gli argomenti della funzione di alto livello. Alcuni argomenti sono comuni a quasi tutte le funzioni grafiche:

Argomento Significato
main titolo del grafico
xlab, ylab etichette degli assi
xlim, ylim intervallo dei valori degli assi, ad esempio ylim = c(0, 100)
col colore (dei punti, delle linee, delle barre)
pch simbolo dei punti (da 0 a 25)
cex dimensione dei simboli e del testo (1 = dimensione di default)
lty, lwd tipo e spessore delle linee
las orientamento delle etichette degli assi (1 = sempre orizzontali)
# la maggior parte delle personalizzazioni si ottiene con gli argomenti
# della funzione di alto livello: titolo, etichette degli assi, simbolo,
# colore e dimensione dei punti
plot(gap_data_2007$gdp_per_cap, gap_data_2007$life_exp,
     main = "Speranza di vita e PIL pro capite (2007)",
     xlab = "PIL pro capite (dollari USA)",
     ylab = "Speranza di vita alla nascita (anni)",
     pch = 19,                # simbolo: cerchio pieno
     col = "steelblue",       # colore
     cex = 0.8)               # dimensione (1 = dimensione di default)

La funzione generica plot

plot è una funzione generica: il tipo di grafico prodotto dipende dalla classe degli oggetti che riceve. È un esempio concreto del fatto che R è un linguaggio orientato agli oggetti.

Un vettore numerico viene rappresentato con un punto per ciascun valore, nell’ordine delle righe:

# plot e' una funzione generica: il grafico prodotto dipende dal tipo
# (classe) degli oggetti ricevuti in input
# un vettore numerico: grafico dei valori nell'ordine delle righe
plot(gap_data_2007$life_exp)

Un factor produce un diagramma a barre delle frequenze:

# un factor: diagramma a barre delle frequenze
plot(gap_data_2007$continent)

Un factor e un vettore numerico producono i boxplot della variabile numerica per ciascuna modalità del factor:

# un factor e un vettore numerico: boxplot affiancati
plot(gap_data_2007$continent, gap_data_2007$life_exp)

Una tabella con più colonne numeriche produce la matrice dei diagrammi di dispersione di tutte le coppie di variabili:

# una tabella con piu' colonne numeriche: matrice dei diagrammi di
# dispersione di tutte le coppie di variabili
plot(gap_data_2007[, c("life_exp", "pop", "gdp_per_cap")])

La sintassi a formula

Molte funzioni grafiche (plot, boxplot, stripchart, mosaicplot, …) accettano la sintassi a formula y ~ x, con l’argomento data per indicare la tabella in cui cercare le variabili. Rispetto all’uso dell’operatore $, il codice è più leggibile e le etichette degli assi riportano direttamente i nomi delle variabili:

# molte funzioni grafiche accettano la sintassi a formula y ~ x, con
# l'argomento data per indicare la tabella in cui cercare le variabili:
# non serve ripetere il nome della tabella con l'operatore $
plot(life_exp ~ gdp_per_cap, data = gap_data_2007)

Una variabile qualitativa

Il diagramma a barre

La funzione barplot rappresenta le frequenze: non va applicata direttamente alla variabile, ma al risultato di table.

# variabile qualitativa: diagramma a barre
# barplot richiede le frequenze, cioe' il risultato di table
freq_continenti <- table(gap_data_2007$continent)
freq_continenti

  Africa Americas     Asia   Europe  Oceania 
      52       25       33       30        2 
barplot(freq_continenti)

Ordinando le frequenze con sort le barre risultano ordinate, e il confronto tra le modalità è più immediato:

# barre ordinate per frequenza decrescente, con colore, titolo ed
# etichette; las = 1 scrive orizzontali le etichette dell'asse verticale
barplot(sort(freq_continenti, decreasing = TRUE),
        col = "steelblue",
        main = "Paesi per continente (2007)",
        ylab = "Numero di paesi",
        las = 1)

Con horiz = TRUE le barre sono orizzontali, una soluzione utile quando le etichette delle modalità sono lunghe:

# barre orizzontali; con las = 1 anche le etichette dei continenti sono
# scritte in orizzontale
barplot(sort(freq_continenti),
        horiz = TRUE,
        col = "steelblue",
        main = "Paesi per continente (2007)",
        xlab = "Numero di paesi",
        las = 1)

Per rappresentare le frequenze percentuali basta applicare barplot al risultato di prop.table:

# frequenze percentuali sull'asse verticale
barplot(sort(prop.table(freq_continenti) * 100, decreasing = TRUE),
        col = "steelblue",
        main = "Paesi per continente (2007)",
        ylab = "Percentuale di paesi",
        las = 1)

Alternative al diagramma a barre

Il dot chart di Cleveland sostituisce ogni barra con un punto: è più leggero e particolarmente adatto quando le modalità sono molte.

# il dot chart di Cleveland: un'alternativa al diagramma a barre che usa
# un punto al posto della barra
dotchart(as.numeric(sort(freq_continenti)),
         labels = names(sort(freq_continenti)),
         pch = 19,
         main = "Paesi per continente (2007)",
         xlab = "Numero di paesi")

Il diagramma a torta è molto diffuso, ma il nostro occhio confronta male gli angoli e le aree: già con cinque modalità è difficile stabilire quale fetta sia più grande. Il diagramma a barre è quasi sempre preferibile (sul portale From Data to Viz trovi una discussione dettagliata).

# il diagramma a torta: l'occhio confronta male gli angoli e le aree,
# per cui il diagramma a barre e' quasi sempre preferibile
pie(freq_continenti,
    col = hcl.colors(5, "Set 2"),
    main = "Paesi per continente (2007)")

Una variabile quantitativa

L’istogramma

L’istogramma rappresenta la distribuzione di una variabile quantitativa suddividendone i valori in classi. Con le impostazioni di default R sceglie automaticamente il numero di classi:

# variabile quantitativa: istogramma
hist(gap_data_2007$life_exp)

L’argomento breaks permette di scegliere gli estremi delle classi. La scelta delle classi può cambiare molto la forma dell’istogramma, e conviene farla guardando i dati e la loro unità di misura:

# scelgo gli estremi delle classi con breaks; col e border definiscono il
# colore di riempimento e quello del bordo delle barre
hist(gap_data_2007$life_exp,
     breaks = seq(35, 85, by = 5),
     col = "lightblue",
     border = "white",
     main = "Speranza di vita alla nascita (2007)",
     xlab = "Anni",
     ylab = "Numero di paesi")

Con freq = FALSE l’altezza delle barre rappresenta la densità di frequenza (l’area complessiva delle barre è pari a 1). In questo modo posso sovrapporre all’istogramma una stima della densità, con le funzioni di basso livello lines e density, e disegnare i singoli valori sotto l’asse orizzontale con rug:

# con freq = FALSE l'altezza delle barre rappresenta la densita' di
# frequenza: posso sovrapporre una stima della densita' (lines) e
# disegnare i singoli valori sotto l'asse orizzontale (rug)
hist(gap_data_2007$life_exp,
     breaks = seq(35, 85, by = 5),
     freq = FALSE,
     col = "lightblue",
     border = "white",
     main = "Speranza di vita alla nascita (2007)",
     xlab = "Anni",
     ylab = "Densita'")
lines(density(gap_data_2007$life_exp), lwd = 2)
rug(gap_data_2007$life_exp)

Il boxplot e lo stripchart

Il boxplot riassume la distribuzione con il minimo, i quartili, il massimo e gli eventuali valori anomali:

# boxplot di una variabile quantitativa
boxplot(gap_data_2007$life_exp,
        main = "Speranza di vita alla nascita (2007)",
        ylab = "Anni")

# boxplot orizzontale
boxplot(gap_data_2007$life_exp,
        horizontal = TRUE,
        main = "Speranza di vita alla nascita (2007)",
        xlab = "Anni")

Lo stripchart rappresenta invece i singoli valori. Con method = "jitter" i punti vengono spostati casualmente in verticale, in modo da ridurre le sovrapposizioni:

# lo stripchart rappresenta i singoli valori; con method = "jitter" i
# punti vengono spostati casualmente in verticale per ridurre le
# sovrapposizioni
stripchart(gap_data_2007$life_exp,
           method = "jitter",
           pch = 19,
           col = "steelblue",
           main = "Speranza di vita alla nascita (2007)",
           xlab = "Anni")

Una variabile quantitativa e una qualitativa

I boxplot affiancati, ottenuti con la sintassi a formula, sono il modo più classico per confrontare la distribuzione di una variabile quantitativa tra i gruppi definiti da una variabile qualitativa:

# variabile quantitativa vs variabile qualitativa: boxplot affiancati
boxplot(life_exp ~ continent, data = gap_data_2007,
        col = hcl.colors(5, "Set 2"),
        main = "Speranza di vita per continente (2007)",
        xlab = "Continente",
        ylab = "Anni",
        las = 1)

La variabile di raggruppamento può essere anche il tempo: qui i boxplot mostrano l’evoluzione della distribuzione della speranza di vita nei diversi anni.

# la stessa logica funziona con qualunque variabile di raggruppamento:
# qui l'evoluzione nel tempo della distribuzione della speranza di vita
boxplot(life_exp ~ year, data = gap_data_1987_2007,
        col = "lightblue",
        main = "Speranza di vita nel mondo (1987-2007)",
        xlab = "Anno",
        ylab = "Anni",
        las = 1)

La logica “a pennellate” della grafica di base permette di sovrapporre più rappresentazioni. Qui aggiungo ai boxplot i singoli paesi, con stripchart e l’argomento add = TRUE; i punti sono semitrasparenti grazie alla funzione adjustcolor. Poiché i punti vengono disegnati dopo i boxplot, si trovano sopra di essi:

# sovrappongo ai boxplot i singoli paesi: stripchart con add = TRUE
# disegna i punti sul grafico gia' presente
# outline = FALSE evita di disegnare due volte i valori anomali
boxplot(life_exp ~ continent, data = gap_data_2007,
        outline = FALSE,
        main = "Speranza di vita per continente (2007)",
        xlab = "Continente",
        ylab = "Anni",
        las = 1)
stripchart(life_exp ~ continent, data = gap_data_2007,
           vertical = TRUE,
           method = "jitter",
           pch = 19,
           col = adjustcolor("steelblue", alpha.f = 0.5),
           add = TRUE)

Violin plot e beeswarm

I package vioplot e beeswarm, basati sulla grafica di base, offrono due alternative al boxplot. Il violin plot affianca al boxplot una stima della densità, e mostra quindi la forma della distribuzione. Il beeswarm disegna un punto per ogni osservazione, disponendo i punti in modo che non si sovrappongano. Prima di eseguire il codice, installa i due package (una sola volta) con install.packages(c("vioplot", "beeswarm")).

# due alternative al boxplot disponibili in package che si basano sulla
# grafica di base (da installare una sola volta con install.packages)
library(vioplot)
library(beeswarm)
par(mfrow = c(1, 2))
vioplot(life_exp ~ continent, data = gap_data_2007,
        col = "lightblue",
        main = "Violin plot",
        xlab = "Continente",
        ylab = "Anni")
boxplot(life_exp ~ continent, data = gap_data_2007,
        outline = FALSE,
        main = "Boxplot e beeswarm",
        xlab = "Continente",
        ylab = "Anni")
beeswarm(life_exp ~ continent, data = gap_data_2007,
         pch = 19,
         cex = 0.6,
         col = "steelblue",
         add = TRUE)

par(mfrow = c(1, 1))

Il beeswarm mostra subito un’informazione che il boxplot nasconde: l’Oceania comprende solo due paesi, e il suo “boxplot” è calcolato su due valori.

Due variabili quantitative

Il diagramma di dispersione

Il diagramma di dispersione rappresenta la relazione tra due variabili quantitative. Il PIL pro capite ha una distribuzione molto asimmetrica: con log = "x" l’asse orizzontale è in scala logaritmica e la relazione con la speranza di vita diventa più leggibile.

# due variabili quantitative: diagramma di dispersione
# con log = "x" l'asse orizzontale e' in scala logaritmica, adatta a una
# variabile molto asimmetrica come il PIL pro capite
plot(life_exp ~ gdp_per_cap, data = gap_data_2007,
     log = "x",
     pch = 19,
     main = "Speranza di vita e PIL pro capite (2007)",
     xlab = "PIL pro capite (dollari USA, scala logaritmica)",
     ylab = "Speranza di vita (anni)")

Per colorare i punti secondo il continente uso un trucco tipico della grafica di base: un factor, usato come indice di un vettore di colori, restituisce per ogni riga il colore corrispondente al suo livello. La legenda, a differenza di ggplot2, non viene creata automaticamente e va aggiunta con la funzione legend:

# coloro i punti secondo il continente: un factor puo' essere usato come
# indice di un vettore di colori (il primo livello prende il primo colore,
# il secondo livello il secondo colore, ecc.)
colori <- hcl.colors(5, "Dark 3")
plot(life_exp ~ gdp_per_cap, data = gap_data_2007,
     log = "x",
     pch = 19,
     col = colori[gap_data_2007$continent],
     main = "Speranza di vita e PIL pro capite (2007)",
     xlab = "PIL pro capite (dollari USA, scala logaritmica)",
     ylab = "Speranza di vita (anni)")
# la legenda va aggiunta esplicitamente con una funzione di basso livello
legend("bottomright",
       legend = levels(gap_data_2007$continent),
       col = colori,
       pch = 19,
       bty = "n")

Combinando più argomenti si ottiene un grafico simile a quelli di Hans Rosling: la dimensione dei punti (cex) è proporzionale alla popolazione, i colori sono semitrasparenti e alcuni paesi sono etichettati con text (l’argomento pos indica se scrivere l’etichetta sotto, a sinistra, sopra o a destra del punto).

# un grafico "alla Rosling": la dimensione dei punti e' proporzionale alla
# popolazione (cex), i colori sono semitrasparenti (adjustcolor) per
# rendere visibili i punti sovrapposti, e alcuni paesi sono etichettati
dim_punti <- sqrt(gap_data_2007$pop / max(gap_data_2007$pop)) * 6
plot(life_exp ~ gdp_per_cap, data = gap_data_2007,
     log = "x",
     pch = 21,
     bg = adjustcolor(colori[gap_data_2007$continent], alpha.f = 0.6),
     cex = dim_punti,
     main = "Speranza di vita e PIL pro capite (2007)",
     xlab = "PIL pro capite (dollari USA, scala logaritmica)",
     ylab = "Speranza di vita (anni)")
# pos indica dove scrivere l'etichetta rispetto al punto (1 = sotto,
# 2 = a sinistra, 3 = sopra, 4 = a destra): per gli Stati Uniti, vicini al
# margine destro, scrivo l'etichetta a sinistra
paesi <- gap_data_2007 |>
  filter(country %in% c("China", "India", "Nigeria", "United States")) |>
  mutate(posizione = if_else(country == "United States", 2, 3))
text(paesi$gdp_per_cap, paesi$life_exp,
     labels = paesi$country,
     pos = paesi$posizione,
     cex = 0.8)
legend("bottomright",
       legend = levels(gap_data_2007$continent),
       pt.bg = colori,
       pch = 21,
       bty = "n")

Due variabili qualitative

Per avere un esempio con due variabili qualitative, classifico la speranza di vita in tre classi con la funzione cut. Applicato a una tabella doppia, barplot disegna una barra per ciascuna colonna della tabella, suddivisa secondo le righe:

# due variabili qualitative: creo una seconda variabile qualitativa
# classificando la speranza di vita in tre classi
gap_data_2007 <- gap_data_2007 |>
  mutate(classe_vita = cut(life_exp,
                           breaks = c(0, 60, 75, Inf),
                           labels = c("bassa", "media", "alta")))
tab_cont_vita <- table(gap_data_2007$classe_vita, gap_data_2007$continent)
tab_cont_vita
       
        Africa Americas Asia Europe Oceania
  bassa     40        0    3      0       0
  media     11       15   21      8       0
  alta       1       10    9     22       2
# barplot di una tabella doppia: una barra per ogni COLONNA (continente),
# suddivisa secondo le righe (classi di speranza di vita)
barplot(tab_cont_vita,
        col = hcl.colors(3, "Blues 3", rev = TRUE),
        legend.text = TRUE,
        args.legend = list(x = "topright", bty = "n"),
        main = "Classi di speranza di vita per continente (2007)",
        ylab = "Numero di paesi",
        las = 1)

Con beside = TRUE le barre vengono affiancate invece che impilate:

# barre affiancate invece che impilate
barplot(tab_cont_vita,
        beside = TRUE,
        col = hcl.colors(3, "Blues 3", rev = TRUE),
        legend.text = TRUE,
        args.legend = list(x = "topright", bty = "n"),
        main = "Classi di speranza di vita per continente (2007)",
        ylab = "Numero di paesi",
        las = 1)

Rappresentando i profili colonna, ogni barra mostra la composizione percentuale di un continente, indipendentemente dal numero di paesi:

# profili colonna: composizione percentuale di ciascun continente
barplot(prop.table(tab_cont_vita, margin = 2) * 100,
        col = hcl.colors(3, "Blues 3", rev = TRUE),
        main = "Classi di speranza di vita per continente (2007)",
        ylab = "Percentuale di paesi",
        las = 1)

Il mosaic plot combina le due informazioni: la larghezza di ciascuna colonna è proporzionale al numero di paesi del continente, e l’altezza dei rettangoli alla composizione per classe di speranza di vita.

# il mosaic plot: la larghezza delle colonne e' proporzionale alla
# numerosita' di ciascun continente, l'altezza dei rettangoli alla
# composizione per classe di speranza di vita
mosaicplot(continent ~ classe_vita, data = gap_data_2007,
           color = hcl.colors(3, "Blues 3", rev = TRUE),
           main = "Classi di speranza di vita per continente (2007)",
           xlab = "Continente",
           ylab = "Classe di speranza di vita")

Andamenti nel tempo

Per rappresentare l’andamento di una variabile nel tempo si usa plot con l’argomento type: "l" disegna una linea, "b" linee e punti, "p" (il default) solo i punti, "h" barre verticali sottili.

# andamento nel tempo: grafico a linee
# type = "l" (linee), "b" (linee e punti), "p" (punti, il default),
# "h" (barre verticali sottili)
italia <- gap_data |>
  filter(country == "Italy")
plot(italia$year, italia$life_exp,
     type = "b",
     pch = 19,
     main = "Speranza di vita in Italia",
     xlab = "Anno",
     ylab = "Anni")

Per confrontare più serie, disegno la prima con plot e aggiungo le altre con la funzione di basso livello lines. Poiché l’intervallo degli assi viene fissato dalla prima serie, imposto con ylim un intervallo che contenga anche le altre:

# piu' serie nello stesso grafico: il primo paese con plot, gli altri
# aggiunti con la funzione di basso livello lines
# ylim fissa l'intervallo dell'asse verticale, in modo che contenga anche
# le serie aggiunte successivamente
giappone <- gap_data |>
  filter(country == "Japan")
nigeria <- gap_data |>
  filter(country == "Nigeria")
plot(italia$year, italia$life_exp,
     type = "l",
     lwd = 2,
     col = colori[1],
     ylim = c(30, 85),
     main = "Speranza di vita in tre paesi",
     xlab = "Anno",
     ylab = "Anni")
lines(giappone$year, giappone$life_exp, lwd = 2, col = colori[2])
lines(nigeria$year, nigeria$life_exp, lwd = 2, col = colori[3])
legend("bottomright",
       legend = c("Italia", "Giappone", "Nigeria"),
       col = colori[1:3],
       lwd = 2,
       bty = "n")

Con molte serie conviene partire da una tabella in formato largo, con una colonna per serie (la si ottiene con pivot_wider), e usare la funzione matplot, che disegna una linea per ciascuna colonna:

# con molte serie conviene partire da una tabella in formato largo (una
# colonna per serie) e usare matplot, che disegna una linea per colonna
vita_continenti <- gap_data |>
  group_by(year, continent) |>
  summarise(life_exp_avg = mean(life_exp), .groups = "drop") |>
  pivot_wider(names_from = continent, values_from = life_exp_avg)
vita_continenti
# A tibble: 12 × 6
    year Africa Americas  Asia Europe Oceania
   <int>  <dbl>    <dbl> <dbl>  <dbl>   <dbl>
 1  1952   39.1     53.3  46.3   64.4    69.3
 2  1957   41.3     56.0  49.3   66.7    70.3
 3  1962   43.3     58.4  51.6   68.5    71.1
 4  1967   45.3     60.4  54.7   69.7    71.3
 5  1972   47.5     62.4  57.3   70.8    71.9
 6  1977   49.6     64.4  59.6   71.9    72.9
 7  1982   51.6     66.2  62.6   72.8    74.3
 8  1987   53.3     68.1  64.9   73.6    75.3
 9  1992   53.6     69.6  66.5   74.4    76.9
10  1997   53.6     71.2  68.0   75.5    78.2
11  2002   53.3     72.4  69.2   76.7    79.7
12  2007   54.8     73.6  70.7   77.6    80.7
matplot(vita_continenti$year, vita_continenti[, -1],
        type = "l",
        lty = 1,
        lwd = 2,
        col = colori,
        main = "Speranza di vita media per continente",
        xlab = "Anno",
        ylab = "Anni")
legend("bottomright",
       legend = names(vita_continenti)[-1],
       col = colori,
       lwd = 2,
       bty = "n")

Il grafico di una funzione matematica

La funzione curve disegna il grafico di una funzione della variabile x: è molto utile per rappresentare funzioni di densità e confrontarle, aggiungendo altre curve con add = TRUE.

# la funzione curve disegna il grafico di una funzione matematica di x,
# ad esempio una funzione di densita'
curve(dnorm(x), from = -4, to = 4,
      lwd = 2,
      main = "Normale standard e t di Student con 3 gradi di liberta'",
      xlab = "x",
      ylab = "Densita'")
curve(dt(x, df = 3), add = TRUE, lwd = 2, lty = 2, col = "red")
legend("topright",
       legend = c("N(0, 1)", "t con 3 gradi di liberta'"),
       lty = c(1, 2),
       col = c("black", "red"),
       lwd = 2,
       bty = "n")

I parametri grafici

Simboli e tipi di linea

Il simbolo dei punti si sceglie con pch (da 0 a 25: i simboli da 21 a 25 hanno un bordo, definito da col, e un riempimento, definito da bg); il tipo di linea con lty (da 1 a 6). I due grafici seguenti, costruiti “da zero” a partire da un grafico vuoto (axes = FALSE elimina gli assi, plot(NULL, ...) apre un grafico senza disegnare nulla) e completati con le funzioni di basso livello, li mostrano tutti:

# simboli (pch) disponibili: li dispongo su due righe, con il numero
# corrispondente sotto ciascun simbolo
x_simboli <- rep(0:12, times = 2)[1:26]
y_simboli <- rep(c(2, 1), each = 13)[1:26]
plot(x_simboli, y_simboli,
     pch = 0:25,
     bg = "orange",
     cex = 2.5,
     xlim = c(-0.5, 12.5),
     ylim = c(0.5, 2.5),
     axes = FALSE,
     xlab = "",
     ylab = "",
     main = "Simboli: pch = 0, ..., 25")
text(x_simboli, y_simboli - 0.3, labels = 0:25)

# tipi di linea (lty) disponibili: segments disegna segmenti da (x0, y0)
# a (x1, y1); gli argomenti sono vettoriali, per cui una sola chiamata
# disegna sei segmenti
plot(NULL,
     xlim = c(0, 1),
     ylim = c(0.5, 6.5),
     axes = FALSE,
     xlab = "",
     ylab = "",
     main = "Tipi di linea: lty = 1, ..., 6")
segments(x0 = 0, y0 = 1:6, x1 = 1, y1 = 1:6, lty = 1:6, lwd = 2)
axis(2, at = 1:6, las = 1)

I colori

Un colore si può indicare con il nome (colors() elenca i 657 nomi disponibili), con un codice esadecimale o con la funzione rgb:

# i colori si possono indicare con un nome, con un codice esadecimale o
# usando una tavolozza (palette)
head(colors())
[1] "white"         "aliceblue"     "antiquewhite"  "antiquewhite1"
[5] "antiquewhite2" "antiquewhite3"
barplot(rep(1, 3),
        col = c("tomato", "#1B9E77", rgb(0.2, 0.4, 0.8)),
        names.arg = c("nome", "esadecimale", "rgb"),
        axes = FALSE,
        main = "Tre modi di indicare un colore")

Per scegliere più colori coerenti tra loro conviene usare una tavolozza (palette). La funzione hcl.colors(n, palette) restituisce n colori di una delle tavolozze disponibili (elencate da hcl.pals()). La scelta dipende dal tipo di variabile rappresentata:

# hcl.colors(n, palette) restituisce n colori di una tavolozza
# (hcl.pals() elenca le tavolozze disponibili)
par(mfrow = c(3, 1), mar = c(1, 1, 2, 1))
barplot(rep(1, 7), col = hcl.colors(7, "Set 2"), axes = FALSE,
        main = "Tavolozza qualitativa (Set 2): per modalita' senza ordine")
barplot(rep(1, 7), col = hcl.colors(7, "Blues 3", rev = TRUE), axes = FALSE,
        main = "Tavolozza sequenziale (Blues 3): per modalita' ordinate")
barplot(rep(1, 7), col = hcl.colors(7, "Blue-Red"), axes = FALSE,
        main = "Tavolozza divergente (Blue-Red): per valori attorno a un centro")

par(mfrow = c(1, 1), mar = c(5, 4, 4, 2) + 0.1)

Più grafici nella stessa finestra e margini

La funzione par imposta i parametri grafici validi per tutti i grafici successivi. L’argomento mfrow = c(righe, colonne) divide la finestra in una griglia, che viene riempita riga per riga. Poiché le impostazioni di par restano attive, conviene salvare i parametri originali (par li restituisce quando li modifica) e ripristinarli alla fine:

# par(mfrow = c(righe, colonne)) divide la finestra grafica in una
# griglia; conviene salvare i parametri originali e ripristinarli alla fine
vecchi_par <- par(mfrow = c(2, 2))
hist(gap_data_2007$life_exp, main = "Istogramma", xlab = "Anni")
boxplot(gap_data_2007$life_exp, horizontal = TRUE,
        main = "Boxplot", xlab = "Anni")
stripchart(gap_data_2007$life_exp, method = "jitter", pch = 19,
           main = "Stripchart", xlab = "Anni")
plot(density(gap_data_2007$life_exp), main = "Densita' stimata", xlab = "Anni")

par(vecchi_par)

L’argomento mar = c(sotto, sinistra, sopra, destra) imposta l’ampiezza dei margini, misurata in righe di testo. Qui allargo il margine sinistro per far spazio alle etichette dei paesi:

# par(mar = c(sotto, sinistra, sopra, destra)) imposta i margini (in
# righe di testo); qui allargo il margine sinistro per le etichette lunghe
paesi_popolosi <- gap_data_2007 |>
  arrange(desc(pop)) |>
  head(10)
vecchi_par <- par(mar = c(5, 9, 4, 2))
barplot(rev(paesi_popolosi$pop) / 1e6,
        names.arg = rev(as.character(paesi_popolosi$country)),
        horiz = TRUE,
        las = 1,
        col = "steelblue",
        main = "I dieci paesi piu' popolosi (2007)",
        xlab = "Popolazione (milioni)")

par(vecchi_par)

Salvare un grafico in un file

In RStudio un grafico si può salvare dal pannello Plots con il pulsante Export. Per rendere il salvataggio riproducibile, però, conviene farlo da codice: si apre un dispositivo grafico su file (con le funzioni png, jpeg, pdf o svg), si eseguono le istruzioni del grafico e si chiude il dispositivo con dev.off(). Finché il dispositivo non viene chiuso il file non è completo, e i grafici successivi continuano a essere scritti nel file invece che nella finestra grafica.

# per salvare un grafico in un file si apre un "dispositivo grafico" su
# file (png, pdf, jpeg, svg), si disegna il grafico e si chiude il
# dispositivo con dev.off()
png("speranza_vita_continenti.png", width = 1600, height = 1000, res = 200)
boxplot(life_exp ~ continent, data = gap_data_2007,
        col = hcl.colors(5, "Set 2"),
        main = "Speranza di vita per continente (2007)",
        xlab = "Continente",
        ylab = "Anni")
dev.off()
# lo stesso in formato pdf (vettoriale, adatto alla stampa): dimensioni
# in pollici
pdf("speranza_vita_continenti.pdf", width = 8, height = 5)
boxplot(life_exp ~ continent, data = gap_data_2007,
        col = hcl.colors(5, "Set 2"),
        main = "Speranza di vita per continente (2007)",
        xlab = "Continente",
        ylab = "Anni")
dev.off()

Grafica di base e ggplot2 a confronto

Grafica di base ggplot2
Logica pennellate successive su una tela grammatica dei grafici: strati sommati con +
Il grafico viene disegnato subito e non può essere modificato è un oggetto che si può salvare, modificare e ridisegnare
Dati vettori (o tabelle con la sintassi a formula) sempre una tabella in formato tidy
Colori per gruppi e legenda vanno costruiti a mano (col, legend) automatici a partire da aes()
Pannelli per sottogruppi par(mfrow), un grafico alla volta facet_wrap() e facet_grid()
Punti di forza immediatezza, grafici veloci per l’esplorazione dei dati, grafici di molti package statistici coerenza, grafici complessi con poco codice, aspetto curato

Per approfondire