Adicionando rótulos de valor em um gráfico de barras matplotlib

92

Fiquei preso em algo que parece que deveria ser relativamente fácil. O código que trago abaixo é um exemplo baseado em um projeto maior no qual estou trabalhando. Não vi motivo para postar todos os detalhes, então aceite as estruturas de dados que trago como estão.

Basicamente, estou criando um gráfico de barras e só posso descobrir como adicionar rótulos de valor nas barras (no centro da barra ou logo acima dela). Tenho procurado exemplos na web, mas sem sucesso na implementação em meu próprio código. Acredito que a solução seja com 'texto' ou 'anotar', mas eu: a) não sei qual usar (e, em geral, não descobri quando usar qual). b) não consigo ver para apresentar os rótulos de valor. Agradeceria sua ajuda, meu código abaixo. Desde já, obrigado!

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
pd.set_option('display.mpl_style', 'default') 
%matplotlib inline

# Bring some raw data.
frequencies = [6, 16, 75, 160, 244, 260, 145, 73, 16, 4, 1]

# In my original code I create a series and run on that, 
# so for consistency I create a series from the list.
freq_series = pd.Series.from_array(frequencies)

x_labels = [108300.0, 110540.0, 112780.0, 115020.0, 117260.0, 119500.0, 
            121740.0, 123980.0, 126220.0, 128460.0, 130700.0]

# Plot the figure.
plt.figure(figsize=(12, 8))
fig = freq_series.plot(kind='bar')
fig.set_title('Amount Frequency')
fig.set_xlabel('Amount ($)')
fig.set_ylabel('Frequency')
fig.set_xticklabels(x_labels)
Optimesh
fonte
2
Matplotlib tem uma demonstração: matplotlib.org/examples/api/barchart_demo.html
Dan

Respostas:

114

Em primeiro lugar, freq_series.plotretorna um eixo, não uma figura, então, para tornar minha resposta um pouco mais clara, alterei o código fornecido para se referir a ele como em axvez defig ser mais consistente com outros exemplos de código.

Você pode obter a lista das barras produzidas na trama do ax.patchesmembro. Então você pode usar a técnica demonstrada neste matplotlibexemplo de galeria para adicionar os rótulos usando o ax.textmétodo.

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Bring some raw data.
frequencies = [6, 16, 75, 160, 244, 260, 145, 73, 16, 4, 1]
# In my original code I create a series and run on that, 
# so for consistency I create a series from the list.
freq_series = pd.Series.from_array(frequencies)

x_labels = [108300.0, 110540.0, 112780.0, 115020.0, 117260.0, 119500.0,
            121740.0, 123980.0, 126220.0, 128460.0, 130700.0]

# Plot the figure.
plt.figure(figsize=(12, 8))
ax = freq_series.plot(kind='bar')
ax.set_title('Amount Frequency')
ax.set_xlabel('Amount ($)')
ax.set_ylabel('Frequency')
ax.set_xticklabels(x_labels)

rects = ax.patches

# Make some labels.
labels = ["label%d" % i for i in xrange(len(rects))]

for rect, label in zip(rects, labels):
    height = rect.get_height()
    ax.text(rect.get_x() + rect.get_width() / 2, height + 5, label,
            ha='center', va='bottom')

Isso produz um gráfico rotulado que se parece com:

insira a descrição da imagem aqui

Simon Gibbons
fonte
Oi Simon! Em primeiro lugar, muito obrigado por responder! 2, acho que não estava claro - queria mostrar o valor y. Acabei de substituir os rótulos no zip (,) por frequências. Agora, você poderia lançar um pouco mais de luz sobre o figo Vs machado? Me deixou confuso. Uma boa frase de pesquisa / recurso também seria ótimo, pois é um pouco genérico para uma pesquisa goog. Muito apreciado!
Optimesh
Uma figura é uma coleção de um ou mais eixos, por exemplo, neste exemplo, matplotlib.org/examples/statistics/… é uma figura composta de 4 eixos diferentes.
Simon Gibbons,
Obrigado novamente. Você pode me ajudar a entender a diferença entre anotação e texto? Obrigado!
Optimesh
2
Ambos podem ser usados ​​para adicionar texto a um gráfico. textsimplesmente imprime algum texto no gráfico, enquanto annotateé um auxiliar que você pode usar para adicionar facilmente também uma seta do texto apontando para um ponto específico no gráfico que está sendo referido pelo texto.
Simon Gibbons,
10
Ótima solução. Eu escrevi uma postagem de blog que se baseia na solução aqui e fornece uma versão um pouco mais robusta que escala de acordo com a altura do eixo, então o mesmo código funciona para diferentes plotagens que têm diferentes alturas de eixo: composition.al/blog/2015/ 29/11 /…
Lindsey Kuper
62

