Come creare bei grafici di distribuzione dell’età con Seaborn e Matplotlib (inclusa l’animazione)

How to create beautiful age distribution graphs using Seaborn and Matplotlib (including animation)

Tutorial Grafici

Visualizzazione di dati demografici per paesi e regioni

Grafici creati dall'autore

Oggi voglio mostrarti come creare bei grafici di distribuzione di età come quelli sopra usando matplotlib e seaborn.

I grafici di distribuzione di età sono eccellenti per visualizzare i dati demografici di un paese o regione. Sono affascinanti, ma i grafici di default di Seaborn + Matplotlib non sono sufficientemente belli per noi.

Ecco cosa imparerai in questo tutorial:

  • Come creare uno stile Seaborn
  • Migliorare gli assi per renderli leggibili e informativi
  • Aggiungere un titolo e una legenda bellissimi
  • Convertire le figure di Matplotlib in immagini PIL e aggiungere un padding esterno
  • Creare griglie di immagini multiple (come nell’esempio sopra)
  • Creare animazioni a intervalli di tempo per mostrare come cambia la popolazione nel tempo

Puoi trovare i dati e il mio codice in questo repository di GitHub se vuoi seguirmi.

Iniziamo.

Una breve panoramica dei dati

I dati originali provengono dal dataset Population Estimates and Projections, che è un dataset della Banca Mondiale con licenza Creative Commons Attribution 4.0. Contiene valori effettivi tra il 1960-2021 e previsioni ufficiali fino al 2050.

Nel repository di GitHub, ho elaborato i dati e creato quattro file CSV separati in modo che tu possa concentrarti sulla creazione di grafici.

Due file, uno per le femmine e uno per i maschi, hanno la popolazione in numeri assoluti.

Screenshot dell'autore

Gli altri due file hanno valori che descrivono il rapporto tra la popolazione totale. Nello screenshot qui sotto, ad esempio, puoi vedere che solo lo 0,336% delle persone in Bahrain aveva tra i 70 e i 74 anni nel 1960.

Screenshot dell'autore

Il dataset ha 17 gruppi di età, 00-04, 05-09, 10-14, 15-19, 20-24, 25-29, 30-34, 35-39, 40-44, 45-49, 50-54, 55-59, 60-64, 65-69, 70-74, 75-79 e 80+.

E oltre 250 paesi e regioni, quindi sentiti libero di creare grafici di distribuzione di età che ti interessano.

Creazione del primo grafico di distribuzione di età

Ora che comprendiamo i dati, possiamo creare un grafico semplice con le impostazioni predefinite di Seaborn. Sto usando il colore rosso per le femmine e blu per i maschi.

È forse un po’ stereotipato, ma rendere i grafici facili da comprendere è vitale, e i colori sono essenziali per quella prima interpretazione.

L’unico “trucco” è che moltiplico i valori per i maschi per meno uno in modo che le barre blu vadano nella direzione opposta.

Ecco la funzione per creare il grafico.

def create_age_distribution(female_df, male_df, country, year):    df_f = female_df[female_df.country_name == country].loc[::-1]    df_m = male_df[male_df.country_name == country].loc[::-1]        ax = sns.barplot(y=df_m["indicator_name"], x=df_m[year] * -1, orient="h", color=MALE_COLOR)    ax = sns.barplot(y=df_f["indicator_name"], x=df_f[year], orient="h", color=FEMALE_COLOR)        return ax

Ecco come lo utilizzo.

fig = plt.figure(figsize=(10, 7))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="Mondo",    year="2021")plt.show()

Questo è il grafico risultante della distribuzione dell’età per il Mondo nel 2021. Mostra tutti i dati, ma non ha un bell’aspetto ed è difficile da capire.

Grafico creato dall'autore

Rendiamolo migliore.

Creare uno stile Seaborn

La cosa migliore di seaborn è che è facile creare i propri stili unici utilizzando sns.set_style(). Prende un dizionario che può avere diversi valori.

Per questo tutorial, ho creato la seguente funzione per provare rapidamente diversi stili.

