Saltar a contenido

Figuras para informes

En un pipeline de informes automatizados las figuras no solo se visualizan en Spyder, sino que se guardan como archivos PNG para luego insertarlas en el informe Word. Esto implica cuidar resolución, tamaño, fuente y nomenclatura de archivos.

Estilo consistente

Definir un estilo base al comienzo del script garantiza que todas las figuras tengan el mismo aspecto:

import matplotlib.pyplot as plt
import matplotlib as mpl

# Estilo global
mpl.rcParams.update({
    'font.family':      'sans-serif',
    'font.size':        10,
    'axes.titlesize':   11,
    'axes.labelsize':   10,
    'xtick.labelsize':  9,
    'ytick.labelsize':  9,
    'legend.fontsize':  9,
    'figure.dpi':       100,
    'axes.grid':        True,
    'grid.alpha':       0.3,
    'axes.spines.top':  False,
    'axes.spines.right':False,
})

O usar un estilo predefinido:

plt.style.use('seaborn-v0_8-whitegrid')

Guardar con calidad para informe

fig.savefig(
    ruta_figura,
    dpi=150,               # 150 dpi es suficiente para Word
    bbox_inches='tight',   # elimina márgenes en blanco
    facecolor='white'      # fondo blanco (no transparente)
)
plt.close(fig)

Nomenclatura de archivos de figura

El autoinforme busca las figuras por nombre. Es importante seguir una convención estricta:

import os

carpeta_figuras = os.path.join(ruta_proyecto, 'figuras_magnitud')
os.makedirs(carpeta_figuras, exist_ok=True)

# Nomenclatura usada en el pipeline de corrientes
fig_serie.savefig(    os.path.join(carpeta_figuras, 'serie_velocidad.png'),    dpi=150, bbox_inches='tight')
fig_rosa.savefig(     os.path.join(carpeta_figuras, 'rosa_corrientes_3m.png'), dpi=150, bbox_inches='tight')
fig_heatmap.savefig(  os.path.join(carpeta_figuras, 'heatmap_velocidad.png'),  dpi=150, bbox_inches='tight')

Figuras por profundidad

Cuando se genera una figura por cada profundidad (rosas, vectores progresivos), se ittera sobre las capas y se guarda con sufijo:

profundidades = [3, 5, 7, 9, 11, 13, 15, 17, 19, 21, 23]

for prof in profundidades:
    fig, ax = plt.subplots(subplot_kw={'projection': 'polar'}, figsize=(6, 6))

    vel_capa = df[df['profundidad'] == prof]['velocidad'].values
    dir_capa = df[df['profundidad'] == prof]['direccion'].values

    # ... graficar ...

    nombre = f'rosa_corrientes_{prof}m.png'
    fig.savefig(os.path.join(carpeta_figuras, nombre), dpi=150, bbox_inches='tight')
    plt.close(fig)
    print(f'  Figura guardada: {nombre}')

Ciclo anual mensual

Para el informe de viento se genera una figura por mes con barras de percentiles:

fig, ax = plt.subplots(figsize=(10, 5))

meses_nombres = ['Sep', 'Oct', 'Nov', 'Dic', 'Ene', 'Feb', 'Mar']
x = np.arange(len(meses_nombres))

ax.bar(x, promedios, color='steelblue', label='Promedio', zorder=3)
ax.vlines(x, p05, p95, color='navy', linewidth=2, label='P5–P95', zorder=4)
ax.scatter(x, maximos, color='firebrick', s=40, zorder=5, label='Máximo')

ax.set_xticks(x)
ax.set_xticklabels(meses_nombres)
ax.set_ylabel('Velocidad (m/s)')
ax.set_title('Ciclo anual de velocidad del viento')
ax.legend()
fig.tight_layout()
fig.savefig(os.path.join(carpeta_figuras, 'ciclo_anual.png'), dpi=150, bbox_inches='tight')
plt.close(fig)

Insertar figuras en Word

Una vez guardadas, las figuras se insertan en la plantilla Word desde el autoinforme. El proceso se describe en detalle en el capítulo de python-docx, pero el patrón básico es:

from docx import Document
from docx.shared import Cm

doc = Document('plantilla.docx')

for parrafo in doc.paragraphs:
    if '[FIGURA_ROSA]' in parrafo.text:
        parrafo.clear()
        run = parrafo.add_run()
        run.add_picture('figuras_magnitud/rosa_corrientes_3m.png', width=Cm(12))
        break

doc.save('informe_final.docx')

Figura multipanel para series de oleaje

El informe incluye una figura con tres paneles sincronizados (Hm0, Tm, Dm):

fig, axes = plt.subplots(3, 1, figsize=(14, 8), sharex=True,
                          gridspec_kw={'hspace': 0.08})

# Panel 1: Altura Hm0
axes[0].plot(df.index, df['Hm0'], color='navy', linewidth=0.7)
axes[0].set_ylabel('Hm0 (m)')
axes[0].set_ylim(0, df['Hm0'].max() * 1.1)

