from __future__ import annotations from typing import Callable, Iterable, Optional, Tuple, Union import matplotlib.pyplot as plt import numpy as np FilterFn = Callable[[object], object] def plot_xy(df, x_col, y_col, label:Optional[str] = None, ax=None, xlim: Optional[Tuple[float, float]] = None, ylim: Optional[Tuple[float, float]] = None, marker: str = "o", linestyle: str = "-", legen_loc=None,): """ Gráfica y vs x para un DataFrame. - ax: para superponer curvas - xlim/ylim: zoom visual (No filtrado datos) """ if ax is None: _, ax = plt.subplots(figsize=(7,5)) ax.plot(df[x_col], df[y_col], marker=marker, linestyle=linestyle, label=label) ax.set_xlabel(x_col) ax.set_ylabel(y_col) ax.grid(True) if xlim is not None: ax.set_xlim(xlim) if ylim is not None: ax.set_ylim(ylim) if legen_loc is not None: ax.legend(loc=legen_loc) elif label: ax.legend() return ax def comparar_rondas(data: dict, experimento: int, x_col: str, y_col: str, rondas: Iterable[int] = (1,2), filter_fn: Optional[FilterFn] = None, xlim: Optional[Tuple[float, float]] = None, ylim: Optional[Tuple[float, float]] = None, title: Optional[str] = None, legend_loc: Optional[str] = None,): """ Compara el mismo experimento entre rondas: data[(ronda, experimento)] -> df filter_fn: funcion opcional que recibe df y devuelve df filtrado. Ejemplo: filter_fn = lambda df: apply_filter(df, row_start=10, row_end=40) """ fig, ax = plt.subplots(figsize=(7,5)) for ronda in rondas: df = data[(ronda, experimento)] if filter_fn is not None: df = filter_fn(df) plot_xy(df, x_col, y_col, label=f"Ronda {ronda}", ax=ax, xlim=xlim, ylim=ylim, legen_loc=legend_loc) if title is "False": pass else: ax.set_title(title or f"Experimento {experimento} - Comparación de Rondas") plt.show() return fig, ax def comparar_experimentos(data: dict, ronda: int, x_col: str, y_col: str, experimentos: Iterable[int] = (1,2,3,4), show: bool = False, filter_fn: Optional[FilterFn] = None, xlim: Optional[Tuple[float, float]] = None, ylim: Optional[Tuple[float, float]] = None, title: Optional[str] = None, legend_loc: Optional[str] = None, ): """ Comparar varios experimentos dentro de una misma ronda. """ fig, ax = plt.subplots(figsize=(7, 5)) for exp in experimentos: df = data[(ronda, exp)] if filter_fn is not None: df = filter_fn(df) plot_xy(df, x_col, y_col, label=f"Experimento {exp}", ax=ax, xlim=xlim, ylim=ylim, legen_loc=legend_loc) if title is "False": pass else: ax.set_title(title or f"Ronda {ronda} - Comparación de Experimentos") if show is True: plt.show() return fig, ax def plot_con_ajuste(df, x_col: str, y_col: str, label_datos: str = "Datos", label_ajuste: str = "Ajuste lineal", show: bool = True ): """ Grafica scatter + ajuste lineal (y = mx + b). Devuelve (m, b) """ x = np.asarray(df[x_col].values, dtype=float) y = np.asarray(df[y_col].values, dtype=float) m, b = np.polyfit(x, y, 1) fig, ax = plt.subplots(figsize=(7, 5)) ax.scatter(x, y, label=label_datos) ax.plot(x, m * x + b, label=f"{label_ajuste} (m={m:.4f})") ax.set_xlabel(x_col) ax.set_ylabel(y_col) ax.grid(True) ax.legend() if show: plt.show() return m, b, fig, ax def plot_3D(df, x_col: str, y_col: str, z_col: str, title: Optional[str] = None,): """ Gráfica 3D: (x, y, z) como dispersión. """ from mpl_toolkits.mplot3d import Axes3D #noqa: F401 fig = plt.figure(figsize=(8, 6)) ax = fig.add_subplot(111, projection="3d") ax.scatter(df[x_col], df[y_col], df[z_col]) ax.set_xlabel(x_col) ax.set_ylabel(y_col) ax.set_zlabel(z_col) ax.set_title(title or f"3D: {x_col} vs {y_col} vs {z_col}") plt.show() return fig, ax def plot_color_map(df, x_col: str, y_col: str, z_col: str, title: Optional[str] = None, ): """ Scatter 2D con tercera variable como color """ fig, ax = plt.subplots(figsize=(7, 5)) sc = ax.scatter(df[x_col], df[y_col], c=df[z_col], cmap="viridis") cbar = plt.colorbar(sc, ax=ax) cbar.set_label(z_col) ax.set_xlabel(x_col) ax.set_ylabel(y_col) ax.grid(True) ax.set_title(title or f"{y_col} vs {x_col} (color={z_col})") plt.show() return fig, ax def plot_dual_axis(df, x_col: str, y1_col: str, y2_col: str, ax=None, label1: Optional[str] = None, label2: Optional[str] = None, title: Optional[str] = None, show: bool = True): """ Gráfica con doble eje Y: y1 vs x (izquierda) y y2 vs x (derecha) """ if ax is None: fig, ax1 = plt.subplots(figsize=(7, 5)) else: ax1 = ax fig = ax1.figure ax2 = ax1.twinx() ax1.plot(df[x_col], df[y1_col], marker="o", linestyle="-", color="b",label= label1 or y1_col) ax2.plot(df[x_col], df[y2_col], marker="s", linestyle="--", color="r",label= label2 or y2_col) ax1.set_xlabel(x_col) ax1.set_ylabel(y1_col) ax2.set_ylabel(y2_col) ax1.grid(True) ax1.set_title(title or f"{y1_col} y {y2_col} vs {x_col}") # Leyendas Combinadas lines, labels = [], [] for a in (ax1, ax2): l, lab = a.get_legend_handles_labels() lines += l labels += lab ax1.legend(lines, labels, loc="best") if show is not True: plt.show() return fig, ax1, ax2 def marcar_zonas(ax, limites, labels=None, colors=None): """ Marca zonas en una gráfica con líneas verticales. limites: lista de voltajes donde cambian las zonas lables: nombre de cada zona colors: colores opcionales """ if colors is None: colors = ["gray", "blue", "green", "red"] for i, v in enumerate(limites): ax.axvline(v, linestyle="--", color="black", alpha=0.6) if labels and i < len(labels): ax.text( v, ax.get_ylim()[1]*0.65, labels[i], rotation=90, verticalalignment="top", horizontalalignment="right", fontsize=12 ) def sombrear_zonas(ax, limites, colores=None, alpha=0.15): """ Sombrea las zonas del experimento """ if colores is None: colores = ["gray", "blue", "green", "red"] for i in range(len(limites)-1): ax.axvspan( limites[i], limites[i+1], color=colores[i], alpha=alpha ) def percent_change(data:dict, ronda: int, x_col: str, y_col: str, experimentos: Iterable[int] = (1,2,3,4), show: bool = False, btween_exp: bool = False, filter_fn: Optional[FilterFn] = None, xlim: Optional[Tuple[float, float]] = None, ylim: Optional[Tuple[float, float]] = None, title: Optional[str] = None, ): fig, ax = plt.subplots(figsize=(7,5)) if btween_exp is False: change_value = {} sum_mean = 0 for exp in experimentos: df = data[(ronda, exp)] if filter_fn is not None: df = filter_fn(df) R = df[y_col].values x_ = df[x_col].values for i in range(len(R) - 1): change_value[i] = ((R[i+1] - R[i]) / R[i]) * 100 pct = change_value[i] sum_mean = sum_mean + abs(pct) print(f"Δ% Experiment:{exp} between data {i} and {i+1}: {pct:.2f}%") if show is True: ax.annotate(f"{pct:.1f}%", (x_[i+1], R[i+1]), textcoords="offset points", xytext=(0,8), ha='center', fontsize=8, color="red") mean = sum_mean/len(change_value) change_mx_min = ((R[(len(R)-1)] - R[0])/R[0]) * 100 print(f"Last_Value:{R[(len(R)-1)]:.3f} Fst_value:{R[0]:.3f}") print(f"The mean percent change value in Experiment {exp} are : {mean:.2f}%") print(f"The percent change between the last and first value of experiment {exp}: {change_mx_min:.2f}%") plot_xy(df, x_col, y_col, label=f"Experimento {exp}", ax=ax, xlim=xlim, ylim=ylim) sum_mean = 0 else: change_value = {} df = {} i = 0 for exp in experimentos: df[i] = data[(ronda, exp)] if filter_fn is not None: df[i] = filter_fn(df[i]) i = i + 1 R1 = df[0][y_col].values R2 = df[1][y_col].values x_1 = df[0][x_col].values x_2 = df[1][x_col].values for x in range(len(R1)): change_value[x] = ((R2[x] - R1[x]) / R1[x]) * 100 pct = change_value[x] print(f"Δ% Punto {x}: Exp{experimentos[0]} vs Exp{experimentos[1]} = {pct:.2f}%") if show is True: # Line that connect each point ax.plot([x_1[x], x_2[x]], [R1[x], R2[x]], color="black", linestyle="-", linewidth=1) # Text with the change percent in the middle of the line ax.annotate(f"{pct:.1f}%", xy=((x_1[x]+x_2[x])/2, (R1[x]+R2[x])/2), textcoords="offset points", xytext=(0,3), ha='center', fontsize=8, color="red") plot_xy(df[0], x_col, y_col, label=f"Experimento {experimentos[0]}", ax=ax, xlim=xlim, ylim=ylim) plot_xy(df[1], x_col, y_col, label=f"Experimento {experimentos[1]}", ax=ax, xlim=xlim, ylim=ylim) ax.set_title(title or f"Round {ronda} - Experiments Comparative") if show is True: plt.show() return fig, ax