def set_seaborn_style(font_family, background_color, grid_color, text_color):    sns.set_style({        "axes.facecolor": background_color,        "figure.facecolor": background_color,        "axes.labelcolor": text_color,        "axes.edgecolor": grid_color,        "axes.grid": True,        "axes.axisbelow": True,        "grid.color": grid_color,        "font.family": font_family,        "text.color": text_color,        "xtick.color": text_color,        "ytick.color": text_color,        "xtick.bottom": False,        "xtick.top": False,        "ytick.left": False,        "ytick.right": False,        "axes.spines.left": False,        "axes.spines.bottom": True,        "axes.spines.right": False,        "axes.spines.top": False,    })

Potresti voler avere qualcosa che ti dia ancora più controllo. Ho omesso alcune opzioni che non mi interessano qui, e riutilizzo gli stessi colori in più posti.

Dobbiamo selezionare il colore di sfondo, della griglia e del testo per eseguire la funzione. Preferisco i grafici con un colore di sfondo perché risaltano di più dalla pagina. Uno sfondo bianco può sembrare buono, ma non è il mio stile.

Quando creo uno schema di colori nuovo, spesso inizio trovando un colore che mi piace. Un buon punto di partenza è Canva Color Palettes o ColorHunt.

Dopo aver trovato alcuni colori che mi piacciono, genero colori aggiuntivi con Coolors.

Ecco la palette di colori principale che sto usando in questo tutorial.

Screenshot dell'autore

Ora posso eseguire set_seaborn_style() con i nostri nuovi colori, e ho selezionato PT Mono come tipo di carattere.

FEMALE_COLOR = "#F64740"MALE_COLOR = "#05B2DC"set_seaborn_style(    font_family="PT Mono",    background_color="#253D5B",    grid_color="#355882",    text_color="#EEEEEE")

Ecco com’è il grafico ora.

Grafico creato dall'autore

È un chiaro miglioramento rispetto a quello che avevamo prima, ma manca di informazioni ed è ancora difficile da capire.

Continuiamo risolvendo gli assi.

Migliorare gli assi

Ora che i colori hanno un bell’aspetto, è il momento di rendere il grafico più informativo.

Ecco tre cose che voglio fare.

  • Rimuovere le etichette degli assi perché non aggiungono informazioni
  • Formattare i valori sull’asse x per renderli più informativi
  • Rendere il testo più grande in modo che il grafico abbia un bell’aspetto su schermi più piccoli

La soluzione consiste in due funzioni.

In primo luogo, la funzione create_x_labels() si occupa del secondo punto elencato e mi consente di adattare l’asse x in base alla popolazione di un paese in modo rapido o se voglio utilizzare rapporti invece di numeri assoluti.

def create_x_labels(ax, xformat):    if xformat == "miliardi":        return ["{}B".format(round(abs(x / 1e9))) for x in ax.get_xticks()[1:-1]]    elif xformat == "milioni":        return ["{}M".format(round(abs(x / 1e6))) for x in ax.get_xticks()[1:-1]]    elif xformat == "migliaia":        return ["{}K".format(round(abs(x / 1e3))) for x in ax.get_xticks()[1:-1]]    elif xformat == "percentuale":        return ["{}%".format(round(abs(x), 1)) for x in ax.get_xticks()[1:-1]]

In secondo luogo, la funzione format_ticks(), che si occupa del primo e del terzo punto elencati e chiama create_x_labels().

def format_ticks(ax, xformat, xlim=(None, None)):    ax.tick_params(axis="x", labelsize=12, pad=8)    ax.tick_params(axis="y", labelsize=12)    ax.set(ylabel=None, xlabel=None, xlim=xlim)        plt.xticks(        ticks=ax.get_xticks()[1:-1],        labels=create_x_labels(ax, xformat)    )

Il parametro xlim è essenziale se vogliamo confrontare due diverse distribuzioni di età. Se lo lasciamo vuoto, l’asse si adatta ai valori dei dati e le barre si estendono su tutto l’asse.

Aggiungo le funzioni quando creo il grafico. È esattamente come prima, ma con format_tricks() alla fine.

