def _cerrar(fig, titulo_superior=None, y_sup=1.03):
if titulo_superior:
fig.suptitle(titulo_superior, y=y_sup)
plt.tight_layout()
plt.show()
def _nube(ax, x, y, etiqueta="Observaciones", tamano=22):
ax.scatter(
x, y,
c=COLOR_DATOS, s=tamano, alpha=0.7,
edgecolors="white", linewidths=0.35, zorder=3, label=etiqueta,
)
def _verdad(ax, x, y, etiqueta="Relación verdadera"):
ax.plot(x, y, color=COLOR_VERDAD, lw=1.5, ls="--", label=etiqueta)
def _marca_knots(ax, knots):
for xi in np.asarray(knots, dtype=float).ravel():
ax.axvline(xi, color=COLOR_KNOT, ls=":", lw=1.4)
def _eje(ax, titulo, xlabel, ylabel=None, xlim=None):
ax.set_title(titulo)
ax.set_xlabel(xlabel)
if ylabel:
ax.set_ylabel(ylabel)
if xlim is not None:
ax.set_xlim(*xlim)
ax.grid(True, alpha=0.25)
def _trazo(ax, x, y, color, etiqueta=None, trozos=None):
if trozos is None:
ax.plot(x, y, color=color, lw=2.2, label=etiqueta)
return
for i, (izq, der, *_) in enumerate(trozos):
mascara = (x >= izq) & (x <= der)
ax.plot(x[mascara], y[mascara], color=color, lw=2.2, label=etiqueta if i == 0 else None)
def figura_nube(
x, y,
x_linea=None, y_linea=None, etiqueta_linea="Relación verdadera",
titulo="", xlabel="Hora del día", ylabel="Flujo (vehículos / min)", xlim=(0, 24),
):
"""Figura completa: la nube de observaciones y, si se pasan, una curva (casi siempre la verdadera)."""
fig, ax = plt.subplots(figsize=(9.5, 4.8))
_nube(ax, x, y, tamano=28)
if x_linea is not None:
_verdad(ax, x_linea, y_linea, etiqueta=etiqueta_linea)
_eje(ax, titulo, xlabel, ylabel, xlim)
ax.legend(frameon=False)
_cerrar(fig)
def figura_curvas(
x, y, x_grid, curvas,
y_verdad=None, knots=None, trozos=None, colores=None,
titulo="", xlabel="Hora del día", ylabel="Flujo (vehículos / min)", xlim=(0, 24),
loc_leyenda="best",
):
"""Figura completa: observaciones + curva verdadera + uno o varios ajustes.
curvas es un diccionario: {"Regresión lineal": prediccion_en_x_grid, ...}.
knots dibuja líneas verticales punteadas. trozos parte el trazo si las piezas pueden saltar.
"""
fig, ax = plt.subplots(figsize=(9.5, 4.8))
_nube(ax, x, y)
if y_verdad is not None:
_verdad(ax, x_grid, y_verdad)
if colores is None:
colores = [COLOR_LINEAL, COLOR_POLI, COLOR_SPLINE, COLOR_KNOT] + COLORES_BASE
for i, (nombre, pred) in enumerate(curvas.items()):
_trazo(ax, x_grid, pred, colores[i % len(colores)], etiqueta=nombre, trozos=trozos)
if knots is not None:
_marca_knots(ax, knots)
_eje(ax, titulo, xlabel, ylabel, xlim)
ax.legend(frameon=False, loc=loc_leyenda)
_cerrar(fig)
def figura_residuos(
x, residuos,
titulo="", xlabel="Hora del día", ylabel="Residuo (observado − predicción)", xlim=(0, 24),
):
"""Figura completa de residuos: puntos contra X y una línea horizontal en cero."""
fig, ax = plt.subplots(figsize=(9.5, 4.2))
_nube(ax, x, residuos, etiqueta=None, tamano=28)
ax.axhline(0, color=COLOR_VERDAD, lw=1.2)
_eje(ax, titulo, xlabel, ylabel, xlim)
_cerrar(fig)
def figura_paneles(
x, y, x_grid, paneles,
y_verdad=None, knots=None, trozos=None, colores=None,
titulo_figura=None, xlabel="Hora del día", ylabel="Flujo (vehículos / min)", xlim=(0, 24),
n_cols=None, knots_por_panel=None,
):
"""Varios paneles con la misma nube. paneles = {"Grado 3": pred, "Grado 8": pred, ...}.
En cada panel se pinta una predicción. knots_por_panel, si se pasa, es una lista de
cortes distinta por panel (por ejemplo 3, 5, 8 y 15 knots).
"""
nombres = list(paneles.keys())
preds = list(paneles.values())
n = len(nombres)
if n_cols is None:
n_cols = n if n <= 3 else 2
n_filas = int(np.ceil(n / n_cols))
fig, axes = plt.subplots(
n_filas, n_cols,
figsize=(4.2 * n_cols + 0.4, 3.7 * n_filas + 0.5),
sharey=True, sharex=True,
)
axes_lista = np.atleast_1d(axes).ravel()
if colores is None:
colores = [COLOR_POLI] * n
for i, ax in enumerate(axes_lista):
if i >= n:
ax.axis("off")
continue
_nube(ax, x, y, etiqueta=None, tamano=14)
if y_verdad is not None:
_verdad(ax, x_grid, y_verdad, etiqueta=None)
_trazo(ax, x_grid, preds[i], colores[i % len(colores)], trozos=trozos)
cortes = knots
if knots_por_panel is not None:
cortes = knots_por_panel[i]
if cortes is not None:
_marca_knots(ax, cortes)
_eje(ax, nombres[i], xlabel, ylabel if i % n_cols == 0 else None, xlim)
_cerrar(fig, titulo_superior=titulo_figura)
def figura_esquema_knots(x, y, knots, nombres_region, titulo=""):
"""Figura didáctica: una curva suave, líneas verticales en los knots y el nombre de cada región."""
fig, ax = plt.subplots(figsize=(9.8, 4.6))
ax.plot(x, y, color=COLOR_SPLINE, lw=2.6, zorder=3)
knots = np.asarray(knots, dtype=float).ravel()
for i, xi in enumerate(knots, start=1):
ax.axvline(xi, color=COLOR_KNOT, ls=":", lw=1.8)
ax.text(xi, y.max() + 3.5, rf"$\xi_{i}$", color=COLOR_KNOT, ha="center", fontsize=13, fontweight="bold")
bordes = np.concatenate([[x.min()], knots, [x.max()]])
for i, nombre in enumerate(nombres_region):
ax.text(0.5 * (bordes[i] + bordes[i + 1]), y.min() - 4.5, nombre, ha="center", va="top", fontsize=11, color=COLOR_VERDAD)
ax.set_xlabel("X")
ax.set_ylabel("f(X)")
ax.set_title(titulo)
ax.set_xlim(0, 24)
ax.set_ylim(y.min() - 10, y.max() + 10)
ax.set_yticks([])
ax.grid(True, alpha=0.25, axis="x")
_cerrar(fig)
def figura_histograma_knots(
x, grupos_knots, n_bins=24,
titulo_hist="", titulo_knots="", xlabel="Hora del día", xlim=(0, 24),
):
"""Dos paneles: arriba un histograma de X; abajo los mismos puntos y varios juegos de knots.
grupos_knots = {"Uniformes": (valores, color, estilo), "Cuantiles": (...)}.
"""
fig, axes = plt.subplots(2, 1, figsize=(9.5, 6.2), sharex=True)
axes[0].hist(x, bins=n_bins, color=COLOR_DATOS, edgecolor="white")
axes[0].set_ylabel("Cantidad de mediciones")
axes[0].set_title(titulo_hist)
axes[0].grid(True, alpha=0.25, axis="y")
axes[1].scatter(x, np.zeros_like(x), c=COLOR_DATOS, s=14, alpha=0.45, label="Observaciones (eje X)")
for nombre, (xs, color, estilo) in grupos_knots.items():
for i, xi in enumerate(np.asarray(xs, dtype=float).ravel()):
axes[1].axvline(xi, color=color, ls=estilo, lw=1.6, label=nombre if i == 0 else None)
axes[1].set_yticks([])
_eje(axes[1], titulo_knots, xlabel, xlim=xlim)
axes[1].legend(frameon=False, loc="upper left")
_cerrar(fig)
def figura_cv(valores, rmse, std, elegido=None, xlabel="n_knots", ylabel="RMSE de validación cruzada", titulo=""):
"""Figura completa: error de CV contra la complejidad, con barras de 1 desv. estándar y el valor elegido."""
fig, ax = plt.subplots(figsize=(9.5, 4.8))
ax.errorbar(
valores, rmse, yerr=std, fmt="o-",
color=COLOR_SPLINE, ecolor=COLOR_KNOT, capsize=4, lw=2.0, ms=8,
label="RMSE CV (media ± 1 desv. estándar)",
)
if elegido is not None:
idx = list(valores).index(elegido)
ax.scatter(
[elegido], [rmse[idx]], s=160, facecolors="none",
edgecolors=COLOR_LINEAL, lw=2.2, zorder=5,
label=f"Seleccionado: {xlabel} = {elegido}",
)
ax.axvline(elegido, color=COLOR_LINEAL, ls="--", lw=1.2, alpha=0.85)
ax.annotate(
"Mejor complejidad según CV",
xy=(elegido, rmse[idx]),
xytext=(elegido - 3.2, rmse[idx] + 3.5),
fontsize=10, color=COLOR_LINEAL,
arrowprops=dict(arrowstyle="->", color=COLOR_LINEAL, lw=1.2),
)
_eje(ax, titulo, xlabel, ylabel)
ax.legend(frameon=False)
_cerrar(fig)