import base64 from pathlib import Path from typing import Dict import gradio as gr import numpy as np import plotly.graph_objects as go from scipy.interpolate import UnivariateSpline from scipy.optimize import curve_fit def get_color_for_accuracy(accuracy: float) -> str: """Get background color based on accuracy (0-1). Closer to 1.0 = greener.""" acc_pct = accuracy * 100 if acc_pct >= 90: green = 200 + int((acc_pct - 90) * 5.5) return f"rgb({255 - green}, {min(255, green)}, {100})" elif acc_pct >= 70: green = 150 + int((acc_pct - 70) * 2.5) return f"rgb({255 - green}, {min(255, green)}, {50})" elif acc_pct >= 50: yellow = 200 + int((acc_pct - 50) * 2.75) return f"rgb({255}, {min(255, yellow)}, {100})" elif acc_pct >= 30: red = 255 - int((acc_pct - 30) * 2.75) return f"rgb({red}, {150 + int((acc_pct - 30) * 2.75)}, {50})" else: red = 255 green = 100 + int(acc_pct * 1.67) return f"rgb({red}, {green}, {100})" def _short_label(name: str, max_len: int = 35) -> str: return name if len(name) <= max_len else name[: max_len - 1] + "\u2026" def escape_html(s: str) -> str: return str(s).replace("&", "&").replace("<", "<").replace(">", ">") # ── Weight / Overall-Score helpers ────────────────────────────────────── DEFAULT_WEIGHTS = { "Detection Rate (%)": 1.0, "FPR (%)": 1.0, "Mean Latency (ms)": 1.0, "Total Cost": 1.0, } _RANK_CONFIG = { "Detection Rate (%)": ("_accuracy", False), "FPR (%)": ("_fpr", True), "Mean Latency (ms)": ("_mean_latency", True), "Total Cost": ("_total_cost", True), } _WEIGHT_COLORS = { "Detection Rate (%)": "#22c55e", "FPR (%)": "#ef4444", "Mean Latency (ms)": "#3b82f6", "Total Cost": "#f59e0b", } _WEIGHT_SHORT_LABELS = { "Detection Rate (%)": "Detection", "FPR (%)": "FPR", "Mean Latency (ms)": "Latency", "Total Cost": "Cost", } def sort_by_overall_score(df, weights: Dict[str, float] | None = None): """Sort dataframe by weighted average rank across metrics.""" if weights is None: weights = DEFAULT_WEIGHTS rank_cols = [] for metric, (col, ascending) in _RANK_CONFIG.items(): w = weights.get(metric, 0.0) if w == 0.0: continue rank_col = f"_rank_{col}" df[rank_col] = df[col].rank(ascending=ascending, method="min") rank_cols.append((rank_col, w)) if not rank_cols: return df df["_overall_score"] = sum(df[rc] * w for rc, w in rank_cols) / sum(w for _, w in rank_cols) df = df.sort_values(by="_overall_score", ascending=True) return df def create_weight_bar_html(weights: Dict[str, float] | None = None) -> str: """Create an HTML horizontal stacked bar showing the weight distribution.""" if weights is None: weights = DEFAULT_WEIGHTS total = sum(max(w, 0) for w in weights.values()) if total == 0: return '
' segments = "" for metric, w in weights.items(): if w <= 0: continue pct = w / total * 100 color = _WEIGHT_COLORS.get(metric, "#6b7280") label = _WEIGHT_SHORT_LABELS.get(metric, metric) text = f"{label} {pct:.0f}%" if pct >= 12 else f"{pct:.0f}%" if pct >= 5 else "" segments += ( f'
' f"{text}
" ) return ( f'
{segments}
' ) def build_weights(w_det, w_fpr, w_lat, w_cost): return { "Detection Rate (%)": w_det if w_det is not None else 0.0, "FPR (%)": w_fpr if w_fpr is not None else 0.0, "Mean Latency (ms)": w_lat if w_lat is not None else 0.0, "Total Cost": w_cost if w_cost is not None else 0.0, } # ── Pareto helpers ────────────────────────────────────────────────────── def create_empty_pareto_figure(message: str = "Please select different metrics for X and Y axes"): fig = go.Figure() fig.add_annotation( text=message, x=0.5, y=0.5, showarrow=False, font={"size": 16}, xref="paper", yref="paper", ) fig.update_layout( xaxis={"visible": False}, yaxis={"visible": False}, template="plotly_white", ) return fig def build_pareto_figure(df, x_metric, y_metric, metric_map): """Build a Pareto frontier plot from a prepared leaderboard dataframe. Args: df: DataFrame with columns including Model Snapshot and internal sort columns. x_metric: Display name of x metric (key in metric_map). y_metric: Display name of y metric (key in metric_map). metric_map: Dict mapping display metric name to (column_name, minimize_bool). """ if df.empty: return create_empty_pareto_figure("No data available for the selected dataset") x_col, x_minimize = metric_map[x_metric] y_col, y_minimize = metric_map[y_metric] x = df[x_col] y = df[y_col] # Pareto detection is_pareto = [] for i in range(len(df)): dominated = False for j in range(len(df)): if i == j: continue better_x = (x.iloc[j] <= x.iloc[i]) if x_minimize else (x.iloc[j] >= x.iloc[i]) better_y = (y.iloc[j] <= y.iloc[i]) if y_minimize else (y.iloc[j] >= y.iloc[i]) strictly = (x.iloc[j] != x.iloc[i]) or (y.iloc[j] != y.iloc[i]) if better_x and better_y and strictly: dominated = True break is_pareto.append(not dominated) df["pareto"] = is_pareto fig = go.Figure() # Dominated points fig.add_trace( go.Scatter( x=df.loc[~df.pareto, x_col], y=df.loc[~df.pareto, y_col], mode="markers", name="Dominated", marker=dict(size=10, color="gray"), customdata=[ [model, xv, yv] for model, xv, yv in zip( df.loc[~df.pareto, "Model Snapshot"], df.loc[~df.pareto, x_col], df.loc[~df.pareto, y_col] ) ], hovertemplate=( "%{customdata[0]}
" f"{x_metric}:" " %{customdata[1]:.3f}
" f"{y_metric}:" " %{customdata[2]:.3f}" "" ), ) ) pareto_df = df[df.pareto].reset_index(drop=True) pareto_df_sorted = pareto_df.sort_values(by=x_col).reset_index(drop=True) # Pareto frontier curve if len(pareto_df_sorted) >= 2: try: x_pareto = np.array(pareto_df_sorted[x_col].tolist(), dtype=float) y_pareto = np.array(pareto_df_sorted[y_col].tolist(), dtype=float) x_is_bounded = "Detection Rate" in x_metric or "FPR" in x_metric y_is_bounded = "Detection Rate" in y_metric or "FPR" in y_metric # Normalize to [0, 1] to avoid overflow in power fitting x_all = np.array(df[x_col].tolist(), dtype=float) x_range = float(np.max(x_all) - np.min(x_all)) or 1.0 x_offset = float(np.min(x_all)) y_all = np.array(df[y_col].tolist(), dtype=float) y_range = float(np.max(y_all) - np.min(y_all)) or 1.0 y_offset = float(np.min(y_all)) x_pareto_norm = (x_pareto - x_offset) / x_range y_pareto_norm = (y_pareto - y_offset) / y_range if x_minimize != y_minimize: def pareto_func(xv, a, b): return a * np.power(np.clip(xv, 1e-10, None), b) else: def pareto_func(xv, a, b): return a * np.power(np.clip(1 - xv, 1e-10, None), b) try: popt, _ = curve_fit(pareto_func, x_pareto_norm, y_pareto_norm, p0=[1.0, 1.0], maxfev=5000) # Extend curve beyond data range to cover full axis if x_is_bounded: x_curve_min, x_curve_max = 0.0, 1.0 else: x_curve_min = 0.0 x_curve_max = float(np.max(x_all)) * 1.5 x_curve_norm = (np.linspace(x_curve_min, x_curve_max, 500) - x_offset) / x_range # For regions where normalization goes negative (curve extends # below the data minimum), linearly extrapolate from the edge # instead of letting the power function clip to ~0. in_range = x_curve_norm >= 0 y_curve_norm = np.empty_like(x_curve_norm) y_curve_norm[in_range] = pareto_func(x_curve_norm[in_range], *popt) if not np.all(in_range): edge = 1e-3 y0 = float(pareto_func(np.array([edge]), *popt)[0]) y1 = float(pareto_func(np.array([2e-3]), *popt)[0]) slope = (y1 - y0) / 1e-3 y_curve_norm[~in_range] = y0 + slope * (x_curve_norm[~in_range] - edge) # Denormalize back to original scale x_curve = x_curve_norm * x_range + x_offset y_curve = y_curve_norm * y_range + y_offset if y_is_bounded: valid_indices = np.where((y_curve >= 0) & (y_curve <= 1))[0] else: valid_indices = np.where((y_curve >= 0) & (y_curve <= float(np.max(y_all)) * 1.5))[0] if len(valid_indices) > 0: x_curve_valid = x_curve[valid_indices] y_curve_valid = y_curve[valid_indices] fig.add_trace( go.Scatter( x=x_curve_valid.tolist(), y=y_curve_valid.tolist(), mode="lines", name="Pareto Frontier Curve", line={"color": "red", "width": 3}, showlegend=True, hoverinfo="skip", ) ) except Exception: k = min(3, len(x_pareto_norm) - 1) spline = UnivariateSpline(x_pareto_norm, y_pareto_norm, k=k, s=0) if x_is_bounded: x_curve_min, x_curve_max = 0.0, 1.0 else: x_curve_min = 0.0 x_curve_max = float(np.max(x_all)) * 1.5 x_curve_norm = (np.linspace(x_curve_min, x_curve_max, 500) - x_offset) / x_range y_curve_norm = np.clip(spline(x_curve_norm), 0, None) x_curve = x_curve_norm * x_range + x_offset y_curve = y_curve_norm * y_range + y_offset if y_is_bounded: valid_indices = np.where((y_curve >= 0) & (y_curve <= 1))[0] else: valid_indices = np.where((y_curve >= 0) & (y_curve <= float(np.max(y_all)) * 1.5))[0] if len(valid_indices) > 0: fig.add_trace( go.Scatter( x=x_curve[valid_indices].tolist(), y=y_curve[valid_indices].tolist(), mode="lines", name="Pareto Frontier Curve", line={"color": "red", "width": 3}, showlegend=True, hoverinfo="skip", ) ) except Exception as e: print(f"Warning: Could not create Pareto frontier curve: {e}") # Pareto points with labels text_positions = ["top center", "bottom center"] text_pos = [text_positions[i % 2] for i in range(len(pareto_df))] fig.add_trace( go.Scatter( x=pareto_df[x_col], y=pareto_df[y_col], mode="markers+text", name="Pareto Frontier", marker=dict(size=14, color="red"), text=[_short_label(m) for m in pareto_df["Model Snapshot"]], textposition=text_pos, customdata=[ [model, xv, yv] for model, xv, yv in zip(pareto_df["Model Snapshot"], pareto_df[x_col], pareto_df[y_col]) ], hovertemplate=( "%{customdata[0]}
" f"{x_metric}:" " %{customdata[1]:.3f}
" f"{y_metric}:" " %{customdata[2]:.3f}" "" ), ) ) fig.update_layout( autosize=True, height=800, xaxis_title=x_metric, yaxis_title=y_metric, hovermode="closest", template="plotly_white", ) if x_minimize: fig.update_xaxes(autorange="reversed") if y_minimize: fig.update_yaxes(autorange="reversed") return fig # ── Footer ────────────────────────────────────────────────────────────── def build_footer(): logo_path = Path(__file__).parent / "cesia_logo.png" if logo_path.exists(): with open(logo_path, "rb") as logo_file: logo_data = base64.b64encode(logo_file.read()).decode() logo_src = f"data:image/png;base64,{logo_data}" else: logo_src = "" span_display = "display:inline;" if not logo_src else "display:none;" footer_html = f""" """ gr.HTML(footer_html) # ── Global CSS ────────────────────────────────────────────────────────── GLOBAL_CSS = """ """ # ── Shared CSS snippets for leaderboard & category tables ─────────────── LEADERBOARD_TABLE_CSS = """ """ LEADERBOARD_DARK_MODE_CSS = """ """ CATEGORY_TABLE_CSS = """ """