fig = plt.figure(figsize=(10, 7))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="Mondo",    year="2021")# Nuove funzioniformat_ticks(ax, xformat="milioni")plt.show()

Ecco come appare il nuovo grafico.

Grafico creato dall'autore

Possiamo anche testare il formato percentuale impostando xformat="percentuale" e utilizzando population_ratio_male e population_ratio_female. Ho anche impostato xlim=(-10, 10).

Grafico creato dall'autore

Sembra buono, ma possiamo fare ancora di più.

Aggiunta di un titolo e di una legenda

Due miglioramenti ovvi che voglio apportare ora sono:

  • Aggiungere un titolo che descriva il grafico
  • Aggiungere una legenda che spieghi cosa rappresentano le barre

Per creare la legenda, ho scritto la seguente funzione che prende i parametri x e y per definire la posizione.

def add_legend(x, y):     patches = [        Patch(color=MALE_COLOR, label="Maschio"),        Patch(color=FEMALE_COLOR, label="Femmina")    ]        leg = plt.legend(        handles=patches,        bbox_to_anchor=(x, y), loc='center',        ncol=2, fontsize=15,        handlelength=1, handleheight=0.4,        edgecolor=background_color    )

Quindi, aggiungo questa funzione proprio come ho fatto con format_tricks() nel passaggio precedente.

fig = plt.figure(figsize=(10, 8))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="Mondo",    year="2021")# Nuove funzioniformat_ticks(ax, xformat="milioni")add_legend(x=0.5, y=1.09)plt.title("Distribuzione dell'età per il mondo nel 2021", y=1.14, fontsize=20)plt.tight_layout()plt.show()

Ho anche aggiunto plt.title() per aggiungere un titolo.

Quando eseguo tutto, il grafico della distribuzione di età appare così.

Grafico creato dall'autore

Sembra fantastico. Andiamo avanti.

Creazione di un’immagine PIL e aggiunta di padding

Ad un certo punto, voglio trasformare le mie figure in immagini che posso salvare su disco e personalizzare in altri modi.

Una tale personalizzazione è quella di aggiungere del padding intorno al grafico per renderlo meno affollato.

Prima di tutto, ho creato la funzione create_image_from_figure() che trasforma una figura di Matplotlib in un’immagine PIL.

def create_image_from_figure(fig):    plt.tight_layout()        fig.canvas.draw()    data = np.frombuffer(fig.canvas.tostring_rgb(), dtype=np.uint8)    data = data.reshape((fig.canvas.get_width_height()[::-1]) + (3,))    plt.close()         return Image.fromarray(data)

Ecco una funzione per aggiungere del padding.

def add_padding_to_chart(chart, left, top, right, bottom, background):    size = chart.size    image = Image.new("RGB", (size[0] + left + right, size[1] + top + bottom), background)    image.paste(chart, (left, top))    return image

Di nuovo, aggiungo queste funzioni al codice originale che crea il grafico. Ora appare così.

fig = plt.figure(figsize=(10, 8))ax = create_age_distribution(    female_df=population_female,    male_df=population_male,    country="Mondo",    year="2021")# Nuove funzioniformat_ticks(ax, xformat="milioni")add_legend(x=0.5, y=1.09)plt.title("Distribuzione per età per il Mondo nel 2021", y=1.14, fontsize=20)image = create_image_from_figure(fig)image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)

Ecco il grafico risultante.

Grafico creato dall'autore

A mio parere, sembra quasi perfetto. Ci sono altre due cose che voglio mostrare, come creare griglie e visualizzazioni temporizzate.

Iniziamo con la prima.

Creazione di una griglia con più paesi

Puoi usare plt.subplots() per creare le griglie, ma in questo tutorial voglio creare una griglia di immagini perché penso che sia più bello.

La seguente funzione prende una lista di immagini e crea una griglia con ncols. Funziona creando un’immagine vuota con un singolo colore di sfondo abbastanza grande da contenere tutte le figure.