# Panel 2: Periodo Tm
axes[1].plot(df.index, df['Tm'], color='teal', linewidth=0.7)
axes[1].set_ylabel('Tm (s)')

# Panel 3: Dirección Dm (puntos, no línea)
axes[2].scatter(df.index, df['Dm'], s=1, color='darkorange', alpha=0.6)
axes[2].set_ylabel('Dm (°)')
axes[2].set_ylim(0, 360)
axes[2].set_yticks([0, 90, 180, 270, 360])
axes[2].set_yticklabels(['N', 'E', 'S', 'O', 'N'])
axes[2].set_xlabel('Fecha')

# Formatear eje X con fechas
import matplotlib.dates as mdates
axes[2].xaxis.set_major_formatter(mdates.DateFormatter('%b %Y'))
axes[2].xaxis.set_major_locator(mdates.MonthLocator())
plt.setp(axes[2].xaxis.get_majorticklabels(), rotation=30, ha='right')

fig.savefig('serie_oleaje.png', dpi=150, bbox_inches='tight')
plt.close(fig)

GridSpec — layouts complejos

subplots crea paneles de igual tamaño. Cuando se necesita que algunos paneles sean más anchos o altos que otros, se usa GridSpec:

import matplotlib.gridspec as gridspec

fig = plt.figure(figsize=(14, 8))
gs  = gridspec.GridSpec(2, 3, figure=fig, hspace=0.35, wspace=0.4)

# Panel grande a la izquierda (ocupa toda la columna 0)
ax_serie  = fig.add_subplot(gs[:, 0])    # filas 0:2, columna 0

# Dos paneles pequeños a la derecha
ax_hist   = fig.add_subplot(gs[0, 1:])  # fila 0, columnas 1-2
ax_rosa   = fig.add_subplot(gs[1, 1:], projection='polar')  # fila 1, columnas 1-2
# Layout con ratios de tamaño explícitos
gs = gridspec.GridSpec(
    3, 1,
    height_ratios=[3, 1, 1],   # panel superior 3× más alto que los otros dos
    hspace=0.05
)
ax_vel = fig.add_subplot(gs[0])
ax_dir = fig.add_subplot(gs[1], sharex=ax_vel)
ax_qc  = fig.add_subplot(gs[2], sharex=ax_vel)

Twin axes — dos escalas en el mismo panel

Cuando se grafican dos variables con distinta escala en el mismo panel (por ejemplo, velocidad y temperatura), se usa un eje secundario:

fig, ax1 = plt.subplots(figsize=(12, 4))

# Eje izquierdo: velocidad
ax1.plot(df.index, df['velocidad'], color='steelblue', linewidth=0.8, label='Velocidad')
ax1.set_ylabel('Velocidad (m/s)', color='steelblue')
ax1.tick_params(axis='y', labelcolor='steelblue')

# Eje derecho: temperatura — comparte el eje X
ax2 = ax1.twinx()
ax2.plot(df.index, df['temperatura'], color='firebrick', linewidth=0.8,
         linestyle='--', label='Temperatura')
ax2.set_ylabel('Temperatura (°C)', color='firebrick')
ax2.tick_params(axis='y', labelcolor='firebrick')

# Leyenda combinada de ambos ejes
lineas1, labels1 = ax1.get_legend_handles_labels()
lineas2, labels2 = ax2.get_legend_handles_labels()
ax1.legend(lineas1 + lineas2, labels1 + labels2, loc='upper left')

fig.tight_layout()

Cuándo no usar twin axes

Dos ejes con distinta escala en el mismo panel pueden inducir correlaciones visuales falsas — el lector ve las curvas cruzarse o alinearse dependiendo de cómo se elijan las escalas. Preferir paneles separados con sharex=True cuando las variables son independientes. Reservar twinx para cuando la relación entre las dos variables es el punto central de la figura.

Tamaño de figura para Word

El ancho de la zona de texto de un documento Word A4 con márgenes estándar es ~15.5 cm. Para que las figuras queden alineadas y a escala correcta al insertarlas:

# Figura de ancho completo (serie temporal, heatmap)
fig, ax = plt.subplots(figsize=(15.5/2.54, 5/2.54))   # cm → pulgadas (/2.54)

# Figura de media página (rosa, histograma)
fig, ax = plt.subplots(figsize=(7.5/2.54, 7.5/2.54))

# Guardar siempre con bbox_inches='tight' para no perder espacio en blanco
fig.savefig('figura.png', dpi=150, bbox_inches='tight', facecolor='white')
# Función utilitaria para convertir cm a pulgadas
def cm(x): return x / 2.54

fig, ax = plt.subplots(figsize=(cm(15.5), cm(6)))

Cuándo usar sharex=True

En series temporales con múltiples variables (Hm0, Tm, Dm), sharex=True sincroniza el zoom y el paneo entre paneles. Si el usuario hace zoom en un panel en Spyder, todos los demás se actualizan juntos.