Com base em um recurso mencionado nesta resposta a outra pergunta , descobri uma solução muito aplicável para colocar rótulos em um gráfico de barras.

Outras soluções infelizmente não funcionam em muitos casos, porque o espaçamento entre o rótulo e a barra é dado em unidades absolutas das barras ou é escalado pela altura da barra . O primeiro só funciona para uma faixa estreita de valores e o último fornece espaçamento inconsistente dentro de um gráfico. Nenhum dos dois funciona bem com eixos logarítmicos.

A solução que proponho funciona independente da escala (ou seja, para números pequenos e grandes) e até mesmo coloca rótulos corretamente para valores negativos e com escalas logarítmicas porque usa a unidade visual pointspara deslocamentos.

Eu adicionei um número negativo para mostrar a colocação correta das etiquetas em tal caso.

O valor da altura de cada barra é usado como um rótulo para ela. Outros rótulos podem ser facilmente usados ​​com o for rect, label in zip(rects, labels)snippet do Simon .

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt

# Bring some raw data.
frequencies = [6, -16, 75, 160, 244, 260, 145, 73, 16, 4, 1]

# In my original code I create a series and run on that,
# so for consistency I create a series from the list.
freq_series = pd.Series.from_array(frequencies)

x_labels = [108300.0, 110540.0, 112780.0, 115020.0, 117260.0, 119500.0,
            121740.0, 123980.0, 126220.0, 128460.0, 130700.0]

# Plot the figure.
plt.figure(figsize=(12, 8))
ax = freq_series.plot(kind='bar')
ax.set_title('Amount Frequency')
ax.set_xlabel('Amount ($)')
ax.set_ylabel('Frequency')
ax.set_xticklabels(x_labels)


def add_value_labels(ax, spacing=5):
    """Add labels to the end of each bar in a bar chart.

    Arguments:
        ax (matplotlib.axes.Axes): The matplotlib object containing the axes
            of the plot to annotate.
        spacing (int): The distance between the labels and the bars.
    """

    # For each bar: Place a label
    for rect in ax.patches:
        # Get X and Y placement of label from rect.
        y_value = rect.get_height()
        x_value = rect.get_x() + rect.get_width() / 2

        # Number of points between bar and label. Change to your liking.
        space = spacing
        # Vertical alignment for positive values
        va = 'bottom'

        # If value of bar is negative: Place label below bar
        if y_value < 0:
            # Invert space to place label below
            space *= -1
            # Vertically align label at top
            va = 'top'

        # Use Y value as label and format number with one decimal place
        label = "{:.1f}".format(y_value)

        # Create annotation
        ax.annotate(
            label,                      # Use `label` as label
            (x_value, y_value),         # Place label at end of the bar
            xytext=(0, space),          # Vertically shift label by `space`
            textcoords="offset points", # Interpret `xytext` as offset in points
            ha='center',                # Horizontally center label
            va=va)                      # Vertically align label differently for
                                        # positive and negative values.


# Call the function above. All the magic happens there.
add_value_labels(ax)

plt.savefig("image.png")

Edit: Extraí a funcionalidade relevante em uma função, conforme sugerido por barnhillec .

Isso produz a seguinte saída:

Gráfico de barras com rótulos colocados automaticamente em cada barra

E com a escala logarítmica (e alguns ajustes nos dados de entrada para mostrar a escala logarítmica), este é o resultado:

Gráfico de barras com escala logarítmica com rótulos colocados automaticamente em cada barra

