Creación de mapas de calor con anotaciones

MatplotlibMatplotlibBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En el análisis de datos, a menudo queremos mostrar datos que dependen de dos variables independientes como una gráfica de imagen codificada por colores conocida como mapa de calor. En este laboratorio, usaremos la función imshow de Matplotlib para crear un mapa de calor con anotaciones. Comenzaremos con un ejemplo simple y lo expandiremos para que sea usable como una función universal.

Consejos sobre la VM

Una vez que se haya completado la inicialización de la VM, haga clic en la esquina superior izquierda para cambiar a la pestaña Cuaderno y acceder a Jupyter Notebook para practicar.

A veces, es posible que tenga que esperar unos segundos a que Jupyter Notebook termine de cargarse. La validación de las operaciones no se puede automatizar debido a las limitaciones de Jupyter Notebook.

Si tiene problemas durante el aprendizaje, no dude en preguntar a Labby. Deje comentarios después de la sesión y lo resolveremos rápidamente para usted.

Mapa de calor categórico simple

Comenzaremos definiendo algunos datos. Necesitamos una lista o matriz bidimensional que defina los datos para codificar por colores. Luego también necesitamos dos listas o matrices de categorías. El mapa de calor en sí es una gráfica imshow con las etiquetas establecidas en las categorías. Usaremos la función text para etiquetar los datos mismos creando un Text dentro de cada celda que muestre el valor de esa celda.

import matplotlib.pyplot as plt
import numpy as np

vegetales = ["pepino", "tomate", "lechuga", "espárrago", "patata", "trigo", "cebada"]
granjeros = ["Granjero Joe", "Upland Bros.", "Smith Gardening", "Agrifun", "Organiculture", "BioGoods Ltd.", "Cornylee Corp."]

cosecha = np.array([[0.8, 2.4, 2.5, 3.9, 0.0, 4.0, 0.0],
                    [2.4, 0.0, 4.0, 1.0, 2.7, 0.0, 0.0],
                    [1.1, 2.4, 0.8, 4.3, 1.9, 4.4, 0.0],
                    [0.6, 0.0, 0.3, 0.0, 3.1, 0.0, 0.0],
                    [0.7, 1.7, 0.6, 2.6, 2.2, 6.2, 0.0],
                    [1.3, 1.2, 0.0, 0.0, 0.0, 3.2, 5.1],
                    [0.1, 2.0, 0.0, 1.4, 0.0, 1.9, 6.3]])

fig, ax = plt.subplots()
im = ax.imshow(cosecha)

## Muestra todas las marcas de división y las etiqueta con las respectivas entradas de la lista
ax.set_xticks(np.arange(len(granjeros)), labels=granjeros)
ax.set_yticks(np.arange(len(vegetales)), labels=vegetales)

## Rota las etiquetas de las marcas de división y establece su alineación
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")

## Itera sobre las dimensiones de los datos y crea anotaciones de texto
for i in range(len(vegetales)):
    for j in range(len(granjeros)):
        text = ax.text(j, i, cosecha[i, j], ha="center", va="center", color="w")

ax.set_title("Cosecha de los granjeros locales (en toneladas/anio)")
fig.tight_layout()
plt.show()

Estilo de código de la función auxiliar

Crearemos una función que tome los datos y las etiquetas de filas y columnas como entrada y permita argumentos que se usen para personalizar la gráfica. Apagaremos las espinas de los ejes circundantes y crearemos una cuadrícula de líneas blancas para separar las celdas. Aquí, también queremos crear una barra de colores y posicionar las etiquetas encima del mapa de calor en lugar de debajo de él. Las anotaciones tendrán diferentes colores según un umbral para un mejor contraste con el color de los píxeles.

