Matplotlib és una llibreria de baix nivell que serveix per dibuixar figures, funcions i tot tipus de gràfiques en imatges estàtiques. Funciona molt bé amb qualsevol estructura de dades de Python.

Matplotlib és una llibreria de baix nivell que serveix per dibuixar figures, funcions i tot tipus de gràfiques estàtiques (en imatges png). Funciona molt bé amb estructures de Python, arrays de Numpy, sèries i dataFrames de Pandas...

Tot i que hi ha llibreries estàtiques més modernes i senzilles a Python (Seaborn) i altres que ofereixen gràfics animats (Plotly, Bokeh) ens interessa aprendre com funciona Matplotlib, perquè és la que ofereix el màxim nivell de personalització i perquè totes aquestes llibreries tenen un funcionament similar a Matplotlib. Per aquest motiu, encara hi ha molts treballs científics que presenten les funcions i gràfics amb Matplotlib.

Instal·lar i provar Matplotlib

Si ja has instal·lat en el teu projecte Numpy o Pandas, ja podràs importar Matplotlib.

Pots reaprofitar el projecte que usaves a Numpy o bé crear-ne un de nou.

Recorda com es fa per crear-ne un de nou:

$ mkdir numpyplots
$ cd numpyplots
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install numpy

Si no s'instal·la matplotlib aplica aquesta instrucció:

$ pip install matplotlib

També recomanem que guardis la versió de les llibreries que has usat en un fitxer requirements.txt:

$ pip freeze > requirements.txt

Per tal que quan vulguis puguis instal·lar de cop totes les llibreries del projecte. És una bona pràctica habitual en els projectes en Python.

$ pip -R install requirements.txt

Consideracions d'ús de Jupyter Notebook.

Com ja saps, molts/es científics/ques se senten més còmodes treballant amb Jupyter Notebooks, fitxers amb format .IPYNB que es poden executar a plataformes com Google Collab o Kaggle i així poden lluïr de Data Science sense instal·lar i mantenir programari.

Nosaltres som informàtics i no usarem aquests quaderns com a producte final, com a molt ens interessarà consultar i provar fitxers .IPYNB per explorar els datasets; la nostra prioritat és que les dades estiguin integrades en qualsevol aplicació web o d'altres tipus.

Simplement, que sabeu que si trobeu linies com aquestes:

%matplotlib inline

Només són necessàries si useu Jupyter Notebook.

import matplotlib.pyplot as plt
# If you're using Jupyter Notebook, you may also want to run the following
# line of code to display your code in the notebook:
%matplotlib inline

plt.plot(a)
# If you are running from a command line, you may need to do this:
# >>> plt.show()

També heu de saber que si useu codi de Jupyter Notebook podeu mostrar els gràfics amb aquesta instrucció

plt.show()

En canvi, si esteu en un projecte de Python (.py) el més habitual és guardar el gràfic resultant en un fitxer d'imatge .png per veure els resultats.

plt.savefig('students.png')

