Agregar etiquetas de valor en un gráfico de barras matplotlib

95

Me quedé atascado en algo que parece que debería ser relativamente fácil. El código que traigo a continuación es una muestra basada en un proyecto más grande en el que estoy trabajando. No vi ninguna razón para publicar todos los detalles, así que acepte las estructuras de datos que traigo como están.

Básicamente, estoy creando un gráfico de barras y puedo descubrir cómo agregar etiquetas de valor en las barras (en el centro de la barra o justo encima de ella). He estado buscando ejemplos en la web, pero no he podido implementarlo en mi propio código. Creo que la solución es con 'texto' o 'anotar', pero yo: a) no sé cuál usar (y en general, no he descubierto cuándo usar cuál). b) no puedo ver para que ninguno de los dos presente las etiquetas de valor. Agradecería su ayuda, mi código a continuación. ¡Gracias por adelantado!

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
fuente
2
Matplotlib tiene una demostración: matplotlib.org/examples/api/barchart_demo.html
Dan

Respuestas:

119

En primer lugar, freq_series.plotdevuelve un eje, no una figura, por lo que para que mi respuesta sea un poco más clara, he cambiado el código dado para referirme a él en axlugar de figser más consistente con otros ejemplos de código.

Puede obtener la lista de las barras producidas en la trama del ax.patchesmiembro. Luego, puede usar la técnica demostrada en este matplotlibejemplo de galería para agregar las etiquetas usando el 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')

Esto produce una trama etiquetada que se parece a:

ingrese la descripción de la imagen aquí

Simon Gibbons
fuente
Hola simon Primero, ¡muchas gracias por responder! En segundo lugar, supongo que no estaba claro: quería mostrar el valor y. Reemplacé las etiquetas en el zip (,) con frecuencias. Ahora, ¿podría arrojar algo más de luz sobre fig Vs ax? Me confundió. Una buena frase / recurso de búsqueda también sería genial, ya que es un poco genérico para una búsqueda de Google. ¡Muy apreciado!
Optimesh
Una figura es una colección de uno o más ejes, por ejemplo, en este en este ejemplo matplotlib.org/examples/statistics/… es una figura que se compone de 4 ejes diferentes.
Simon Gibbons
Gracias de nuevo. ¿Puede ayudarme a entender la diferencia entre anotar y texto? ¡Gracias!
Optimesh
2
Ambos se pueden usar para agregar texto a una trama. textsimplemente imprime algo de texto en la trama, mientras que annotatees un ayudante que puede usar para agregar fácilmente también una flecha desde el texto que apunta a un punto específico en la trama al que se refiere el texto.
Simon Gibbons
10
Buena solucion. Escribí una publicación de blog que se basa en la solución aquí y ofrece una versión un poco más robusta que se escala de acuerdo con la altura del eje, por lo que el mismo código funciona para diferentes parcelas que tienen diferentes alturas de eje: composición.al/
Lindsey Kuper
65

Basado en una característica mencionada en esta respuesta a otra pregunta , he encontrado una solución de aplicación muy general para colocar etiquetas en un gráfico de barras.

Desafortunadamente, otras soluciones no funcionan en muchos casos, porque el espacio entre la etiqueta y la barra se da en unidades absolutas de las barras o se escala por la altura de la barra . El primero solo funciona para un rango estrecho de valores y el segundo proporciona un espaciado inconsistente dentro de un gráfico. Ninguno de los dos funciona bien con ejes logarítmicos.

La solución que propongo funciona independientemente de la escala (es decir, para números pequeños y grandes) e incluso coloca correctamente etiquetas para valores negativos y con escalas logarítmicas porque usa la unidad visual pointspara compensaciones.

Agregué un número negativo para mostrar la ubicación correcta de las etiquetas en tal caso.

El valor de la altura de cada barra se utiliza como etiqueta. Se pueden usar fácilmente otras etiquetas con el for rect, label in zip(rects, labels)fragmento de 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")

Editar: he extraído la funcionalidad relevante en una función, como sugirió barnhillec .

Esto produce la siguiente salida:

Gráfico de barras con etiquetas colocadas automáticamente en cada barra

Y con la escala logarítmica (y algunos ajustes a los datos de entrada para mostrar la escala logarítmica), este es el resultado:

Gráfico de barras con escala logarítmica con etiquetas colocadas automáticamente en cada barra

justfortherec
fuente
1
¡Respuesta fantástica! Gracias. Esto funcionó perfectamente con pandas en el trazado de barras integrado.
m4p85r
1
Mejora sugerida: use ax.annotate en lugar de plt.annotate. Este cambio permitiría encapsular toda la rutina en una función a la que se le pasa un eje ax, que luego se puede factorizar en una útil función de utilidad de trazado independiente.
barnhillec
@barnhillec, gracias por la sugerencia. He hecho exactamente eso en mi edición. Tenga en cuenta que esto actualmente solo funciona con gráficos de barras verticales y no con otros tipos de gráficos (tal vez con histogramas). Hacer la función más genérica también la haría más difícil de entender y, por lo tanto, menos adecuada para una respuesta aquí.
justfortherec
Respuesta muy robusta que otras que encontré. Explique bien cada línea con un comentario, ayúdeme a asimilar toda la noción.
code_conundrum
34

Partiendo de la respuesta anterior (¡genial!), También podemos hacer un diagrama de barras horizontales con solo unos pocos 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 barras horizontales con anotaciones

Oleson
fuente
1
Para que la cuadrícula muestre:freq_series.plot(kind='barh', grid=True)
sinapan
Funciona perfectamente incluso con gráficos de barras de grupo. Gracias.
Prabah
¡Bien hecho con gráfico de barras horizontales!
code_conundrum
Para mí, los números se cruzan con el cuadro que rodea el gráfico de barras. ¿Hay alguna forma de prevenir esto?
bweber13
Resolví mi propio problema usandoax.set_xlim([0, 1.1*max_value])
bweber13
14

Si solo desea etiquetar los puntos de datos sobre la barra, puede usar plt.annotate ()

Mi 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]), ha='center', va='bottom')

plt.show()

Al especificar una alineación horizontal y vertical de 'center'y 'bottom'respectivamente, se pueden obtener anotaciones centradas.

un gráfico de barras etiquetado

Ajay
fuente
1
limpio y simple
Ethan Yanjia Li
¿Puede agregar cómo podemos colocar la etiqueta en el centro exacto?
x89
@ x89 Puede especificar la alineación horizontal y vertical del texto que realiza el centrado. - He editado la respuesta para mejorarla.
Simon Gibbons
0

Si solo desea agregar puntos de datos encima de las barras, puede hacerlo fácilmente con:

 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
usuario11638654
fuente