Una guida alle subfigure di Matplotlib per creare complessi grafici a pannelli multipli

Una guida completa alle subfigure di Matplotlib per creare grafici a pannelli multipli di grande complessità

Sottofigure — uno strumento potente per bellissime figure multi-pannello

Motivazione

Le figure complesse (scientifiche) spesso sono composte da più grafici con dimensioni o annotazioni diverse. Se lavori con l’ecosistema matplotlib/seaborn, ci sono molti modi per creare figure complesse, ad esempio usando gridspec. Tuttavia, ciò può diventare impegnativo molto velocemente, specialmente se desideri integrare grafici multiassi da seaborn come jointplot o pairgrid nella tua figura perché non hanno l’opzione di fornire gli assi come parametri di input. Ma c’è un altro modo per assemblare figure in matplotlib anziché lavorare solo con sottoplot: Sottofigure. Un framework potente per creare figure multi-pannello come questa:

L'obiettivo dell'articolo è mostrarti come realizzare questa figura.

In questo articolo, fornirò un’introduzione alle sottofigure e alle loro capacità. Combineremo le sottofigure con i sottoplot e i gridspec per ricreare questa figura. Per seguire questo articolo, dovresti avere una comprensione di base dei sottoplot di matplotlib sottoplot e dei gridspec (se non li hai, puoi consultare i tutorial collegati).

Sottofigure di Matplotlib

Per prima cosa, importiamo matplotlib, seaborn e carichiamo alcuni dati di esempio, che useremo per riempire i grafici con contenuti:

import matplotlib.pyplot as pltimport seaborn as snsdata = sns.load_dataset('mpg')

Cominciamo con il concetto di sottofigure in matplotlib. Per creare sottofigure, dobbiamo prima creare una figura:

fig = plt.figure(figsize=(10, 7))

Da questo punto, possiamo definire le sottofigure allo stesso modo dei sottoplot. È possibile creare una griglia di sottofigure fornendo il numero di righe (2) e colonne (1). Inoltre, coloriamo gli sfondi della figura per evidenziarli:

(topfig, bottomfig) = fig.subfigures(2, 1)topfig.set_facecolor('#cbe4c6ff')topfig.suptitle('Superiore')bottomfig.set_facecolor('#c6c8e4ff')bottomfig.suptitle('Inferiore')

Le figure senza alcun grafico (assi) non verranno visualizzate, pertanto dobbiamo definire i sottoplot per ogni sottofigura. Qui possiamo già vedere una grande funzionalità delle sottofigure, per ogni sottofigura possiamo definire layout diversi dei sottoplot:

top_axs = topfig.subplots(2, 4)bottom_axs = bottomfig.subplots(3, 7)plt.show()

Ora abbiamo due figure separate che possiamo configurare in modo diverso ma posizionare insieme in una figura finale. Naturalmente, possiamo anche giocare con i rapporti delle dimensioni delle sottofigure:

figure = plt.figure(figsize=(10, 7))figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))figs = figs.flatten()for i, fig in enumerate(figs): fig.suptitle(f'Sottofigura {i}') axs = fig.subplots(2, 2)plt.show()

Tuttavia, c’è un inconveniente delle sottofigure. Per eliminare sovrapposizioni di etichette o elementi al di fuori della figura, `plt.tight_layout()` è un buon modo per inserire tutto in modo ordinato nella figura. Tuttavia, questo non è supportato per le sottofigure. Qui puoi vedere cosa accade se provi ad usarlo:

figure = plt.figure(figsize=(10, 7))
figs = figure.subfigures(2, 2, height_ratios=(2,1), width_ratios=(2,1))
figs = figs.flatten()
for i, fig in enumerate(figs): fig.suptitle(f'Sottofigura {i}')
axs = fig.subplots(2, 2)
plt.tight_layout()
plt.show()

Non proprio quello che avevamo intenzione… Per inserire uno spazio tra i grafici e rimuovere sovrapposizioni, è necessario utilizzare la funzione `subplots_adjust`, che ci permette di inserire (o rimuovere) più spazio tra i grafici e i bordi:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Superiore')
bottomfig.set_facecolor('#c6c8e4ff')
bottomfig.suptitle('Inferiore')
top_axs = topfig.subplots(2, 4)
bottom_axs = bottomfig.subplots(3, 7)
# Aggiungere più spazio tra i grafici e ridurre lo spazio ai lati
topfig.subplots_adjust(left=.1, right=.9, wspace=.5, hspace=.5)
# Possiamo anche comprimere i grafici in basso
bottomfig.subplots_adjust(wspace=.5, hspace=.8, top=.7, bottom=.3)
plt.show()

Un altro aspetto interessante delle sottofigure è che possono essere annidate, ossia possiamo dividere ogni sottofigura in ulteriori sottofigure:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Superiore')
top_axs = topfig.subplots(2, 4)
(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))
bottomleft.set_facecolor('#c6c8e4ff')
bottomleft.suptitle('Inferiore sinistra')
bottom_axs = bottomleft.subplots(2, 2)
bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Inferiore destra')
bottom_axs = bottomright.subplots(3, 3)
# Spaziatura tra le sottofigure
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
plt.show()