justfortherec
fonte
1
Resposta fantástica! Obrigado. Isso funcionou perfeitamente com pandas em plotagem de barra construída.
m4p85r
1
Melhoria sugerida: use ax.annotate em vez de plt.annotate. Essa mudança permitiria que toda a rotina fosse encapsulada em uma função que é passada por um eixo de eixo, que pode então ser fatorada em uma função útil de plotagem autônoma útil.
barnhillec
@barnhillec, obrigado pela sugestão. Eu fiz exatamente isso na minha edição. Observe que isso atualmente funciona apenas com gráficos de barras verticais e não com quaisquer outros tipos de gráficos (talvez com histogramas). Tornar a função mais genérica também tornaria mais difícil de entender e, portanto, menos adequada para uma resposta aqui.
justfortherec
Resposta muito robusta do que outras que encontrei. Explicar bem cada linha por comentário me ajuda a assimilar a noção inteira.
code_conundrum
31

Com base na resposta acima (ótimo!), Também podemos fazer um gráfico de barra horizontal com apenas alguns ajustes:

# Bring some raw data.
frequencies = [6, -16, 75, 160, 244, 260, 145, 73, 16, 4, 1]

freq_series = pd.Series(frequencies)

y_labels = [108300.0, 110540.0, 112780.0, 115020.0, 117260.0, 119500.0, 
            121740.0, 123980.0, 126220.0, 128460.0, 130700.0]

# Plot the figure.
plt.figure(figsize=(12, 8))
ax = freq_series.plot(kind='barh')
ax.set_title('Amount Frequency')
ax.set_xlabel('Frequency')
ax.set_ylabel('Amount ($)')
ax.set_yticklabels(y_labels)
ax.set_xlim(-40, 300) # expand xlim to make labels easier to read

rects = ax.patches

# For each bar: Place a label
for rect in rects:
    # Get X and Y placement of label from rect.
    x_value = rect.get_width()
    y_value = rect.get_y() + rect.get_height() / 2

    # Number of points between bar and label. Change to your liking.
    space = 5
    # Vertical alignment for positive values
    ha = 'left'

    # If value of bar is negative: Place label left of bar
    if x_value < 0:
        # Invert space to place label to the left
        space *= -1
        # Horizontally align label at right
        ha = 'right'

    # Use X value as label and format number with one decimal place
    label = "{:.1f}".format(x_value)

    # Create annotation
    plt.annotate(
        label,                      # Use `label` as label
        (x_value, y_value),         # Place label at end of the bar
        xytext=(space, 0),          # Horizontally shift label by `space`
        textcoords="offset points", # Interpret `xytext` as offset in points
        va='center',                # Vertically center label
        ha=ha)                      # Horizontally align label differently for
                                    # positive and negative values.

plt.savefig("image.png")

gráfico de barra horizontal com anotações

Oleson
fonte
1
Para a grade mostrar:freq_series.plot(kind='barh', grid=True)
sinapan
Funciona perfeitamente mesmo com gráficos de barras do Grupo. obrigado.
Prabah
Muito bem feito com gráfico de barra horizontal!
code_conundrum
9

Se você quiser apenas rotular os pontos de dados acima da barra, você pode usar plt.annotate ()

Meu código:

import numpy as np
import matplotlib.pyplot as plt

n = [1,2,3,4,5,]
s = [i**2 for i in n]
line = plt.bar(n,s)
plt.xlabel('Number')
plt.ylabel("Square")

for i in range(len(s)):
plt.annotate(str(s[i]), xy=(n[i],s[i]))

plt.show()

resultado

Bem, o texto com vários caracteres pode ser exibido um pouco fora do centro. Mas isso pode ser superado diminuindo ligeiramente a coordenada x no parâmetro xy dependendo do tamanho do texto

Ajay
fonte
limpo e simples
Ethan Yanjia Li
Você pode adicionar como podemos colocar o rótulo exatamente no centro?
x89
0

Se você quiser apenas adicionar pontos de dados acima das barras, pode facilmente fazer isso com:

 for i in range(len(frequencies)): # your number of bars
    plt.text(x = x_values[i]-0.25, #takes your x values as horizontal positioning argument 
    y = y_values[i]+1, #takes your y values as vertical positioning argument 
    s = data_labels[i], # the labels you want to add to the data
    size = 9) # font size of datalabels
user11638654
fonte