def create_grid(figures, pad, ncols):    nrows = int(len(figures) / ncols)    size = figures[0].size    image = Image.new(        "RGBA",        (ncols * size[0] + (ncols - 1) * pad, nrows * size[1] + (nrows - 1) * pad),        "#ffffff00"    )    for i, figure in enumerate(figures):        col, row = i % ncols, i // ncols        image.paste(figure, (col * (size[0] + pad), row * (size[1] + pad)))    return image

Nel seguente codice, itero su una lista di paesi, aggiungo il grafico risultante a figures e creo una griglia eseguendo create_grid() alla fine.

figures = []for country in [    "Stati Uniti", "Cina", "Giappone", "Brasile", "Canada",    "Germania", "Pakistan", "Federazione Russa", "Nigeria",     "Svezia", "Cambogia", "Arabia Saudita", "Islanda",    "Spagna", "Sudafrica", "Marocco"]:    fig = plt.figure(figsize=(10, 8))    ax = create_age_distribution(        female_df=population_ratio_female,        male_df=population_ratio_male,        country=country,        year="2021"    )        ax.set(xlim=(-10, 10))    # Nuove funzioniformat_ticks(ax, xformat="percentuale")    add_legend(x=0.5, y=1.09)    plt.title("Distribuzione per età per {} nel 2021".format(country), y=1.14, fontsize=20)    image = create_image_from_figure(fig)    image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)        figures.append(image)    grid = create_grid(figures, pad=20, ncols=4)

Nota che uso i rapporti invece di numeri assoluti e imposto xlim=(-10, 10). In caso contrario, non sarei in grado di confrontare i paesi tra di loro visualmente.

Grafici creati dall'autore

Passiamo all’ultima parte di questo tutorial – Come creare visualizzazioni time-lapse.

Creazione di una visualizzazione time-lapse

Le grafiche statiche sulla distribuzione per età sembrano ottime, ma è affascinante vedere come cambiano nel tempo.

Dato che abbiamo i valori effettivi dal 1960 al 2021 e le previsioni fino al 2050, possiamo creare un’animazione time-lapse per un periodo relativamente lungo.

Prima di iniziare, devo dirti che il carattere che utilizzo, PT Mono, non ha la stessa altezza per tutti i caratteri. Per rendere la visualizzazione migliore, ho dovuto usare plt.text() per l’anno invece di plt.title(). Se utilizzi un altro carattere, non è necessario farlo.

Ecco il codice:

images = []years = list(population_male.columns[4:])for year in years:    fig = plt.figure(figsize=(10, 8))    ax = create_age_distribution(        female_df=population_female,        male_df=population_male,        country="Mondo",        year=year    )    # Nuove funzioni    format_ticks(ax, xformat="milioni", xlim=(-400000000, 400000000))    add_legend(x=0.5, y=1.09)    plt.title("Distribuzione per età per il Mondo in      ", y=1.14, fontsize=21)    plt.text(x=0.77, y=1.15, s=str(year), fontsize=21, transform=ax.transAxes)    image = create_image_from_figure(fig)    image = add_padding_to_chart(image, 20, 20, 20, 5, background_color)    images.append(image)

Uso imageio per creare un GIF dalla lista di immagini.

# Duplicazione degli ultimi fotogrammi per aggiungere un ritardo # prima che l'animazione ripartaimages = images + [images[-1] for _ in range(20)]imageio.mimwrite('./time-lapse.gif', images, duration=0.2)

Diamo un’occhiata al risultato.

Visualizzazione creata dall'autore

Fantastico! Questo è tutto per questo tutorial; fammi sapere se ti è piaciuto e se hai imparato qualcosa di utile.

Conclusione

È stato divertente scrivere questo tutorial e spero che ti sia piaciuto.

Le distribuzioni per età sono una grande visualizzazione della demografia di un paese e ora hai visto alcuni modi per farle risaltare.

Abbiamo imparato a creare stili, griglie e animazioni. Scrivere funzioni come ho fatto qui è anche ottimo se si desidera testare rapidamente idee e stili diversi.

Spero di aver imparato qualcosa che userai in futuro.

Grazie per aver dedicato del tempo alla lettura del mio tutorial. Fammi sapere se ti piace questo tipo di contenuto.

Posso creare altri tutorial se le persone li vogliono! 🙂

A presto.