Gráficos honestos
En resumen: Un gráfico engañoso muestra una tabla verdadera con una impresión falsa (un eje cortado, dos ejes, 3D); uno honesto empieza las barras en cero y enuncia la conclusión en el título.
Problema
«Septiembre se desplomó»
Un compañero envía un gráfico para la reunión con el gerente: los ingresos de los últimos cuatro meses. La barra de septiembre casi ha desaparecido. Parece un desastre.
Septiembre es realmente más flojo: 7128,80 € frente a 8812,70 € en agosto. Por qué, lo descubriremos en el proyecto del final del curso. Aquí la pregunta es otra: ¿cuánto más flojo parece en el gráfico, y cuánto más flojo es de verdad?
La idea
Las barras empiezan en cero
El ojo lee una barra por su longitud: una barra el doble de larga significa el doble. Por eso la longitud debe ser el propio número. El gráfico del compañero empieza en 7000 (set_ylim(7000, 10000)), y eso se llama eje truncado. Aquí están los mismos números con un eje desde 7000 y desde 0:
La caída real de agosto a septiembre es del 19,1 %. A la izquierda la barra de septiembre es un 92,9 % más corta que la de agosto: la caída parece 4,9 veces mayor. El eje truncado quita los 7000 € de abajo que comparten los dos meses y deja solo la diferencia.
matplotlib empieza las barras en 0 por sí mismo. Un eje truncado viene de set_ylim o de herramientas que «acercan» el eje por su cuenta. Si la diferencia entre las barras no se ve desde 0, es pequeña. Eso también es una respuesta.
Más precisamente, para curiosos
Una línea es distinta: se lee por su pendiente y por cuándo sube y baja, no por la longitud. El eje de una línea puede empezar en otro sitio que no sea 0 si tiene etiqueta. Una línea que cambia unos pocos por ciento parece casi plana en un eje desde 0, y el cambio no se ve.
Pruébalo
Corrige el gráfico
Alguien dibujó los ingresos por mes con un eje desde 7000. Corrige el gráfico: 12 barras con los ingresos de cada mes (monthly, sin los pedidos cancelados), y el eje y empezando en 0 y llegando por encima de diciembre.
Solución
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Los ingresos dejan fuera los pedidos cancelados.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly)
ax.set_ylim(0, 17000)
ax.set_title('Ingresos por mes, €')
ax.tick_params(axis='x', rotation=90)
print(monthly.min().round(2), monthly.max().round(2))ax.bar(months, monthly) con ax.set_ylim(0, 17000) muestra el mes más flojo (7128,80 €) algo por debajo de la mitad del más fuerte (15 915,90 €), tal como es. Con un eje desde 7000 los meses flojos casi desaparecen. Si dibujas los ingresos de orders, los pedidos cancelados también entran en las barras.
La idea
Dos ejes y
Los ingresos están en miles de euros, y los pedidos en cientos. Para ponerlos en un gráfico, a menudo se da a la segunda línea su propio eje a la derecha: ax.twinx() crea un nuevo eje y que comparte el eje x con el primero. Pero la escala de cada eje se elige por separado, y cada línea se estira para llenar la suya. Así dos líneas pueden parecer fundidas o separadas, como quiera el autor.
La alternativa honesta es un índice: dividir cada mes entre el primero y multiplicar por 100. by_month / by_month.iloc[0] * 100 lo hace para las dos columnas a la vez (broadcasting sobre las columnas). Entonces las dos líneas están en un eje y empiezan en 100:
A la izquierda las líneas casi coinciden, y los ingresos y los pedidos parecen moverse juntos. A la derecha ves la diferencia: en diciembre los ingresos son 171,2 y los pedidos 136,7. Los ingresos crecieron mucho más que los pedidos, porque los pedidos de diciembre también son más grandes. Lo viste también en los diagramas de caja de la lección anterior.
Otra manera honesta son dos gráficos uno debajo del otro, cada uno con su propio eje y etiqueta: plt.subplots(2, 1, sharex=True).
La idea
3D, gráficos circulares e imágenes
Tres cosas que impresionan y engañan:
- Barras 3D y gráficos circulares 3D. La perspectiva hace mayor lo que está cerca que lo que está lejos. La tercera dimensión no lleva datos; solo distorsiona.
- Gráficos circulares. El ojo compara ángulos y áreas mucho peor que longitudes. Lo viste en la lección sobre cómo elegir un gráfico: las barras ordenadas dicen lo mismo con más claridad.
- Un valor en el tamaño de una imagen. Si la imagen crece en altura y en anchura, su área crece con el cuadrado: el doble parece cuatro veces.
La idea
Color para la respuesta
El color no es decoración: dice dónde mirar. Un color para lo que importa y gris para el resto es lo más claro. Aquí están los ingresos por mes dos veces: a la izquierda todas las barras son iguales, a la derecha diciembre está coloreado:
np.where(monthly.index.month == 12, '#E69F00', 'lightgray') da un color por mes: naranja para diciembre, gris para los demás. color=colors los pone en las barras.
El título de la derecha también funciona: no «Ingresos por mes», sino la conclusión: «Diciembre aporta el 14 % de los ingresos anuales» (exactamente el 14,3 %). El lector lo entiende antes de mirar los ejes.
El naranja #E69F00 es de la paleta Okabe-Ito: ocho colores que las personas que no ven algunos colores pueden distinguir igualmente. Muchas personas no distinguen el rojo del verde. Por eso nunca separes dos cosas solo con rojo y verde. Si necesitas más colores, la paleta tab10 de matplotlib (los colores por defecto, de C0 a C9) también es un buen comienzo.
La idea
Etiquetas en lugar de leyenda
Seis líneas en seis colores y una leyenda en la esquina: el ojo salta entre la línea y la leyenda para averiguar cuál es cuál. Una etiqueta justo junto a la línea se ahorra eso. Aquí están los ingresos por categoría, con una leyenda a la izquierda y con etiquetas a la derecha:
ax.text(x, y, 'Cocina') escribe texto en el punto (x, y) de los datos. Los meses son texto, y matplotlib los coloca en las posiciones 0, 1, 2…, así que len(table) - 0.8 queda justo a la derecha del último mes, y 1.3 junto a diciembre. La etiqueta de la cocina tiene el azul de su línea, y la cocina se dibuja la última y más gruesa, así que queda encima.
En diciembre la cocina vende 3240,60 €, 4,4 veces más que su mes medio en el resto del año. Se venden sobre todo sartenes y cuchillos, probablemente como regalos. En el gráfico de la izquierda esto es una de seis líneas, en el de la derecha es lo primero que ves. La tarjeta de regalo no tiene categoría, por eso no está aquí.
Pruébalo
Colores
Ejercicio
El gráfico para el informe anual
El gerente quiere un gráfico para el informe anual: los ingresos por mes (monthly), diciembre en color, los demás meses en gris, el eje desde 0 y un título que diga cuánto de los ingresos del año aporta diciembre. Guarda la parte en share y los colores en colors.
Solución
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
orders = pd.read_csv('year_orders.csv', parse_dates=['ordered_at'])
items = pd.read_csv('year_items.csv')
items['revenue'] = items['quantity'] * items['unit_price']
per_order = items.groupby('order_id', as_index=False)['revenue'].sum()
orders = orders.merge(per_order, left_on='id', right_on='order_id', how='left', validate='one_to_one')
# Los ingresos dejan fuera los pedidos cancelados.
sold = orders[orders['status'] != 'cancelled']
monthly = sold.set_index('ordered_at')['revenue'].resample('MS').sum()
months = monthly.index.strftime('%Y-%m')
share = monthly['2025-12-01'] / monthly.sum() * 100
colors = np.where(monthly.index.month == 12, '#E69F00', 'lightgray')
fig, ax = plt.subplots(figsize=(9, 4))
ax.bar(months, monthly, color=colors)
ax.set_ylim(0, 17000)
ax.set_title(f'Diciembre aporta el {share:.0f} % de los ingresos anuales')
ax.tick_params(axis='x', rotation=90)Diciembre aporta el 14,3 % de los ingresos anuales. Si todos los meses fueran iguales, sería 1/12, cerca del 8,3 %. np.where(monthly.index.month == 12, "#E69F00", "lightgray") colorea solo diciembre, y f"Diciembre aporta el {share:.0f} % de los ingresos anuales" pone la respuesta en el título. Un título como «Ingresos por mes» solo dice lo que hay en los ejes, y unas barras que se ven todas iguales dejan que el lector busque solo.
Test
Ponte a prueba
Lecciones relacionadas
Sección «Lecciones relacionadas»- Se basa en: Ancho y largo: pivot_table y melt · Fechas y series temporales · El primer gráfico · Qué gráfico para qué pregunta
- Continúa en: ¿Por qué bajaron los ingresos?
- Ejercicios sobre el tema: Gráficos