Criando Heatmaps Anotados

Beginner

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

Introdução

Na análise de dados, frequentemente desejamos exibir dados que dependem de duas variáveis independentes como um gráfico de imagem codificado por cores, conhecido como um heatmap (mapa de calor). Neste laboratório, usaremos a função imshow do Matplotlib para criar um heatmap com anotações. Começaremos com um exemplo simples e o expandiremos para que seja utilizável como uma função universal.

Dicas para a VM

Após a inicialização da VM, clique no canto superior esquerdo para mudar para a aba Notebook e acessar o Jupyter Notebook para praticar.

Às vezes, pode ser necessário aguardar alguns segundos para que o Jupyter Notebook termine de carregar. A validação das operações não pode ser automatizada devido a limitações no Jupyter Notebook.

Se você enfrentar problemas durante o aprendizado, sinta-se à vontade para perguntar ao Labby. Forneça feedback após a sessão, e resolveremos o problema prontamente para você.

Heatmap Categórico Simples

Começaremos definindo alguns dados. Precisamos de uma lista ou array (matriz) 2D que defina os dados a serem codificados por cores. Em seguida, também precisamos de duas listas ou arrays de categorias. O heatmap em si é um gráfico imshow com os rótulos definidos para as categorias. Usaremos a função text para rotular os próprios dados, criando um objeto Text dentro de cada célula, mostrando o valor dessa célula.

import matplotlib.pyplot as plt
import numpy as np

vegetables = ["cucumber", "tomato", "lettuce", "asparagus", "potato", "wheat", "barley"]
farmers = ["Farmer Joe", "Upland Bros.", "Smith Gardening", "Agrifun", "Organiculture", "BioGoods Ltd.", "Cornylee Corp."]

harvest = 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(harvest)

## Show all ticks and label them with the respective list entries
ax.set_xticks(np.arange(len(farmers)), labels=farmers)
ax.set_yticks(np.arange(len(vegetables)), labels=vegetables)

## Rotate the tick labels and set their alignment
plt.setp(ax.get_xticklabels(), rotation=45, ha="right", rotation_mode="anchor")

## Loop over data dimensions and create text annotations
for i in range(len(vegetables)):
    for j in range(len(farmers)):
        text = ax.text(j, i, harvest[i, j], ha="center", va="center", color="w")

ax.set_title("Harvest of local farmers (in tons/year)")
fig.tight_layout()
plt.show()

Usando o Estilo de Código da Função Auxiliar

Criaremos uma função que recebe os dados e os rótulos das linhas e colunas como entrada e permite argumentos que são usados para personalizar o gráfico. Desativaremos as bordas dos eixos circundantes e criaremos uma grade de linhas brancas para separar as células. Aqui, também queremos criar uma barra de cores e posicionar os rótulos acima do heatmap em vez de abaixo dele. As anotações devem ter cores diferentes, dependendo de um limite para melhor contraste em relação à cor do pixel.

