Matplotlib constituye una de las bibliotecas más utilizadas en el ecosistema Python para la generación de gráficos. Junto con NumPy y Pandas, permite construir visualizaciones de forma programática, funcionando como una alternativa libre y flexible a herramientas comerciales como MATLAB.
Representación básica de series temporales y columnas
Para comenzar a trabajar con datos tabulares, es habitual cargar un archivo CSV mediante Pandas y convertir una columna temporal en el índice del DataFrame. A continuación se muestran distintas formas de representar filas y columnas individualmente.
import pandas as pd
import matplotlib.pyplot as plt
import altair as alt
%matplotlib inline
# Carga del conjunto de datos
dataset = pd.read_csv('datos.csv', encoding='utf-8', sep=',', on_bad_lines='skip')
dataset.set_index(pd.to_datetime(dataset['timestamp']), inplace=True)
# Representación de una columna concreta
dataset['throughput'].plot()
plt.show()
# Representación de una sola fila
dataset.iloc[3].plot()
plt.show()
# Representación de múltiples filas con leyenda
for idx in range(10):
dataset.iloc[idx].plot(label=f'Fila {idx}')
plt.legend()
plt.show()
Organización de gráficos en subplots
La función subplot facilita la disposición de varios gráficos dentro de la misma figura. El siguiente ejemplo contrasta un gráfico de barras vertical con uno horizontal, añadiendo líneas de referencia en el origen.
import numpy as np
import matplotlib.pyplot as plt
# Generación de datos sintéticos
valores_x = np.arange(5)
valores_y = np.random.randint(-5, 5, size=5)
# Panel izquierdo: barras verticales
plt.subplot(1, 2, 1)
plt.bar(valores_x, valores_y, color='steelblue')
plt.axhline(0, color='navy', linewidth=1.5)
# Panel derecho: barras horizontales
plt.subplot(1, 2, 2)
plt.barh(valores_x, valores_y, color='tomato')
plt.axvline(0, color='darkred', linewidth=1.5)
plt.tight_layout()
plt.show()
Superposición de series en un mismo gráfico
Para comparar dos conjuntos de datos sobre los mismos ejes, basta con invocar plot varias veces antes de mostrar la figura. La función legend permite identificar cada serie.
import matplotlib.pyplot as plt
eje_x = [1, 2, 3, 4, 5, 6, 7, 8]
serie_a = [1, 2, 3, 4, 55, 6, 6, 7]
serie_b = [6, 20, 9, 2, 5, 8, 2, 8]
plt.plot(eje_x, serie_a, marker='o')
plt.plot(eje_x, serie_b, marker='s')
plt.legend(['Serie A', 'Serie B'])
plt.show()
Tratamiento de índices jerárquicos
Cuando un DataFrame posee un índice multinivel, es necesario reorganizarlo antes de visualizarlo. El método unstack permite desplegar uno de los niveles como columnas, facilitando la representación gráfica.
import pandas as pd
import numpy as np
# Construcción de un DataFrame con índice multinivel
datos = pd.DataFrame(
np.arange(1, 5).reshape((4, 1)),
index=[['a', 'b', 'c', 'd'], ['A', 'B', 'C', 'D']],
columns=['valor']
)
# Asignación de nombres a los niveles del índice
datos = datos.rename_axis(index=['nivel_1', 'nivel_2'])
# Aplanado temporal del índice
datos = datos.reset_index()
# Reconstrucción del índice multinivel
datos.set_index(['nivel_1', 'nivel_2'], inplace=True)
# Despliegue del nivel_2 como columnas
matriz = datos.unstack(fill_value=0)['valor']
matriz.rename(columns={'A': 'cat_a', 'B': 'cat_b', 'C': 'cat_c'}, inplace=True)
Gráficos de barras apiladas porcentuales
Este tipo de visualización es útil cuando se desea mostrar la contribución relativa de varias categorías. A continuación se presenta una implementación que normaliza los valores a porcentajes y los representa tanto en orientación vertical como horizontal.
Variante vertical
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
def barras_apiladas_porcentuales(df):
categorias = df.index.tolist()
diccionario = df.to_dict(orient='list')
nombres_columnas = list(diccionario.keys())
matriz_datos = np.array(list(diccionario.values()))
# Selección de paleta de colores
colores = plt.get_cmap('RdYlGn')(
np.linspace(0.15, 0.85, matriz_datos.shape[0])
)
fig, ax = plt.subplots(figsize=(12, 9))
ax.yaxis.set_visible(False)
ax.set_xticks(range(len(categorias)))
ax.set_xticklabels(categorias, rotation=0)
ax.set_ylim(0, 1)
base = np.zeros(len(categorias))
for idx, (nombre, color) in enumerate(zip(nombres_columnas, colores)):
proporciones = matriz_datos[idx, :] / matriz_datos.sum(axis=0)
ax.bar(
range(len(categorias)),
proporciones,
bottom=base,
width=0.5,
label=nombre,
color=color,
edgecolor='gray'
)
centros = base + proporciones / 2
base += proporciones
r, g, b, _ = color
color_texto = 'white' if r * g * b < 0.5 else 'black'
for pos, (centro, valor) in enumerate(zip(centros, proporciones)):
ax.text(
pos, centro, f'{round(valor * 100, 2)}%',
ha='center', va='center',
color=color_texto, rotation=90
)
ax.legend(
ncol=len(nombres_columnas),
bbox_to_anchor=(0, 1),
loc='lower left',
fontsize='large'
)
return fig, ax
# Datos de ejemplo
categorias = ['Muy en desacuerdo', 'En desacuerdo', 'Neutral', 'De acuerdo', 'Muy de acuerdo']
respuestas = {
'Pregunta 1': [10, 15, 17, 32, 26],
'Pregunta 2': [26, 22, 29, 10, 13],
'Pregunta 3': [35, 37, 7, 2, 19],
'Pregunta 4': [32, 11, 9, 15, 33],
'Pregunta 5': [21, 29, 5, 5, 40],
'Pregunta 6': [8, 19, 5, 30, 38]
}
df_encuesta = pd.DataFrame(respuestas, index=categorias)
barras_apiladas_porcentuales(df_encuesta)
plt.show()
Variante horizontal
import numpy as np
import matplotlib.pyplot as plt
import pandas as pd
def barras_horizontales_porcentuales(df):
niveles = df.index.tolist()
diccionario = df.to_dict(orient='list')
etiquetas = list(diccionario.keys())
matriz = np.array(list(diccionario.values()))
# Normalización a porcentajes
matriz_normalizada = matriz / matriz.sum(axis=1).reshape(-1, 1)
acumulado = matriz_normalizada.cumsum(axis=1)
colores = plt.get_cmap('RdYlGn')(
np.linspace(0.15, 0.85, matriz_normalizada.shape[1])
)
fig, ax = plt.subplots(figsize=(9.2, 5))
ax.invert_yaxis()
ax.xaxis.set_visible(False)
ax.set_xlim(0, 1)
for i, (nombre, color) in enumerate(zip(niveles, colores)):
anchuras = matriz_normalizada[:, i]
inicios = acumulado[:, i] - anchuras
ax.barh(
etiquetas, anchuras, left=inicios,
height=0.5, label=nombre, color=color
)
centros = inicios + anchuras / 2
r, g, b, _ = color
color_texto = 'white' if r * g * b < 0.5 else 'darkgrey'
for pos, (centro, valor) in enumerate(zip(centros, anchuras)):
ax.text(
centro, pos, f'{round(valor * 100, 2)}%',
ha='center', va='center', color=color_texto
)
ax.legend(
ncol=len(niveles),
bbox_to_anchor=(0, 1),
loc='lower left',
fontsize='small'
)
return fig, ax
df_horizontal = pd.DataFrame(respuestas, index=categorias)
barras_horizontales_porcentuales(df_horizontal)
plt.show()
Aplicación práctica con datos de una base de datos
En escenarios reales, los datos suelen provenir de una base de datos. El siguiente ejemplo muestra cómo consultar registros de MySQL, filtrar por un rango temporal y visualizar la distribución de errores mediante gráficos de barras apiladas.
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from sqlalchemy import create_engine
import datetime
def generar_grafico_encuesta(df, transponer=False, porcentual=True):
if transponer:
df = df.T
niveles = df.index.tolist()
diccionario = df.to_dict(orient='list')
etiquetas = list(diccionario.keys())
matriz = np.array(list(diccionario.values()))
if porcentual:
matriz = matriz / matriz.sum(axis=1).reshape(-1, 1)
acumulado = matriz.cumsum(axis=1)
colores = plt.get_cmap('RdYlGn')(
np.linspace(0.15, 0.85, matriz.shape[1])
)
fig, ax = plt.subplots(figsize=(9.2, 5))
ax.invert_yaxis()
ax.xaxis.set_visible(False)
ax.set_xlim(0, 1 if porcentual else matriz.sum(axis=1).max())
for i, (nombre, color) in enumerate(zip(niveles, colores)):
anchuras = matriz[:, i]
inicios = acumulado[:, i] - anchuras
ax.barh(etiquetas, anchuras, left=inicios, height=0.5,
label=nombre, color=color)
centros = inicios + anchuras / 2
r, g, b, _ = color
color_texto = 'white' if r * g * b < 0.5 else 'darkgrey'
for pos, (centro, valor) in enumerate(zip(centros, anchuras)):
etiqueta = f'{round(valor * 100, 2)}%' if porcentual else str(int(valor))
ax.text(centro, pos, etiqueta,
ha='center', va='center', color=color_texto)
ax.legend(ncol=len(niveles), bbox_to_anchor=(0, 1),
loc='lower left', fontsize='small')
return fig, ax
# Conexión y consulta a la base de datos
motor = create_engine('mysql+pymysql://usuario:clave@host:3306/base_datos')
consulta = '''
SELECT nombre_caso, resultado, plataforma, mensaje_error, fecha_reporte
FROM detalle_reporte;
'''
df = pd.read_sql_query(consulta, motor)
# Filtrado de las últimas dos semanas
df = df.set_index(pd.to_datetime(df['fecha_reporte'])).drop('fecha_reporte', axis=1)
fecha_limite = datetime.date.today() - datetime.timedelta(days=14)
df = df.sort_index().truncate(before=fecha_limite)
# Separación por plataforma y resultado
errores_hwe = df[(df['resultado'] == 'fail') & (df['plataforma'] == 'HWE')].copy()
errores_hwv = df[(df['resultado'] == 'fail') & (df['plataforma'] == 'HWV')].copy()
# Truncado de mensajes largos
errores_hwe['mensaje_corto'] = errores_hwe['mensaje_error'].str[:60]
errores_hwv['mensaje_corto'] = errores_hwv['mensaje_error'].str[:60]
# Índice multinivel y agrupación
errores_hwe.set_index(['mensaje_corto', 'nombre_caso'], inplace=True)
errores_hwv.set_index(['mensaje_corto', 'nombre_caso'], inplace=True)
errores_hwe = errores_hwe.groupby(level=errores_hwe.index.names).count()
errores_hwv = errores_hwv.groupby(level=errores_hwv.index.names).count()
# Renombrado de columnas
errores_hwe = errores_hwe.rename(columns={'resultado': 'frecuencia'})
errores_hwv = errores_hwv.rename(columns={'resultado': 'frecuencia'})
# Preparación para visualización
matriz_hwe = errores_hwe.unstack(fill_value=0)['frecuencia']
matriz_hwe.rename(columns={
'DL Tput is below 400Mbps!': 'Error DL Tput',
'UE 5G attach failed!': 'Attach fallido',
'UL Tput is below 20Mbps!': 'Error UL Tput'
}, inplace=True)
# Generación del gráfico
generar_grafico_encuesta(matriz_hwe, transponer=True, porcentual=False)
plt.show()