def heatmap(data, row_labels, col_labels, ax=None, cbar_kw=None, cbarlabel="", **kwargs):
    """
    Crea un mapa de calor a partir de una matriz de numpy y dos listas de etiquetas.

    Parámetros
    ----------
    data
        Una matriz bidimensional de numpy de forma (M, N).
    row_labels
        Una lista o matriz de longitud M con las etiquetas de las filas.
    col_labels
        Una lista o matriz de longitud N con las etiquetas de las columnas.
    ax
        Una instancia de `matplotlib.axes.Axes` a la que se traza el mapa de calor. Si no se proporciona, se utiliza el eje actual o se crea uno nuevo. Opcional.
    cbar_kw
        Un diccionario con argumentos para `matplotlib.Figure.colorbar`. Opcional.
    cbarlabel
        La etiqueta para la barra de colores. Opcional.
    **kwargs
        Todos los demás argumentos se transmiten a `imshow`.
    """

    if ax is None:
        ax = plt.gca()

    if cbar_kw is None:
        cbar_kw = {}

    ## Dibuja el mapa de calor
    im = ax.imshow(data, **kwargs)

    ## Crea la barra de colores
    cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw)
    cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom")

    ## Muestra todas las marcas de división y las etiqueta con las respectivas entradas de la lista.
    ax.set_xticks(np.arange(data.shape[1]), labels=col_labels)
    ax.set_yticks(np.arange(data.shape[0]), labels=row_labels)

    ## Hace que las etiquetas de los ejes horizontales aparezcan en la parte superior.
    ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False)

    ## Rota las etiquetas de las marcas de división y establece su alineación.
    plt.setp(ax.get_xticklabels(), rotation=-30, ha="right", rotation_mode="anchor")

    ## Apaga las espinas y crea una cuadrícula blanca.
    ax.spines[:].set_visible(False)
    ax.set_xticks(np.arange(data.shape[1]+1)-.5, minor=True)
    ax.set_yticks(np.arange(data.shape[0]+1)-.5, minor=True)
    ax.grid(which="minor", color="w", linestyle='-', linewidth=3)
    ax.tick_params(which="minor", bottom=False, left=False)

    return im, cbar


def annotate_heatmap(im, data=None, valfmt="{x:.2f}", textcolors=("black", "white"), threshold=None, **textkw):
    """
    Una función para anotar un mapa de calor.

    Parámetros
    ----------
    im
        La AxesImage que se va a etiquetar.
    data
        Datos utilizados para la anotación. Si es None, se utilizan los datos de la imagen. Opcional.
    valfmt
        El formato de las anotaciones dentro del mapa de calor. Esto debe utilizar el método de formato de cadenas, por ejemplo, "$ {x:.2f}", o ser un `matplotlib.ticker.Formatter`. Opcional.
    textcolors
        Un par de colores. El primero se utiliza para los valores por debajo de un umbral, el segundo para aquellos por encima. Opcional.
    threshold
        Valor en unidades de datos según el cual se aplican los colores de textcolors. Si es None (el valor predeterminado), se utiliza el centro de la paleta de colores como separación. Opcional.
    **kwargs
        Todos los demás argumentos se transmiten a cada llamada a `text` utilizada para crear las etiquetas de texto.
    """

    if not isinstance(data, (list, np.ndarray)):
        data = im.get_array()

    ## Normaliza el umbral al rango de colores de la imagen.
    if threshold is not None:
        threshold = im.norm(threshold)
    else:
        threshold = im.norm(data.max())/2.

    ## Establece la alineación predeterminada en el centro, pero permite que se reescriba por textkw.
    kw = dict(horizontalalignment="center", verticalalignment="center")
    kw.update(textkw)

    ## Obtiene el formateador en caso de que se proporcione una cadena
    if isinstance(valfmt, str):
        valfmt = matplotlib.ticker.StrMethodFormatter(valfmt)

    ## Itera sobre los datos y crea un `Text` para cada "pixel".
    ## Cambia el color del texto según los datos.
    texts = []
    for i in range(data.shape[0]):
        for j in range(data.shape[1]):
            kw.update(color=textcolors[int(im.norm(data[i, j]) > threshold)])
            text = im.axes.text(j, i, valfmt(data[i, j], None), **kw)
            texts.append(text)

    return texts