def heatmap(data, row_labels, col_labels, ax=None, cbar_kw=None, cbarlabel="", **kwargs):
    """
    Cria um heatmap a partir de um array numpy e duas listas de rótulos.

    Parâmetros
    ----------
    data
        Um array numpy 2D de forma (M, N).
    row_labels
        Uma lista ou array de comprimento M com os rótulos para as linhas.
    col_labels
        Uma lista ou array de comprimento N com os rótulos para as colunas.
    ax
        Uma instância `matplotlib.axes.Axes` para a qual o heatmap é plotado. Se não fornecido, use os eixos atuais ou crie um novo. Opcional.
    cbar_kw
        Um dicionário com argumentos para `matplotlib.Figure.colorbar`. Opcional.
    cbarlabel
        O rótulo para a barra de cores. Opcional.
    **kwargs
        Todos os outros argumentos são encaminhados para `imshow`.
    """

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

    if cbar_kw is None:
        cbar_kw = {}

    ## Plot the heatmap
    im = ax.imshow(data, **kwargs)

    ## Create colorbar
    cbar = ax.figure.colorbar(im, ax=ax, **cbar_kw)
    cbar.ax.set_ylabel(cbarlabel, rotation=-90, va="bottom")

    ## Show all ticks and label them with the respective list entries.
    ax.set_xticks(np.arange(data.shape[1]), labels=col_labels)
    ax.set_yticks(np.arange(data.shape[0]), labels=row_labels)

    ## Let the horizontal axes labeling appear on top.
    ax.tick_params(top=True, bottom=False, labeltop=True, labelbottom=False)

    ## Rotate the tick labels and set their alignment.
    plt.setp(ax.get_xticklabels(), rotation=-30, ha="right", rotation_mode="anchor")

    ## Turn spines off and create white grid.
    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):
    """
    Uma função para anotar um heatmap.

    Parâmetros
    ----------
    im
        O AxesImage a ser rotulado.
    data
        Dados usados para anotar. Se None, os dados da imagem são usados. Opcional.
    valfmt
        O formato das anotações dentro do heatmap. Isso deve usar o método de formatação de string, por exemplo, "$ {x:.2f}", ou ser um `matplotlib.ticker.Formatter`. Opcional.
    textcolors
        Um par de cores. A primeira é usada para valores abaixo de um limite, a segunda para aqueles acima. Opcional.
    threshold
        Valor em unidades de dados de acordo com o qual as cores de textcolors são aplicadas. Se None (o padrão), usa o meio do mapa de cores como separação. Opcional.
    **kwargs
        Todos os outros argumentos são encaminhados para cada chamada para `text` usada para criar os rótulos de texto.
    """

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

    ## Normalize the threshold to the images color range.
    if threshold is not None:
        threshold = im.norm(threshold)
    else:
        threshold = im.norm(data.max())/2.

    ## Set default alignment to center, but allow it to be overwritten by textkw.
    kw = dict(horizontalalignment="center", verticalalignment="center")
    kw.update(textkw)

    ## Get the formatter in case a string is supplied
    if isinstance(valfmt, str):
        valfmt = matplotlib.ticker.StrMethodFormatter(valfmt)

    ## Loop over the data and create a `Text` for each "pixel".
    ## Change the text's color depending on the data.
    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 a Função

Agora que temos as funções, podemos usá-las para criar um heatmap com anotações. Criamos um novo conjunto de dados, fornecemos argumentos adicionais para imshow, usamos um formato inteiro nas anotações e fornecemos algumas cores. Também ocultamos os elementos diagonais (que são todos 1) usando um matplotlib.ticker.FuncFormatter.

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

fig, ax = plt.subplots()
im, _ = heatmap(data, y, x, ax=ax, vmin=0, cmap="magma_r", cbarlabel="weekly sold copies")
annotate_heatmap(im, valfmt="{x:d}", size=7, threshold=20, textcolors=("red", "white"))

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

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

Exemplos de Heatmap Mais Complexos

A seguir, mostramos a versatilidade das funções criadas anteriormente, aplicando-as em diferentes casos e usando diferentes argumentos.

np.random.seed(19680801)

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

## Replicate the above example with a different font size and colormap.

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

## Sometimes even the data itself is categorical. Here we use a `matplotlib.colors.BoundaryNorm` to get the data into classes and use this to colorize the plot, but also to obtain the class labels from an array of classes.

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="Quality Rating")
annotate_heatmap(im, valfmt=fmt, size=9, fontweight="bold", threshold=-1, textcolors=("red", "black"))

## We can nicely plot a correlation matrix. Since this is bound by -1 and 1, we use those as vmin and vmax.

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

plt.tight_layout()
plt.show()

Resumo

Neste laboratório, aprendemos como criar heatmaps anotados em Python usando a função imshow do Matplotlib. Começamos criando um heatmap categórico simples e, em seguida, expandimos para ser uma função reutilizável. Finalmente, exploramos alguns exemplos de heatmap mais complexos usando diferentes argumentos.