Lollipop

Python
code
Visualization
Author

Marco Aguirre

Published

September 29, 2025

Un gráfico de piruleta (Lollipop chart) es una variación del gráfico de barras en el que cada barra se sustituye por una línea delgada que termina en un punto. Al igual que el gráfico de barras, permite mostrar la relación entre una variable categórica y una numérica, pero con una apariencia más ligera y atractiva. Su simplicidad visual ayuda a evitar la sobrecarga gráfica cuando existen muchas categorías o valores muy similares.

Este tipo de gráfico resulta especialmente útil cuando se busca destacar diferencias sutiles entre categorías o cuando varias tienen valores cercanos, reduciendo el “efecto Moiré” que puede aparecer en un barplot clásico.

Para mejorar su claridad, conviene:
- Ordenar los grupos (idealmente de mayor a menor si no existe un orden natural).
- Usar la versión horizontal cuando las etiquetas son extensas, para mejorar la lectura.
- Resaltar los puntos con colores adecuados que faciliten la comparación.
- Usarlo cuando solo se represente un valor por categoría, sin subgrupos múltiples.

Errores habituales:

  • No ordenar las categorías: dificulta la lectura y comprensión.
  • Usarlo con varios valores por grupo: en ese caso es preferible un boxplot o violin plot.
  • Mantener los datos sin orden natural en lollipop puede confundir: en esas situaciones un barplot es más claro.
  • Forzar el uso cuando se necesita mostrar distribuciones completas; el lollipop solo muestra un valor central.

Dataset: Precios Agroindustria de Cacao (2012 - 2025)

Archivo: MAG_PreciosAgroindustriaCacao_2025Junio.csv
Fuente: Ministerio de Agricultura y Ganadería (MAG), Ecuador
Última actualización: Junio 2025

import pandas as pd
df = pd.read_csv("mag_preciosagroindustriacacao_2025junio.csv", sep=";")
print(df.head())
print(df.info())
   PACC_ANIO PACC_MES DPA_PROVINCIA DPA_CANTON        PACC_PRODUCTO  \
0       2012    Enero        El Oro  Arenillas  Cacao seco mezclado   
1       2012    Enero        El Oro  Arenillas  Cacao seco mezclado   
2       2012    Enero        El Oro   El Guabo  Cacao seco mezclado   
3       2012    Enero        El Oro   El Guabo  Cacao seco mezclado   
4       2012    Enero        El Oro    Machala  Cacao seco mezclado   

          PACC_PRESENTACION PACC_TIPO  PACC_PRECIO_USD  PACC_USD_KG  