Aplicando la función

Ahora que tenemos las funciones, podemos usarlas para crear un mapa de calor con anotaciones. Creamos un nuevo conjunto de datos, damos argumentos adicionales a imshow, usamos un formato entero en las anotaciones y proporcionamos algunos colores. También ocultamos los elementos de la diagonal (que son todos 1) usando un matplotlib.ticker.FuncFormatter.

data = np.random.randint(2, 100, size=(7, 7))
y = [f"Libro {i}" for i in range(1, 8)]
x = [f"Tienda {i}" for i in list("ABCDEFG")]

fig, ax = plt.subplots()
im, _ = heatmap(data, y, x, ax=ax, vmin=0, cmap="magma_r", cbarlabel="copias vendidas semanalmente")
annotate_heatmap(im, valfmt="{x:d}", size=7, threshold=20, textcolors=("rojo", "blanco"))

def func(x, pos):
    return f"{x:.2f}".replace("0.", ".").replace("1.00", "")

annotate_heatmap(im, valfmt=matplotlib.ticker.FuncFormatter(func), size=7)

Ejemplos de mapas de calor más complejos

A continuación, demostramos la versatilidad de las funciones creadas anteriormente al aplicarlas en diferentes casos y utilizar diferentes argumentos.

np.random.seed(19680801)

fig, ((ax, ax2), (ax3, ax4)) = plt.subplots(2, 2, figsize=(8, 6))

## Repite el ejemplo anterior con un tamaño de fuente y una paleta de colores diferentes.

im, _ = heatmap(harvest, vegetables, farmers, ax=ax, cmap="Wistia", cbarlabel="cosecha [t/anio]")
annotate_heatmap(im, valfmt="{x:.1f}", size=7)

## A veces, incluso los datos mismos son categóricos. Aquí usamos un `matplotlib.colors.BoundaryNorm` para clasificar los datos y usarlos para colorear la gráfica, pero también para obtener las etiquetas de clase a partir de una matriz de clases.

data = np.random.randn(6, 6)
y = [f"Prod. {i}" for i in range(10, 70, 10)]
x = [f"Cycle {i}" for i in range(1, 7)]

qrates = list("ABCDEFG")
norm = matplotlib.colors.BoundaryNorm(np.linspace(-3.5, 3.5, 8), 7)
fmt = matplotlib.ticker.FuncFormatter(lambda x, pos: qrates[::-1][norm(x)])

im, _ = heatmap(data, y, x, ax=ax3, cmap=mpl.colormaps["PiYG"].resampled(7), norm=norm, cbar_kw=dict(ticks=np.arange(-3, 4), format=fmt), cbarlabel="Calificación de calidad")
annotate_heatmap(im, valfmt=fmt, size=9, fontweight="bold", threshold=-1, textcolors=("rojo", "negro"))

## Podemos trazar una matriz de correlación de manera agradable. Dado que está limitada por -1 y 1, los usamos como vmin y vmax.

corr_matrix = np.corrcoef(harvest)
im, _ = heatmap(corr_matrix, vegetables, vegetables, ax=ax4, cmap="PuOr", vmin=-1, vmax=1, cbarlabel="coeficiente de correlación")
annotate_heatmap(im, valfmt=matplotlib.ticker.FuncFormatter(func), size=7)

plt.tight_layout()
plt.show()

Resumen

En este laboratorio, aprendimos cómo crear mapas de calor con anotaciones en Python utilizando la función imshow de Matplotlib. Comenzamos creando un mapa de calor categórico simple y luego lo expandimos para convertirlo en una función reusable. Finalmente, exploramos algunos ejemplos de mapas de calor más complejos utilizando diferentes argumentos.