Inseriamo un jointplot in questa figura. Purtroppo, non è direttamente possibile poiché la funzione di seaborn non accetta un oggetto figura come input. Tuttavia, se analizziamo il codice sorgente della funzione, notiamo che questo grafico è composto da tre sottografici con gli assi x e y condivisi, definiti tramite un gridspec.

Ciò significa che possiamo facilmente inserirlo in una sottofigura:

fig = plt.figure(figsize=(10, 7))
(topfig, bottomfig) = fig.subfigures(2, 1)
topfig.set_facecolor('#cbe4c6ff')
topfig.suptitle('Superiore')
top_axs = topfig.subplots(2, 4)
# Utilizziamo la sottofigura in basso a sinistra per il jointplot
(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))
# Questo parametro definisce il rapporto di dimensioni tra il grafico principale e gli altri grafici laterali
ratio=2
# Definizione di un gridspec in cui sono posizionati i sottografici
gs = plt.GridSpec(ratio + 1, ratio + 1)
# Il grafico di dispersione principale
ax_joint  = bottomleft.add_subplot(gs[1:, :-1])
# Gli assi dei margini condividono gli assi del grafico principale
ax_marg_x = bottomleft.add_subplot(gs[0, :-1], sharex=ax_joint)
ax_marg_y = bottomleft.add_subplot(gs[1:, -1], sharey=ax_joint)
# Labels e ticklabels degli assi dei margini non sono visibili
# Poiché condividono gli assi del grafico principale,
# la loro rimozione dai margini comporterà la loro rimozione anche dal grafico principale
plt.setp(ax_marg_x.get_xticklabels(), visible=False)
plt.setp(ax_marg_y.get_yticklabels(), visible=False)
plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)
plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)
# Riempiamo i plot con i dati
sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)
sns.histplot(data=data, y='horsepower', ax=ax_marg_y)
sns.histplot(data=data, x='mpg', ax=ax_marg_x)
bottomright.set_facecolor('#aac8e4ff')
bottomright.suptitle('Inferiore destra')
bottom_axs = bottomright.subplots(3, 3)
# Spaziatura tra le sottofigure
topfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)
plt.show()

Puoi giocare con il parametro di ratio e vedere come cambia il grafico.

Ora abbiamo tutti gli strumenti necessari per creare figure complesse, utilizzando subfigure, subplots e griglie. Per queste figure, è spesso fondamentale annotare ogni grafico con lettere per spiegarli nella didascalia o farvi riferimento in un testo. Questo avviene spesso con altri software come Adobe Illustrator o Inkscape dopo la creazione della figura. Ma possiamo farlo anche direttamente in python, risparmiandoci sforzi aggiuntivi in seguito.

A tale scopo, definiremo una funzione per creare queste annotazioni:

def letter_annotation(ax, xoffset, yoffset, letter): ax.text(xoffset, yoffset, letter, transform=ax.transAxes,         size=12, weight='bold')

La funzione prende un oggetto “axes” come input, insieme alle coordinate x e y, che verranno trasformate in coordinate relative all’oggetto “axes”. Possiamo usarla per annotare alcuni grafici nella figura precedentemente creata:

fig = plt.figure(figsize=(10, 7))(topfig, bottomfig) = fig.subfigures(2, 1)topfig.set_facecolor('#cbe4c6ff')topfig.suptitle('In Alto')top_axs = topfig.subplots(2, 4)letter_annotation(top_axs[0][0], -.2, 1.1, 'A')(bottomleft, bottomright) = bottomfig.subfigures(1, 2, width_ratios=(1,2))bottomleft.set_facecolor('#c6c8e4ff')bottomleft.suptitle('In Basso a Sinistra')bottoml_axs = bottomleft.subplots(2, 2)letter_annotation(bottoml_axs[0][0], -.2, 1.1, 'B')bottomright.set_facecolor('#aac8e4ff')bottomright.suptitle('In Basso a Destra')bottomr_axs = bottomright.subplots(3, 3)letter_annotation(bottomr_axs[0][0], -.2, 1.1, 'C')# Spaziatura tra le subcharttopfig.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)bottomleft.subplots_adjust(left=.2, right=.9, wspace=.5, hspace=.4)bottomright.subplots_adjust(left=.1, right=.9, wspace=.4, hspace=.4)plt.show()

Possiamo ora creare il grafico mostrato all’inizio dell’articolo. È composto da tre subfigure. Una subfigure superiore, che occupa la prima riga, e due subfigure inferiori. La subfigure inferiore sinistra verrà utilizzata per il jointplot (come mostrato in precedenza) e per la subfigure inferiore destra definiremo una gridspec per posizionare 4 subgrafici di diverse dimensioni.