Més endavant crearem alguna aplicació web per a mostrar aquests gràfics (que són fitxers d'imatge).

Crear gràfics senzills amb el mètode plot()

El gràfic més senzill que podem crear és una línia a partir d'una matriu plana (1D) o una llista.

A l'importar, ja creem un contenidor (un canvas = un lienzo) dins de l'objecte plt.

El mètode plot és el que dibuixa el gràfic dins del contenidor plt.

Si no li diem res més crea un gràfic de punts amb les dades de la llista que li hem passat.

import numpy as np
import matplotlib.pyplot as plt
%matplotlib inline
l1 = [2, 1, 5, 7, 11, 12, 15, 13]
plt.plot(l1)
plt.show()

També podem crear un gràfic amb diverses línies (una des d'un array 1D i una altra des d'una linia)

import numpy as np
import matplotlib.pyplot as plt
#%matplotlib inline
a1 = np.arange(2,17,2)
l1 = [2, 1, 5, 7, 11, 12, 15, 13]
plt.plot(l1)
plt.plot(a1)
#plt.show()
plt.savefig('matplotlib1.png')

També pots veure com crear una matriu i dibuixar una recta i posar-li punts a damunt.

x = np.linspace(0, 5, 20)
y = np.linspace(0, 10, 20)
plt.plot(x, y, color = 'purple')    # linia
plt.plot(x, y, 'o')         # punts
plt.savefig('matplotlib2.png')

Amb Matplotlib, pots accedir a un enorme ventall d'opcions de visualizació 2D i fins i tot 3D. En aquest cas mostrem una ona sinusoidal radial.

Com a curiositat, aquesta funció es pot descriure matemàticament com:

[ Z = \sin(R) ]

on

[ R = \sqrt{X^2 + Y^2} ]

Desglossem-ho una mica més:

  • R és la distància euclideana (equivalent al teorema de Pitagoras) des de l'origen (0, 0) fins a qualsevol punt ((X, Y)) en el pla.
  • La funció sin(R) pren aquesta distància radial i calcula el valor del sinus d'aquesta distància.

Les ones sinusoidals radials són útils per modelar fenòmens que es propaguen radialment des d'un punt central, com ara les ones de calor o electromagnètiques.

Aquest tipus de visualització només la mostrem com a demostració del potencial de la biblioteca.

fig = plt.figure()
ax = fig.add_subplot(projection='3d')
X = np.arange(-5, 5, 0.15)
Y = np.arange(-5, 5, 0.15)
X, Y = np.meshgrid(X, Y)
R = np.sqrt(X**2 + Y**2)
Z = np.sin(R)
ax.plot_surface(X, Y, Z, rstride=1, cstride=1, cmap='viridis')
plt.savefig('matplotlib3.png')

Paràmetres bàsics de la funció plot()

Els més importants i comuns en tots els gràfics són:

  • x --> Estructura de dades (llista, diccionari, array, dataframe ...) on es troben les dades a dibuixar. Necessari.
  • y --> Dimensió Y per figures 2D i gràfics de funcions que representin linies (siguin rectes o no) o punts en un pla.
  • z --> Dimensió Z per gràfics 3D.
  • color --> Color dels punts/rectes/figures.
  • label --> posem llegenda a la línia.
  • fmt --> format. Si tenim números decimals

Si no li diem res, la funció infereix els paràmetres (sobretot els x, y que són molt habituals), però és bona pràctica especificar paràmetres com el color.

Apart dels paràmetres de la funció plot, tenim altres mètodes importants dins el contenidor del gràfic (plt en el nostre cas).

Els més bàsics són els que mostren les etiquetes dels eixos i del títol del gràfic.

  • ylabel("Etiqueta eix vertical")
  • xlabel("Etiqueta eix horitzontal")
  • title("Titol gràfic")
  • legend() --> Mostra una llegenda de les funcions o barres
  • grid() --> Mostra una graella per facilitar la visió

I com fer el gràfic més gran o més petit ? Podem usar aquest mètode abans de mostrar o guardar el gràfic, que indica les proporcions:

py plt.figure(figsize=(10,6))

Exercici 1. Crea un gràfic amb 3 línies que representin la evolució del seu nivell de glucosa a la sang en mg/L que tenen 3 pacients amb grip i amb 7 columnes, una per dia; guardat en numpy. Que siguin dades versemblants de persones hospitalitzades. Intenta que es vegi una llegenda i/o la descripció del gràfic i els seus eixos, així com diferenciar cada pacient per un color diferent.

Una presentació suggerida del gràfic:

Opció 1. Més senzilla, amb dades realistes.

import numpy as np
import matplotlib.pyplot as plt

# Dades de glucosa per a tres pacients
days_x = np.arange(1, 8)
gl_patients_y = np.array([[110,130,120,145,140,131,120],
                [125,135,151,143,132,120,111],
                [120,125,112,103,108,111,105]])

# Gràfica de línies (eix x--> dies, eix y--> pacients)
plt.plot(days_x, gl_patients_y[0], color='blue', label='Pacient 1', linewidth=2, marker='o')
plt.plot(days_x, gl_patients_y[1], color='green', label='Pacient 2', linewidth=2, linestyle='--', marker='o')
plt.plot(days_x, gl_patients_y[2], color='red', label='Pacient 3', linewidth=2, linestyle='-.', marker='o')

# Afegir títol i etiquetes
plt.title('Evolució Nivell de Glucosa a la Sang (mg/L) durant 7 Dies')
plt.xlabel('Dies')
plt.ylabel('Nivell de Glucosa (mg/L)')
plt.legend()

# Afegir una graella
plt.grid(True)

# Mostrar la gràfica
plt.tight_layout()
plt.show()

Opció 2. Conté subgràfics (opcionals en aquest cas) amb dades realistes generades aleatòriament.

import numpy as np
import matplotlib.pyplot as plt

# Generació de dades sintètiques per als 3 pacients durant 7 dies
days = np.arange(1, 8)
glucose_patient1 = np.random.normal(110, 10, 7) 
glucose_patient2 = np.random.normal(140, 15, 7)
glucose_patient3 = np.random.normal(130, 12, 7)

# Creació de la figura
fig, ax = plt.subplots(figsize=(10, 6))

# Gràfica de línies
ax.plot(days, glucose_patient1, color='blue', label='Pacient 1', linewidth=2, marker='o')
ax.plot(days, glucose_patient2, color='green', label='Pacient 2', linewidth=2, linestyle='--', marker='o')
ax.plot(days, glucose_patient3, color='red', label='Pacient 3', linewidth=2, linestyle='-.', marker='o')

# Afegir títol i etiquetes
ax.set_title('Evolució Nivell de Glucosa a la Sang (mg/L) durant 7 Dies')
ax.set_xlabel('Dia')
ax.set_ylabel('Nivell de Glucosa (mg/L)')

# Afegir una graella
ax.grid(True)
plt.tight_layout()
plt.show()

Diagrames de barres

Com els altres gràfics, es pot fer sense Numpy i aplicant el que hem après amb els diagrames de linies.

Aquests diagrames són molt adequats per mostrar les freqüències d'aparició de cada valor d'una variable.

import matplotlib.pyplot as plt

# Dades del gràfic
titol = "Gràfic de màximes golejadores del Barça (Lliga 2022-2023)."
etiq_y = "Núm gols"
etiq_x = "Jugadores"
llegenda_x = ["A.Oshoala", "S.Paralluelo", "A.Bonmati"]
valors_y = [21, 11, 10]
colors_y = ['tab:purple', 'tab:brown', 'tab:red']

# Creem el gràfic.
bar_container = plt.bar(llegenda_x, valors_y, color=colors_y)
plt.bar_label(bar_container, fmt='%.0f')
plt.ylabel(etiq_y)
plt.xlabel(etiq_x)
plt.title(titol)
#plt.legend(title='Jugadores') 
# Només si vols usar Collab (ipynb)
#plt.show() 
plt.savefig('players.png')

Amb Matplotlib i Numpy, podem fer diagrames de barres agrupats; de manera semblant a com hem fet abans gràfics de diverses linies.

Suposem que volem veure l'increment dels preus dels lloguers a ciutats de Catalunya.

Les posarem en un array de Numpy, on a l'eix Y hi haurà cada ciutat (Badalona a l'index 0, Barcelona a l'1...) i al de les X els anys (del 2021 al 2023, per exemple)

Ja veureu com us surt.

Compartim amb vosaltres l'adreça del dataset original del preu mitjà dels lloguers als municipis de Catalunya

import numpy as np
import matplotlib.pyplot as plt

ciutats = ['Badalona', 'Barcelona', 'Cornella', 'L\'Hospitalet']
anys = ['2021', '2022', '2023']

preusLloguers23 = np.array([
     [730.4,808.02,848.91],  #Badalona
     [918.84,1026.86,1136.4], #Barcelona
     [695.5,723.23,790.39], #Cornella
     [688.74,731.31,794.87] #L'Hospitalet
])
print(preusLloguers23)

# Generem el diagrama de barres per cada any
fig, ax = plt.subplots(figsize=(10, 6))

# Ample de les barres
bar_width = 0.2
# Posicions de les barres per cada any
x = np.arange(len(ciutats))

# Creem les barres per cada any
for i, any in enumerate(anys):
    ax.bar(x + i * bar_width, data[:, i], width=bar_width, label=any)

# Configuració del gràfic
ax.set_xlabel('Ciutats')
ax.set_ylabel('Preu mitjà lloguer')
ax.set_title('Increment preu mitjà lloguer ciutats AMB')
ax.set_xticks(x + bar_width * (len(anys) - 1) / 2)
ax.set_xticklabels(ciutats)
ax.legend()

# Ajustar la visualització
plt.tight_layout()
plt.show()

Exercici 2. Investiga i prova un exemple de com crear un gràfic de barres apilades.

Pots començar analitzant l'exemple d'Stacked bar chart de Matplotlib i ara que ja saps com són, cercar-ne algún altre.

Classificació de mostres

Tingueu en compte que no sempre us vindran les dades "curades" i en ocasions tindreu, per exemple 100 observacions i les haureu de classificar d'alguna manera: per gènere, per edat...

Suposem que tenim en un array (o llista) els grups sanguinis de 40 pacients (A, O, AB, B) i volem fer un gràfic del número de pacients de cada grup.

Amb la funció de np.unique aconseguirem aquesta agrupació.

Veiem-ho:

import numpy as np
import matplotlib.pyplot as plt

# Definir els grups sanguinis possibles
grups_sanguinis = ['A', 'B', 'AB', 'O']

# Generar aleatòriament els grups sanguinis per a 40 pacients
np.random.seed(0)  # Fixem la llavor per a reproduir els resultats
mostres = np.random.choice(grups_sanguinis, 40)
print(mostres)

# Agrupar les mostres per tipus.
unique, counts = np.unique(mostres, return_counts=True)
data = dict(zip(unique, counts))

print(data)

# Diagrama de barres
plt.figure(figsize=(10, 5))
plt.bar(data.keys(), data.values(), color=['red', 'blue', 'green', 'purple'])
plt.title('Distribució de Grups Sanguinis (Diagrama de Barres)')
plt.xlabel('Grup Sanguini')
plt.ylabel('Nombre de Pacients')
plt.show()

# Diagrama de sectors
plt.figure(figsize=(8, 8))
plt.pie(data.values(), labels=data.keys(), autopct='%1.1f%%', colors=['red', 'blue', 'green', 'purple'])
plt.title('Distribució de Grups Sanguinis (Diagrama de Sectors)')
plt.show()

Les parts més interessants i novedoses del codi es troben al principi:

  1. Es generen les dades aleatòriament.
  2. Encara més important, aquestes linies que guarden els valors únics (unique) i el número d'ocurrències (counts)
  3. Al final guarden tot en un diccionari, per conveniència (però potser no caldria)
# Agrupar les mostres per tipus.
unique, counts = np.unique(mostres, return_counts=True)
data = dict(zip(unique, counts))

Ja sabem el més bàsic de generació de gràfics: la selecció de dades.

Heatmaps

Els heatmaps o mapes de calor són una alternativa molt útil als diagrames de barra compostos, per tal de veure totes les freqüències d'aparició de cada variable.

Generen una taula bidimensional amb els valors de 2 variables, on la llegenda de colors de les dades més altes les marca d'un color intens (pex vermell) i les més baixs d'un altre color oposat (pex blau).

Veiem-ho amb un exemple que ja vam veure a la secció de Numpy: el fitxer CSV de les temperatures de Barcelona dels últims 8 anys

Anem a suposar que ja hem carregat les temperatures dels últims 8 anys i generem el gràfic:

import numpy as np
import matplotlib.pyplot as plt

dades = [
    [2016, 10.7, 11.3, 11.1, 13.6, 16.4, 21.6, 24.9, 24.5, 22.3, 17.1, 12.7, 11.5],
    [2017, 7.9, 11.4, 13.3, 14.2, 18.3, 23.6, 24.2, 24.5, 19.5, 18.6, 12.5, 8.5],
    [2018, 10.5, 6.7, 10.8, 14.7, 17.1, 21.5, 25.3, 25.8, 22.5, 17.0, 12.4, 11.1],
    [2019, 8.1, 11.9, 13.5, 13.4, 15.6, 21.9, 25.4, 25.1, 21.8, 18.5, 11.9, 11.2],
    [2020, 10.0, 12.8, 11.9, 14.3, 19.4, 20.1, 25.0, 25.5, 21.7, 16.4, 14.7, 9.3],
    [2021, 7.7, 11.6, 12.1, 12.9, 17.3, 23.3, 24.8, 24.5, 23.0, 18.1, 11.3, 10.9],
    [2022, 10.2, 11.8, 10.8, 14.1, 20.7, 24.7, 26.7, 27.2, 22.5, 20.7, 15.2, 12.6],
    [2023, 9.2, 10.3, 14.1, 16.1, 18.1, 23.4, 25.5, 26.0, 23.2, 20.2, 14.8, 12.1]
]
data = np.array(dades)

# Separar les columnes per a les etiquetes dels anys i les temperatures
anys = data[:, 0]
temperatures = data[:, 1:]

# Crear el heatmap anotat
fig, ax = plt.subplots(figsize=(12, 8))
cax = ax.matshow(temperatures, cmap='coolwarm')

# Afegir la barra de color
cbar = fig.colorbar(cax, ax=ax, orientation='vertical')
cbar.set_label('Temperatura (°C)')

# Afegir anotacions
for (i, j), val in np.ndenumerate(temperatures):
    ax.text(j, i, f'{val:.1f}', ha='center', va='center', color='black')

# Configurar les etiquetes
ax.set_xlabel('Mesos')
ax.set_ylabel('Anys')
ax.set_title('Heatmap de les temperatures mensuals a Barcelona (2016-2023)')

# Ajustar les etiquetes de l'eix X (mesos)
ax.set_xticks(np.arange(12))
ax.set_xticklabels(['Gen', 'Feb', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Oct', 'Nov', 'Des'])

# Ajustar les etiquetes de l'eix Y (anys)
ax.set_yticks(np.arange(data.shape[0]))
ax.set_yticklabels([str(int(year)) for year in anys])

# Mostrar el gràfic
plt.show()

Però a la vida real no tindrem les dades tan curades, haurem de carregar-les d'un o més fonts i preprocessar-les.

En el nostre cas, necessitem descarregar-nos les dades si no les tenim al disc (connectar-se a una altra URL té un cost computacional i ambiental considerable).

Recordeu com podem aconseguir aquesta descàrrega i lectura de fitxers CSV que són en una URL externa:

xtec.dev -> Lectura i tractament de fitxers amb Python

Un cop les tenim, carreguem les dades de les temperatures en un array, i filtrem únicament les 8 últimes.

Guardem els últims anys (2016 - 2023) en un vector 1D separat de les temperatures (sinó, tindrem errors ja que els arrays han de ser homogenis).

La resta de passos, per a generar el gràfic, són els mateixos.

El codi final queda així:

import numpy as np
# Per a ipynb: 
# !pip install nptyping
from nptyping import NDArray, Int, Float
import os.path
from urllib.request import urlretrieve
import matplotlib.pyplot as plt

url : str = 'https://opendata-ajuntament.barcelona.cat/data/dataset/73f09843-ab4e-4f13-81fb-b801ca371909/resource/0e3b6840-7dff-4731-a556-44fac28a7873/download/temperaturesbcndesde1780_2023.csv'

file : str = 'temperaturesbcn_2023.csv'

# Descarregar el fitxer CSV només si no existeix
if not os.path.isfile(file):
    try:
        urlretrieve(url, file)
        print(f"Fitxer {file} descarregat correctament.")
    except Exception as e:
        print(f"No s'ha pogut descarregar el fitxer des de {url}: {str(e)}")

# Lectura del tot el fitxer CSV excepte la capçalera
data = np.loadtxt('temperaturesbcn_2023.csv', delimiter=',', skiprows=1)

# Seleccionem les últimes 8 files de temperatures (2016 - 2023)
# Separem la columna dels anys i la eliminem de l'array de temperatures.
temps_array = data[-8:,1:13]
anys_array = data[-8:,0]

print(temps_array)
print(anys_array)

# Crear el heatmap anotat
fig, ax = plt.subplots(figsize=(12, 8))
cax = ax.matshow(temps_array, cmap='coolwarm')

# Afegir la barra de color
cbar = fig.colorbar(cax, ax=ax, orientation='vertical')
cbar.set_label('Temperatura (°C)')

# Afegir anotacions i etiquetes
for (i, j), val in np.ndenumerate(temps_array):
    ax.text(j, i, f'{val:.1f}', ha='center', va='center', color='black')

ax.set_xlabel('Mesos')
ax.set_ylabel('Anys')
ax.set_title('Heatmap de les temperatures mensuals a Barcelona (2016-2023)')

# Ajustar les etiquetes de l'eix X (mesos)
ax.set_xticks(np.arange(12))
ax.set_xticklabels(['Gen', 'Feb', 'Mar', 'Abr', 'Mai', 'Jun', 'Jul', 'Ago', 'Set', 'Oct', 'Nov', 'Des'])

# Ajustar les etiquetes de l'eix Y (anys)
ax.set_yticks(np.arange(len(anys_array)))  # Canviar el número de ticks a 8 (les últimes 8 files)
ax.set_yticklabels([str(int(year)) for year in anys_array])

# Mostrar el gràfic
plt.show()

El resultat del gràfic és el mateix, però el resolem amb un codi útil en més contextos i mantenible.

Comentar també que hem importat una llibreria per habilitar Type Hints a Numpy. Si vols saber què són revisa aquest article breu:

xtec.dev -> Python -> Type Hints

Exercici 3. Crea un Heatmap a partir de l'exemple dels preus de lloguer, en vermell els preus més alts i en verd els més baixos

El codi de partida és el següent:

import numpy as np
import matplotlib.pyplot as plt

ciutats = ['Badalona', 'Barcelona', 'Cornella', 'L\'Hospitalet']
anys = ['2021', '2022', '2023']

preusLloguers23 = np.array([
     [730.4,808.02,848.91],  #Badalona
     [918.84,1026.86,1136.4], #Barcelona
     [695.5,723.23,790.39], #Cornella
     [688.74,731.31,794.87] #L'Hospitalet
])
print(preusLloguers23)

import numpy as np
import matplotlib.pyplot as plt

ciutats_llista : list[str] = ['Badalona', 'Barcelona', 'Cornella', 'L\'Hospitalet']
anys_llista : list[str] = ['2021', '2022', '2023']

preusLloguers23 = np.array([
     [730.4,808.02,848.91],  #Badalona
     [918.84,1026.86,1136.4], #Barcelona
     [695.5,723.23,790.39], #Cornella
     [688.74,731.31,794.87] #L'Hospitalet
])
print(preusLloguers23)
# Em ve de gust trasposar la matriu per presentar millor el heatmap.
preusLloguers23 = preusLloguers23.T

# Crear el heatmap anotat
fig, ax = plt.subplots(figsize=(10, 6))
# Catàleg colors a: 
# https://matplotlib.org/stable/users/explain/colors/colormaps.html
cax = ax.matshow(preusLloguers23, cmap='YlOrRd')

# Afegir la barra de color
cbar = fig.colorbar(cax, ax=ax, orientation='vertical')
cbar.set_label('Preus (en €)')

# Afegir anotacions
for (i, j), val in np.ndenumerate(preusLloguers23):
    ax.text(j, i, f'{val:.1f}', ha='center', va='center', color='black')

# Configurar les etiquetes
ax.set_xlabel('Ciutats')
ax.set_ylabel('Anys')
ax.set_title('Heatmap dels preus de lloguers aprop de Barcelona (2021-2023)')

# Ajustar les etiquetes de l'eix X (ciutats)
ax.set_xticks(np.arange(len(ciutats_llista)))
ax.set_xticklabels(ciutats_llista)

# Ajustar les etiquetes de l'eix Y (anys)
ax.set_yticks(np.arange(len(anys_llista)))
ax.set_yticklabels(anys_llista)

# Mostrar el gràfic
plt.show()

--

Scatter Plot

Els scatter plots són gràfics de punts en un pla (2D) o espai (3D). Podem pintar aqusts punts de diversos colors o formes si usem subplots. Això als científics els va molt bé per a classificar a mostres d'individus o espècies per dues carecterístiques que puguin tenir relació (per exemple, podem fer un mapa de punts de 2 colors: home i dona; tenint en compte la seva alçada i pes).

Però millor ho veiem amb un dataset d'exemple mostra les divesres espècies de la planta Iris. Aquest conjunt de dades, de 50 plantes de 3 espècies i 4 carecterístiques importants, és un recurs típic per introduïr-nos al data science.

import numpy as np
import matplotlib.pyplot as plt
import requests
import csv
import os

url_file : str = 'https://raw.githubusercontent.com/mwaskom/seaborn-data/master/iris.csv'
local_file : str = 'iris.csv'

# Verificar si el fitxer ja existeix localment
if not os.path.exists(local_file):
    # Descarregar el fitxer si no existeix
    response = requests.get(url_file)
    with open(local_file, 'wb') as f:
        f.write(response.content)

# Llegir les dades del fitxer CSV amb NumPy
with open(local_file, 'r') as f:
    reader = csv.reader(f)
    headers = next(reader)  # Saltar la fila de capçaleres
    # Ho llegirà com array d'strings per evitar errors.
    data = np.array([row for row in reader])

# Convertir les dades a arrays 1D de Numpy amb tipus ben definit.
sepal_length = data[:, 0].astype(float)
petal_length = data[:, 2].astype(float)
species = data[:, 4]

# Crear un subgràfic de punts per a cada espècie (n'hi ha 3)
plt.figure(figsize=(10, 6))

# Recorrem les espècies úniques i dibuixem els punts corresponents
for specie in np.unique(species):
    # Seleccionem les longituds de pètals i sèpals per a l'espècie actual
    petal_lengths = petal_length[species == specie]
    sepal_lengths = sepal_length[species == specie]
    # Dibuixem els punts per a aquesta espècie amb l'etiqueta corresponent
    plt.scatter(petal_lengths, sepal_lengths, label=specie)

plt.xlabel('Longitud dels pètals')
plt.ylabel('Longitud de les sèpals')
plt.title('Distribució de longituds de pètals i sèpals per espècie (Iris Dataset)')
plt.legend()
plt.grid(True)
plt.show()

Amb aquest gràfic, podem deduïr que la Iris Setosa sol tenir els pètals més petits respecte les altres 2 espècies i la Virginica els més grans.


Amb Matplotlib també pots generar ScatterPlots de 3D, molt útils si volem veure en un espai la correlació entre 3 variables que considerem que poden estar relacionades.

Comentar que en DataScience s'utilitza una tècnica per a resumir (reduïr) les 3 carecterístiques més rellevants d'un dataset que en tingui 3; la tècnica PCA (Principal Component Analysis).

Per aprofundir, si teniu curiositat de veure com funciona teniu un exemple a la web de la llibreria Scikit-learn.

  • https://scikit-learn.org/stable/auto_examples/decomposition/plot_pca_iris.html

Ara sí, anem a veure un exemple de com crear un cub 3D per veure quina relació té la longitud dels pètals de les plantes Iris respecte l'amplada i l'alçada dels seus sèpals (que com heu vist a la foto, el sèpal és la fulla que hi ha a la part inferior del pètal).

Necessitarem la importar extensió de Matplotlib mpl_toolkits; i les que haviem importat anteriorment per descarregar-nos el fitxer.

import matplotlib.pyplot as plt
import requests
import csv
import os
import numpy as np
from mpl_toolkits.mplot3d import Axes3D

# Carreguem el dataset d'Iris directament des de la URL
url_file = 'https://archive.ics.uci.edu/ml/machine-learning-databases/iris/iris.data'
local_file = 'iris.data'

# Verificar si el fitxer ja existeix localment
if not os.path.exists(local_file):
    # Descarregar el fitxer si no existeix
    response = requests.get(url_file)
    with open(local_file, 'wb') as f:
        f.write(response.content)

with open(local_file, 'r') as f:
    reader = csv.reader(f)
    iris = np.array([row for row in reader if row])  # Llegeix les files i les guarda en un array

# Seleccionem les tres característiques que volem visualitzar
# Convertim les columnes seleccionades a float
xdata = iris[:, 0].astype(float)  # sepal_length
ydata = iris[:, 1].astype(float)  # sepal_width
zdata = iris[:, 2].astype(float)  # petal_length

# Preparem el gràfic 3D
fig = plt.figure(figsize=(10, 6))
ax = fig.add_subplot(111, projection='3d')

# Creem el scatter plot 3D
scatter = ax.scatter(xdata, ydata, zdata, c=zdata, cmap='viridis')
ax.set_title('3D Scatter of Iris')
ax.set_xlabel('Sepal Length')
ax.set_ylabel('Sepal Width')
ax.set_zlabel('Petal Length')

# Afegim una barra de color per mostrar els valors de les dades
cbar = fig.colorbar(scatter, ax=ax, shrink=0.5, aspect=5)
cbar.set_label('Petal Length')
plt.show()

Histogrames i corbes de distribució normal

Una de les tasques més comuns en estadística (tant descriptiva = dades, com inferencial = probabilitats) és generar gràfics per veure quina distribució presenten les dades un cop sabem la freqüència de cada ocurrència d'una variable; i gràcies a Numpy i Matplotlib podem generar aquest gràfic molt fàcilment.

Una de les distribucions teòriques més utilitzada a la pràctica és la distribució normal, també anomenada distribució gaussiana en honor al matemàtic Carl Friedrich Gauss.

Ara veurem la potència que té Numpy per a generar una colecció de dades que segueixen la distribució Normal i Matplotlib per a dibuixar tant un histograma per veure la distribució i una línia per veure com s'assembla la distribució que tenim respecte la Normal.

Provem aquest exemple fictici del nivell de colesterol a la sang de 300 pacients.

import numpy as np
import matplotlib.pyplot as plt

# Paràmetres per a la distribució normal
mean = 180  # mitjana del colesterol (mg/dL)
std_dev = 25  # desviació estàndard (mg/dL)
n_samples = 300  # nombre de mostres

# Generar dades aleatòries amb una distribució normal
cholesterol_levels = np.random.normal(mean, std_dev, n_samples)

# Crear l'histograma
plt.hist(cholesterol_levels, bins=20, density=True, alpha=0.6, color='g', edgecolor='black')

# Crear la corba de distribució normal
xmin, xmax = plt.xlim()
x = np.linspace(xmin, xmax, 100)
p = np.exp(-0.5*((x-mean)/std_dev)**2) / (std_dev * np.sqrt(2 * np.pi))
plt.plot(x, p, 'k', linewidth=2)

# Afegir títol i etiquetes
plt.title('Distribució dels Nivells de Colesterol')
plt.xlabel('Nivells de Colesterol (mg/dL)')
plt.ylabel('Freqüència')

# Afegir línia vertical a la mitjana
plt.axvline(mean, color='b', linestyle='dashed', linewidth=2, label='Mitjana')

# Afegir línies verticals als intervals de confiança (±1 desviació estàndard)
plt.axvline(mean - std_dev, color='r', linestyle='dotted', linewidth=2, label='-1 Desviació estàndard')
plt.axvline(mean + std_dev, color='r', linestyle='dotted', linewidth=2, label='+1 Desviació estàndard')

# Afegir llegenda
plt.legend()

# Mostrar el gràfic
plt.show()

Activitat. A aquestes alçades hauràs provat diversos gràfics. Aquesta guia ha repetit el codi d'alguns gràfics per tal de facilitar que pugueu provar-lo en qualsevol entorn. Però la gràcia és que es pugui agrupar/refractoritar codi repetit.

Així que us proposem crear un projecte que tingui 2 classes/mòduls que continguin el codi repetitiu: baixar i llegir fitxers, crear gràfics i crear 2 classes/mòduls per a crear 2 gràfics del mateix tipus (de barres, heatmap, linies ...), que importin als mòduls amb el codi repetit.

Una estructura de noms pot ser la següent:

proj-matplotlib/
│
├── grafic1.py
├── grafic2.py
└── utils/
    ├── file-utils.py
    ├── barplot-utils.py

Enviar gràfics de Matplotlib (png) a la web amb Flask.

Per tal d'enviar els gràfics generats a un website ho podem fer mitjançant una llibreria/framework que ens permet programar aplicacions web amb Python.

En aquest tutorial usarem el microframework flask, que és molt senzill i eficient. Ideal per projectes petits.

Tot i això, més endavant, treballarem amb FastAPI, que també és senzill, modern i ens proporciona més funcionalitats que Flask.

Si voleu aprofundir en l'ús de Flask, consulteu:

Creem projecte web en Flask

Primer, crea una nova carpeta i aplica les següents comandes.

$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install --upgrade pip
(.venv) $ pip install matplotlib
(.venv) $ pip install Flask
(.venv) $ pip install pandas
(.venv) $ pip install gunicorn

O bé si ho prefereixes, pots clonar-te el projecte que hem preparat:

git clone https://gitlab.com/xtec/bio/matplotlib.git

Abans de desplegar el projecte, anem a analitzar com funciona Flask. Amb poques línies és capaç de respondre a crides HTTP GET.

Si dins del fitxer app.py tenim aquest codi:

from flask import Flask

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello, World!</p>"

Podem crear un endpoint, un punt d'entrada a un servei web: @app.route("/") aquesta instrucció indica la ruta que hem de posar al navegador per cridar al codi. def hello_world(): aquí hi ha la funció que s'executa.

Per desplegar el projecte, hem d'instal·lar les llibreries necessàries. Aquestes es troben al fitxer requirements.txt del projecte xtec/bio/matplotlib.

Seguiu les instruccions que hi ha al readme.md

Install virtual environment

$ sudo apt install python3-venv

Activate venv and install dependencies

$ python3 -m venv .venv
$ source .venv/bin/activate
(.venv) $ python3 -m pip install --upgrade pip
(.venv) $ pip install -r requirements-dev.txt

Start the web app:

$ flask run -h 0.0.0.0
...
* Running on all addresses (0.0.0.0)
* Running on http://127.0.0.1:5000/ (Press CTRL+C to quit)

Fire up a web browser and navigate to http://localhost:5000

Aquí tenim la mostra del funcionament:

Plantilles i URL's personalitzades amb Flask

Flask també pot implementar crides POST, PUT i DELETE (per enviar les dades des dels formularis de forma segura) però això ho farem amb FastAPI.

També pot crear URL's personalitzades en funció de paràmetres que li indiqui l'usuari. En veurem un exemple molt senzill que us convidem a provar.

Per aconseguir-ho, usem la llibreria de plantilles Jinja (la usen Flask, Django i algún framework més). Jinja proporciona aquests delimitadors per marcar la part del document que ha de processar:

TODO recuperar!!!

Aquesta s'ha de guardar obligatòriament dins la carpeta templates.

També hem de modificar la funció main perquè quan invoqui la funció render_template passi com argument el valor de la variable name que ha d’utilitzar Jinja per crear la pàgina web:

Aquí teniu el codi i el resultat:

Mostrem el gràfic Matplotlib a la web

Ja sabem com mostrar dinàmicament text; ara ens toca veure com enviar els gràfics que creem en Matplotlib a la web. És a dir, com enviar el gràfic en forma d'imatge (png).

Els gràfics de la llibreria Seaborn també es mostren de la mateixa manera.

Crearem l'endpoint /matplotlib que farà el següent:

  1. Crear un gràfic amb Matplotlib (ja hem vist com)
  2. Guardar la imatge png del gràfic en un buffer temporal.
  3. Enviar aquest gràfic a la web, que s'ubica a la carpeta templates.
  4. A la web HTML aprofitarem una plantilla Jinja per rebre la image. Hem d'especificar que és un mapa de bits en format base64.

Codi app.py

from flask import Flask, render_template
import base64
from io import BytesIO
from matplotlib.figure import Figure
import matplotlib.pyplot as plt

app = Flask(__name__)

@app.route("/")
def hello_world():
    return "<p>Hello Flask!</p>"

@app.route('/matplotlib')
def matplotlib():
    # Create plot with matplotlib
    plt.figure(figsize=(6, 4))
    plt.plot([1, 2, 3, 4, 5], [10, 20, 25, 30, 40])
    plt.title('Exemple de Gràfic')
    
   # Save it to a temporary buffer.
    buf = BytesIO()
    plt.savefig(buf, format='png')
    buf.seek(0)
    plot_url = base64.b64encode(buf.getvalue()).decode('utf8')

    # Renderitzar HTML amb la imatge PNG en base64
    return render_template('matplotlib.html', plot_url=plot_url)

if __name__ == "__main__":
    app.run()

Codi templates/matplotlib.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Gràfic des de Fitxer</title>
</head>
<body>
    <h2>Aquí tens el teu gràfic des d'un fitxer!</h2>
    <img src="data:image/png;base64,">
</body>
</html>

Fixa't que pots executar (símbol &) i parar Flask en segon pla:

flask run -h 0.0.0.0 &
pkill flask

Si vols automatitzar la càrrega d'un enpoint que retorna text (com el hello_world) ho pots fer amb la utilitat curl (amb Flask encès, clar):

curl http://127.0.0.1:5000/

Per automatitzar si es veu bé o no el gràfic:

firefox http://127.0.0.1:5000/matplotlib

Recorda que el codi font de tot el que hem fet el tens dins del projecte xtec/bio/matplotlib.

Activitat. Crea un servei web que mitjançant l'endpoint /temperatures/ generi un gràfic de barres amb les temperatures de 12 mesos d'un any passat per paràmetre. Aquestes temperatures les extrauràs del fitxer de Barcelona que hem tractat abans.

TODO


Referències.