0  Quintal de 100,00 libras    Compra            71.41         1.57  
1  Quintal de 100,00 libras     Venta            74.06         1.63  
2  Quintal de 100,00 libras    Compra            73.75         1.63  
3  Quintal de 100,00 libras     Venta            84.00         1.85  
4  Quintal de 100,00 libras    Compra            75.00         1.65  
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 26246 entries, 0 to 26245
Data columns (total 9 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   PACC_ANIO          26246 non-null  int64  
 1   PACC_MES           26246 non-null  object 
 2   DPA_PROVINCIA      26246 non-null  object 
 3   DPA_CANTON         26246 non-null  object 
 4   PACC_PRODUCTO      26246 non-null  object 
 5   PACC_PRESENTACION  26246 non-null  object 
 6   PACC_TIPO          26246 non-null  object 
 7   PACC_PRECIO_USD    26246 non-null  float64
 8   PACC_USD_KG        26246 non-null  float64
dtypes: float64(2), int64(1), object(6)
memory usage: 1.8+ MB
None
import plotly.graph_objects as go
import plotly.io as pio
pio.renderers.default = "notebook"
import matplotlib.pyplot as plt

# Agrupar para obtener el precio promedio por año
df_avg_anio = df.groupby("PACC_ANIO", as_index=False)["PACC_PRECIO_USD"].mean()

# Datos
x = df_avg_anio["PACC_ANIO"]
y = df_avg_anio["PACC_PRECIO_USD"]

# Crear figura
plt.figure(figsize=(10, 6))

# Dibujar líneas desde 0 hasta cada valor (tallo del lollipop)
for i in range(len(x)):
    plt.plot([x[i], x[i]], [0, y[i]], color="darkblue", linewidth=2)
    plt.scatter(x[i], y[i], 
                c=y[i], cmap="viridis", 
                s=120, edgecolor="black", zorder=3)

    # Texto encima de cada punto
    plt.text(x[i], y[i] + 0.5, f"{y[i]:.2f}", 
             ha="center", va="bottom", fontsize=10)

# Estilo general
plt.title("Precio promedio en USD por año (Lollipop Chart)", fontsize=16, color="darkblue")
plt.xlabel("Año")
plt.ylabel("Precio USD")
plt.grid(axis="y", linestyle="--", alpha=0.6)
plt.gca().set_facecolor("white")

plt.show()

import numpy as np
import plotly.graph_objs as go
import plotly.offline as pyo

def plotly_lollipop(signal, orientation="vertical",
                    marker_color="red", line_color="grey",
                    marker_offset=0.04, title="Lollipop Chart"):
    """
    Genera un gráfico tipo Lollipop (vertical u horizontal) con Plotly.

    Parámetros
    ----------
    signal : list, np.array o pd.Series
        Señal de valores numéricos a graficar.
    orientation : str
        'vertical' para palitos en eje Y (default), 'horizontal' para palitos en eje X.
    marker_color : str
        Color de los puntos (default = 'red').
    line_color : str
        Color de las líneas (default = 'grey').
    marker_offset : float
        Margen de separación entre la línea y el punto para evitar superposición.
    title : str
        Título del gráfico.
    """

    def offset_signal(val, marker_offset):
        if abs(val) <= marker_offset:
            return 0
        return val - marker_offset if val > 0 else val + marker_offset

    n = len(signal)

    # Puntos
    if orientation == "vertical":
        data = [go.Scatter(
            x=list(range(n)),
            y=signal,
            mode="markers",
            marker=dict(color=marker_color, size=8)
        )]
        shapes = [dict(
            type="line",
            xref="x",
            yref="y",
            x0=i, y0=0,
            x1=i, y1=offset_signal(signal[i], marker_offset),
            line=dict(color=line_color, width=1)
        ) for i in range(n)]

    elif orientation == "horizontal":
        data = [go.Scatter(
            x=signal,
            y=list(range(n)),
            mode="markers",
            marker=dict(color=marker_color, size=8)
        )]
        shapes = [dict(
            type="line",
            xref="x",
            yref="y",
            x0=0, y0=i,
            x1=offset_signal(signal[i], marker_offset), y1=i,
            line=dict(color=line_color, width=1)
        ) for i in range(n)]

    else:
        raise ValueError("orientation debe ser 'vertical' o 'horizontal'")

    # Layout
    layout = go.Layout(shapes=shapes, title=title)
    fig = go.Figure(data=data, layout=layout)
    pyo.iplot(fig)
    return fig


# Ejemplo de uso
if __name__ == "__main__":
    np.random.seed(42)
    random_signal = np.random.normal(size=20)

    # Vertical
    plotly_lollipop(random_signal, orientation="vertical", marker_color="blue", line_color="black", title="Lollipop Vertical")

    # Horizontal
    plotly_lollipop(random_signal, orientation="horizontal", marker_color="green", line_color="black", title="Lollipop Horizontal")
# Create a dataframe
import pandas as pd
df = pd.DataFrame({'group':list(map(chr, range(65, 85))), 'values':np.random.uniform(size=20) })

# Reorder it following the values:
ordered_df = df.sort_values(by='values')
my_range=range(1,len(df.index)+1)

# Make the plot
plt.stem(ordered_df['values'])
plt.xticks( my_range, ordered_df['group'])
([<matplotlib.axis.XTick at 0x15d4fdace00>,
  <matplotlib.axis.XTick at 0x15d4fdad4c0>,
  <matplotlib.axis.XTick at 0x15d4fdacb60>,
  <matplotlib.axis.XTick at 0x15d4fd95fd0>,
  <matplotlib.axis.XTick at 0x15d4fd95ee0>,
  <matplotlib.axis.XTick at 0x15d4fcbb800>,
  <matplotlib.axis.XTick at 0x15d4fcba180>,
  <matplotlib.axis.XTick at 0x15d4fd94620>,
  <matplotlib.axis.XTick at 0x15d4fcb8650>,
  <matplotlib.axis.XTick at 0x15d4fcb9c40>,
  <matplotlib.axis.XTick at 0x15d4fcbb8f0>,
  <matplotlib.axis.XTick at 0x15d4fcbb950>,
  <matplotlib.axis.XTick at 0x15d4fcbaf00>,
  <matplotlib.axis.XTick at 0x15d4fc35340>,
  <matplotlib.axis.XTick at 0x15d4fc341d0>,
  <matplotlib.axis.XTick at 0x15d4fc36000>,
  <matplotlib.axis.XTick at 0x15d4fc37200>,
  <matplotlib.axis.XTick at 0x15d4fc35520>,
  <matplotlib.axis.XTick at 0x15d4fc375c0>,
  <matplotlib.axis.XTick at 0x15d4fd00ef0>],
 [Text(1, 0, 'S'),
  Text(2, 0, 'F'),
  Text(3, 0, 'I'),
  Text(4, 0, 'N'),
  Text(5, 0, 'Q'),
  Text(6, 0, 'H'),
  Text(7, 0, 'C'),
  Text(8, 0, 'M'),
  Text(9, 0, 'P'),
  Text(10, 0, 'A'),
  Text(11, 0, 'R'),
  Text(12, 0, 'D'),
  Text(13, 0, 'E'),
  Text(14, 0, 'G'),
  Text(15, 0, 'O'),
  Text(16, 0, 'B'),
  Text(17, 0, 'L'),
  Text(18, 0, 'T'),
  Text(19, 0, 'J'),
  Text(20, 0, 'K')])

# --- Usamos la función definida antes ---
import plotly.graph_objs as go
import plotly.offline as pyo

def plotly_lollipop(x, y, orientation="vertical",
                    marker_color="red", line_color="grey",
                    marker_offset=0.04, title="Lollipop Chart"):
    """
    Genera un gráfico tipo Lollipop (vertical u horizontal) con Plotly.
    """
    def offset_signal(val, marker_offset):
        if abs(val) <= marker_offset:
            return 0
        return val - marker_offset if val > 0 else val + marker_offset

    n = len(y)

    if orientation == "vertical":
        data = [go.Scatter(
            x=x,
            y=y,
            mode="markers",
            marker=dict(color=marker_color, size=8)
        )]
        shapes = [dict(
            type="line",
            xref="x",
            yref="y",
            x0=x[i], y0=0,
            x1=x[i], y1=offset_signal(y[i], marker_offset),
            line=dict(color=line_color, width=1)
        ) for i in range(n)]

    elif orientation == "horizontal":
        data = [go.Scatter(
            x=y,
            y=x,
            mode="markers",
            marker=dict(color=marker_color, size=8)
        )]
        shapes = [dict(
            type="line",
            xref="x",
            yref="y",
            x0=0, y0=x[i],
            x1=offset_signal(y[i], marker_offset), y1=x[i],
            line=dict(color=line_color, width=1)
        ) for i in range(n)]

    else:
        raise ValueError("orientation debe ser 'vertical' o 'horizontal'")

    layout = go.Layout(
        shapes=shapes,
        title=title,
        xaxis=dict(title="Año" if orientation=="vertical" else "Precio USD"),
        yaxis=dict(title="Precio USD" if orientation=="vertical" else "Año")
    )

    fig = go.Figure(data=data, layout=layout)
    pyo.iplot(fig)
    return fig


# --- Aplicación a tu dataset ---
x = df_avg_anio["PACC_ANIO"]
y = df_avg_anio["PACC_PRECIO_USD"]

# Vertical
plotly_lollipop(x, y, orientation="vertical", marker_color="blue", line_color="black", title="Precio promedio en USD por año")

# Horizontal (opcional)
# plotly_lollipop(x, y, orientation="horizontal", marker_color="green", line_color="black", title="Precio promedio en USD por año (horizontal)")