fig = plt.figure(figsize=(10, 7))# Creazione di una subfigure per la prima e seconda riga(row1fig, row2fig) = fig.subfigures(2, 1, height_ratios=[1, 1])# Divisione della subfigure della riga inferiore in due subfigure(fig_row2left, fig_row2right) = row2fig.subfigures(1, 2, wspace=.08, width_ratios = (1, 2))# ###### Grafici riga 1# ###### Creazione di 4 subgrafici per la subfigurarow1_axs = row1fig.subplots(1, 4)row1fig.subplots_adjust(wspace=0.5, left=0, right=1, bottom=.16)ax = row1_axs[0]sns.histplot(data=data, x='mpg', ax=ax)ax.set_title('MPG')# Annotazione dei grafici con lettereletter_annotation(ax, -.25, 1, 'A')# Alcune personalizzazioni grafiche per renderli più belli e uniformisns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[1]sns.histplot(data=data, x='displacement', ax=ax)ax.set_title('Displacement')letter_annotation(ax, -.25, 1, 'B')sns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[2]sns.histplot(data=data, x='weight', ax=ax)ax.set_title('Weight')letter_annotation(ax, -.25, 1, 'C')sns.despine(offset=5, trim=False, ax=ax)ax = row1_axs[3]sns.histplot(data=data, x='horsepower', ax=ax)ax.set_title('Horsepower')letter_annotation(ax, -.25, 1, 'D')sns.despine(offset=5, trim=False, ax=ax)# ###### Grafici riga 2# ###### ### Jointplot Seaborn# ### Utilizzando il codice dalla classe Seaborn JointGrid# Rapporto di dimensione tra i grafici principali e i grafici di marginearatio=2# Definizione di una gridspec interna alla subfiguregs = plt.GridSpec(ratio + 1, ratio + 1)ax_joint  = fig_row2left.add_subplot(gs[1:, :-1])# Condivisione degli assi tra i grafici di margine e il grafico principaleax_marg_x = fig_row2left.add_subplot(gs[0, :-1], sharex=ax_joint)ax_marg_y = fig_row2left.add_subplot(gs[1:, -1], sharey=ax_joint)# Rimozione delle etichette degli assi e dei ticklabels per i grafici di marginaltplt.setp(ax_marg_x.get_xticklabels(), visible=False)plt.setp(ax_marg_y.get_yticklabels(), visible=False)plt.setp(ax_marg_x.get_xticklabels(minor=True), visible=False)plt.setp(ax_marg_y.get_yticklabels(minor=True), visible=False)sns.scatterplot(data=data, y='horsepower', x='mpg', ax=ax_joint)sns.histplot(data=data, y='horsepower', ax=ax_marg_y)sns.histplot(data=data, x='mpg', ax=ax_marg_x)sns.despine(offset=5, trim=False, ax=ax_joint)sns.despine(offset=5, trim=False, ax=ax_marg_y)sns.despine(offset=5, trim=False, ax=ax_marg_x)# Lasciare un po' di spazio a destra per evitare sovrapposizionifig_row2left.subplots_adjust(left=0, right=.8)letter_annotation(ax_marg_x, -.25, 1, 'E')# ### Grafici riga 2 a destra# ##gs = plt.GridSpec(2, 3)ax_left   = fig_row2right.add_subplot(gs[:, 0])ax_middle = fig_row2right.add_subplot(gs[:, 1])ax_up     = fig_row2right.add_subplot(gs[0, 2])ax_down   = fig_row2right.add_subplot(gs[1, 2])fig_row2right.subplots_adjust(left=0, right=1, hspace=.5)ax = ax_leftsns.scatterplot(data=data, x='model_year', y='weight', hue='origin', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'F')ax = ax_middlesns.boxplot(data=data, x='origin', y='horsepower', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'G')ax = ax_upsns.kdeplot(data=data, x='mpg', y='acceleration', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'H')ax = ax_downsns.histplot(data=data, x='weight', y='horsepower', ax=ax)sns.despine(offset=5, trim=False, ax=ax)letter_annotation(ax, -.3, 1, 'I')plt.show()

Conclusion

Le sott Figure sono un concetto relativamente nuovo in matplotlib. Rendono facile assemblare grandi figure con molti grafici. Tutto quello mostrato in questo articolo può anche essere ottenuto interamente utilizzando gridspec. Tuttavia, ciò richiede una grande griglia con molte considerazioni riguardo le dimensioni di ogni subplot. Le sott Figure sono più plug-and-play e lo stesso risultato può essere ottenuto con meno lavoro.

Per me, le sott Figure sono uno strumento molto comodo per creare figure scientifiche e spero che possano esserti utili anche a te.

Puoi trovare tutto il codice di questo articolo su GitHub: https://github.com/tdrose/blogpost-subfigures-code

Se non diversamente specificato, tutte le immagini sono state create dall’autore.