Módulo 2: Cost Analysis & Tracking

Diseño de Cost Dashboards

Descripción de la cápsula

Un dashboard de costos que solo muestra "gasto total por día" es un gráfico que nadie mira después de la primera semana. No genera acción. No responde preguntas. No te despierta a las 3am cuando un retry loop está quemando $200/hora. Es decoración.

En la cápsula anterior construiste un CostTracker que produce datos multidimensionales: costo por endpoint, por usuario, por modelo, por tipo de operación. Esos datos son la materia prima. Ahora necesitas transformarlos en visualizaciones que generen insights accionables — el tipo de insight que te hace pausar, investigar, y tomar una decisión de optimización.

Esta cápsula te enseña a diseñar dashboards de costos que responden preguntas reales: ¿Qué tendencia tiene el gasto esta semana? ¿Qué componente domina la factura? ¿Hay algún endpoint con un spike anormal? Construirás los generadores de datos en Python que alimentan cualquier herramienta de visualización — ya sea Grafana (de tu Guía #18), un notebook Jupyter, o cualquier frontend que elijas.


Principios de diseño para cost dashboards

El problema con la mayoría de dashboards

Muchos dashboards de costos fallan por una razón simple: fueron diseñados para impresionar, no para informar. Tienen colores vibrantes, animaciones suaves, y datos que nadie usa para tomar una decisión.

Un buen cost dashboard sigue tres principios:

Principio 1: Cada visualización responde una pregunta específica
  ❌ "Gráfico bonito de costos por mes"
  ✅ "¿El gasto de esta semana está por encima o debajo del promedio?"

Principio 2: Los datos más importantes son los más visibles
  ❌ Total al fondo de una tabla de 50 filas
  ✅ Total en un KPI card grande arriba, con variación vs período anterior

Principio 3: Las anomalías deben ser evidentes sin buscar
  ❌ Requiere revisar 10 gráficos para notar un spike
  ✅ Los spikes están resaltados y generan alertas (cápsula 04)

Las cinco preguntas que tu dashboard debe responder

Todo cost dashboard útil responde estas cinco preguntas en menos de 30 segundos:

  1. ¿Cuánto estoy gastando? → KPI cards con totales y variación
  2. ¿Cómo evoluciona el gasto? → Gráfico de tendencia temporal
  3. ¿Dónde se concentra el gasto? → Distribución por componente
  4. ¿Qué está fuera de lo normal? → Outliers y anomalías
  5. ¿Hacia dónde va la tendencia? → Proyección basada en datos recientes

Si tu dashboard no responde alguna, le falta una visualización. Si tiene visualizaciones que no responden ninguna, sobran.


Generador de datos de time-series

Estructura de datos temporales para costos

El corazón de cualquier dashboard de costos son los datos de series de tiempo.

from dataclasses import dataclass, field
from datetime import datetime, timedelta
from collections import defaultdict
import statistics
import random
import math


@dataclass
class TimeSeriesCostPoint:
    """Un punto de datos de costo en una serie temporal."""
    timestamp: datetime
    cost: float
    dimensions: dict = field(default_factory=dict)


class CostTimeSeriesGenerator:
    """Genera datos de series temporales de costos para dashboards."""

    def __init__(self):
        self.data_points: list[TimeSeriesCostPoint] = []

    def add_point(self, timestamp: datetime, cost: float, **dimensions) -> None:
        self.data_points.append(
            TimeSeriesCostPoint(timestamp=timestamp, cost=cost, dimensions=dimensions)
        )

    def aggregate(
        self, interval: str = "hourly", dimension_filter: dict | None = None,
    ) -> list[dict]:
        """Agrega datos por intervalo temporal."""
        filtered = self.data_points
        if dimension_filter:
            for key, value in dimension_filter.items():
                filtered = [p for p in filtered if p.dimensions.get(key) == value]

        buckets: dict[str, list[float]] = defaultdict(list)
        for point in filtered:
            if interval == "hourly":
                bucket_key = point.timestamp.strftime("%Y-%m-%d %H:00")
            elif interval == "daily":
                bucket_key = point.timestamp.strftime("%Y-%m-%d")
            elif interval == "weekly":
                iso = point.timestamp.isocalendar()
                bucket_key = f"{iso[0]}-W{iso[1]:02d}"
            else:
                raise ValueError(f"Intervalo no soportado: {interval}")
            buckets[bucket_key].append(point.cost)

        return [
            {
                "period": period,
                "total_cost": round(sum(costs), 6),
                "avg_cost": round(statistics.mean(costs), 6),
                "request_count": len(costs),
                "min_cost": round(min(costs), 6),
                "max_cost": round(max(costs), 6),
            }
            for period, costs in sorted(buckets.items())
        ]

    def get_trend(self, interval: str = "daily", window: int = 3) -> list[dict]:
        """Calcula tendencia con moving average."""
        aggregated = self.aggregate(interval=interval)
        for i, point in enumerate(aggregated):
            start = max(0, i - window + 1)
            window_costs = [p["total_cost"] for p in aggregated[start:i + 1]]
            point["moving_avg"] = round(statistics.mean(window_costs), 6)
            if i > 0:
                prev = aggregated[i - 1]["total_cost"]
                point["change_pct"] = round((point["total_cost"] - prev) / prev * 100, 1) if prev > 0 else 0
            else:
                point["change_pct"] = 0
        return aggregated

Simulación de datos realistas

Para probar dashboards necesitas datos con patrones temporales reales: más tráfico en horario laboral, menos en la noche, variaciones por día de la semana.

def generate_realistic_data(days: int = 14) -> CostTimeSeriesGenerator:
    """Genera datos simulados con patrones realistas."""
    random.seed(42)
    ts = CostTimeSeriesGenerator()
    base_date = datetime(2026, 3, 1)

    endpoints = {
        "/chat":      {"model": "gpt-4o",      "base_input": 1200, "base_output": 350, "hourly_volume": 50},
        "/summarize": {"model": "gpt-4o",      "base_input": 3500, "base_output": 800, "hourly_volume": 10},
        "/search":    {"model": "gpt-4o-mini", "base_input": 600,  "base_output": 120, "hourly_volume": 80},
        "/classify":  {"model": "gpt-4o-mini", "base_input": 200,  "base_output": 8,   "hourly_volume": 120},
    }
    pricing = {
        "gpt-4o": {"input": 2.50, "output": 10.00},
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
    }

    for day in range(days):
        current_date = base_date + timedelta(days=day)
        day_factor = 0.6 if current_date.weekday() >= 5 else 1.0

        for hour in range(24):
            if 9 <= hour <= 18:
                hour_factor = 1.0 + 0.3 * math.sin((hour - 9) / 9 * math.pi)
            elif 6 <= hour <= 8 or 19 <= hour <= 22:
                hour_factor = 0.4
            else:
                hour_factor = 0.1

            for ep_path, config in endpoints.items():
                volume = int(config["hourly_volume"] * day_factor * hour_factor * random.uniform(0.7, 1.3))
                p = pricing[config["model"]]
                for _ in range(max(1, volume)):
                    inp = int(config["base_input"] * random.uniform(0.6, 1.4))
                    out = int(config["base_output"] * random.uniform(0.5, 1.5))
                    cost = (inp / 1e6) * p["input"] + (out / 1e6) * p["output"]
                    ts.add_point(
                        timestamp=current_date.replace(hour=hour, minute=random.randint(0, 59)),
                        cost=cost, endpoint=ep_path, model=config["model"],
                    )
    return ts


ts_data = generate_realistic_data(days=7)
total = sum(p.cost for p in ts_data.data_points)
print(f"Data points: {len(ts_data.data_points)}, Total: ${total:.2f}, Diario: ${total/7:.2f}")
# Output esperado (aproximado):
Data points: ~40000-50000, Total: $XX.XX, Diario: $X.XX

Visualización 1: KPI Cards

Las KPI cards son lo primero que se ve al abrir el dashboard. Muestran los números más importantes con variación respecto al período anterior.

def generate_kpi_cards(ts: CostTimeSeriesGenerator, days: int = 7) -> dict:
    """Genera datos para KPI cards del dashboard."""
    now = max(p.timestamp for p in ts.data_points)
    cutoff_current = now - timedelta(days=days)
    cutoff_previous = cutoff_current - timedelta(days=days)

    current = [p for p in ts.data_points if p.timestamp >= cutoff_current]
    previous = [p for p in ts.data_points if cutoff_previous <= p.timestamp < cutoff_current]

    current_total = sum(p.cost for p in current)
    previous_total = sum(p.cost for p in previous) if previous else current_total
    change_pct = ((current_total - previous_total) / previous_total * 100) if previous_total > 0 else 0

    return {
        "total_cost": {"value": round(current_total, 2), "change_pct": round(change_pct, 1),
                       "label": f"Costo Total ({days}d)"},
        "avg_cost_per_request": {"value": round(statistics.mean(p.cost for p in current), 6),
                                  "label": "Costo Promedio / Request"},
        "total_requests": {"value": len(current), "label": "Total Requests"},
        "projected_monthly": {"value": round(current_total / days * 30, 2),
                               "label": "Proyección Mensual"},
    }


kpis = generate_kpi_cards(ts_data)
for kpi in kpis.values():
    change = f" ({kpi['change_pct']:+.1f}%)" if "change_pct" in kpi else ""
    print(f"  {kpi['label']}: ${kpi['value']}{change}")

La proyección mensual es especialmente útil: extrapola el gasto actual a 30 días para comparar contra tu budget. Si tienes un presupuesto de $100/mes y la proyección dice $140, necesitas actuar ahora.


Visualización 2: Tendencia temporal con trend detection

El gráfico central del dashboard. Muestra la evolución del gasto con moving average y detección automática de tendencias.

def detect_trend_direction(trend_data: list[dict], recent_days: int = 3) -> dict:
    """Analiza la dirección de la tendencia basado en datos recientes."""
    if len(trend_data) < recent_days + 1:
        return {"direction": "insufficient_data"}

    recent = trend_data[-recent_days:]
    earlier = trend_data[-(recent_days * 2):-recent_days]

    recent_avg = statistics.mean(p["total_cost"] for p in recent)
    earlier_avg = statistics.mean(p["total_cost"] for p in earlier) if earlier else recent_avg
    change = ((recent_avg - earlier_avg) / earlier_avg * 100) if earlier_avg > 0 else 0

    if change > 15:
        direction, action = "rising_fast", "Investigar causa. Revisar hotspots por endpoint."
    elif change > 5:
        direction, action = "rising", "Monitorear. Revisar nuevos deployments o cambios de tráfico."
    elif change < -15:
        direction, action = "falling_fast", "Verificar que no sea por errores o pérdida de tráfico."
    elif change < -5:
        direction, action = "falling", "Positivo si es resultado de optimización."
    else:
        direction, action = "stable", "Sin cambios significativos. Monitoreo normal."

    return {"direction": direction, "change_pct": round(change, 1), "action": action}


trend_data = ts_data.get_trend(interval="daily", window=3)
print(f"{'Fecha':<12} {'Costo':>10} {'Avg 3d':>10} {'Cambio':>8}")
print("-" * 45)
for point in trend_data:
    print(f"{point['period']:<12} ${point['total_cost']:>8.4f} ${point['moving_avg']:>8.4f} {point['change_pct']:>+7.1f}%")

analysis = detect_trend_direction(trend_data)
print(f"\nTendencia: {analysis['direction']} ({analysis['change_pct']:+.1f}%)")
print(f"Acción: {analysis['action']}")

Visualización 3: Distribución por componente

Muestra cómo se distribuye el gasto entre endpoints o modelos. Responde: "¿qué componente domina mi factura?"

def generate_distribution_data(ts: CostTimeSeriesGenerator, dimension: str = "endpoint") -> list[dict]:
    """Genera datos de distribución por dimensión para pie/donut chart."""
    grouped: dict[str, float] = defaultdict(float)
    counts: dict[str, int] = defaultdict(int)

    for point in ts.data_points:
        key = point.dimensions.get(dimension, "unknown")
        grouped[key] += point.cost
        counts[key] += 1

    total = sum(grouped.values())
    return [
        {
            "label": key, "cost": round(cost, 4),
            "percentage": round(cost / total * 100, 1) if total > 0 else 0,
            "request_count": counts[key],
            "avg_cost_per_request": round(cost / counts[key], 6) if counts[key] > 0 else 0,
        }
        for key, cost in sorted(grouped.items(), key=lambda x: x[1], reverse=True)
    ]


print("--- Distribución por Endpoint ---")
print(f"{'Endpoint':<15} {'Costo':>10} {'%':>6} {'Requests':>10} {'Avg/req':>12}")
print("-" * 58)
for item in generate_distribution_data(ts_data, "endpoint"):
    print(f"{item['label']:<15} ${item['cost']:>8.4f} {item['percentage']:>5.1f}% "
          f"{item['request_count']:>9} ${item['avg_cost_per_request']:>10.6f}")

La columna Avg/req es clave: revela que /summarize puede tener pocos requests pero un costo por request mucho mayor que /classify. El volumen no determina el costo — la combinación de modelo + tokens sí.


Visualización 4: Top cost outliers

Los outliers son requests individuales que cuestan significativamente más que el promedio. Pueden indicar bugs, prompts excesivamente largos, o patrones que necesitan atención.

def find_cost_outliers(ts: CostTimeSeriesGenerator, method: str = "iqr", top_n: int = 10) -> dict:
    """Encuentra outliers de costo usando IQR o Z-score."""
    costs = [p.cost for p in ts.data_points]
    avg = statistics.mean(costs)

    if method == "iqr":
        sorted_costs = sorted(costs)
        q1 = sorted_costs[len(sorted_costs) // 4]
        q3 = sorted_costs[3 * len(sorted_costs) // 4]
        threshold = q3 + 1.5 * (q3 - q1)
    elif method == "zscore":
        threshold = avg + 2 * statistics.stdev(costs)
    else:
        raise ValueError(f"Método no soportado: {method}")

    outliers = sorted(
        [{"timestamp": p.timestamp.isoformat(), "cost": round(p.cost, 6),
          "dimensions": p.dimensions, "ratio_vs_avg": round(p.cost / avg, 1)}
         for p in ts.data_points if p.cost > threshold],
        key=lambda x: x["cost"], reverse=True,
    )

    return {
        "threshold": round(threshold, 6), "avg_cost": round(avg, 6),
        "outlier_count": len(outliers), "total_points": len(ts.data_points),
        "outlier_pct": round(len(outliers) / len(ts.data_points) * 100, 1),
        "top_outliers": outliers[:top_n],
    }


report = find_cost_outliers(ts_data, method="iqr", top_n=5)
print(f"Outliers: {report['outlier_count']} de {report['total_points']} ({report['outlier_pct']}%)")
print(f"Threshold: ${report['threshold']}, Promedio: ${report['avg_cost']}\n")
for i, o in enumerate(report["top_outliers"], 1):
    ep = o["dimensions"].get("endpoint", "?")
    print(f"  {i}. ${o['cost']} ({o['ratio_vs_avg']}x avg) — {ep}")

Nota que los outliers suelen concentrarse en /summarize con gpt-4o — requests con mucho input y output. Esto confirma lo que la distribución ya sugería.


Dashboard completo: composición unificada

class CostDashboardGenerator:
    """Genera todos los datos necesarios para un cost dashboard completo."""

    def __init__(self, ts: CostTimeSeriesGenerator):
        self.ts = ts

    def generate_full_dashboard(self, days: int = 7) -> dict:
        return {
            "generated_at": datetime.now().isoformat(),
            "kpis": generate_kpi_cards(self.ts, days),
            "trend": self.ts.get_trend(interval="daily", window=3),
            "distribution_endpoint": generate_distribution_data(self.ts, "endpoint"),
            "distribution_model": generate_distribution_data(self.ts, "model"),
            "outliers": find_cost_outliers(self.ts, method="iqr", top_n=5),
            "trend_analysis": detect_trend_direction(
                self.ts.get_trend(interval="daily", window=3)
            ),
        }

    def print_executive_summary(self, dashboard: dict) -> None:
        kpis = dashboard["kpis"]
        trend = dashboard["trend_analysis"]
        outliers = dashboard["outliers"]

        print("=" * 55)
        print("  COST DASHBOARD — Executive Summary")
        print("=" * 55)
        print(f"\n  💰 Costo total: ${kpis['total_cost']['value']}")
        print(f"  📊 Proyección mensual: ${kpis['projected_monthly']['value']}")
        print(f"  📈 Tendencia: {trend['direction']} ({trend['change_pct']:+.1f}%)")
        print(f"  ⚡ Requests: {kpis['total_requests']['value']:,}")
        print(f"  💵 Avg/request: ${kpis['avg_cost_per_request']['value']:.6f}")

        print(f"\n  Top endpoints:")
        for item in dashboard["distribution_endpoint"][:3]:
            print(f"    {item['label']}: ${item['cost']:.4f} ({item['percentage']}%)")

        print(f"\n  Outliers: {outliers['outlier_count']} ({outliers['outlier_pct']}%)")
        print(f"\n  Acción: {trend['action']}")
        print("=" * 55)


gen = CostDashboardGenerator(ts_data)
dashboard = gen.generate_full_dashboard()
gen.print_executive_summary(dashboard)
# Output esperado (aproximado):
=======================================================
  COST DASHBOARD — Executive Summary
=======================================================

  💰 Costo total: $XX.XX
  📊 Proyección mensual: $XXX.XX
  📈 Tendencia: stable (+X.X%)
  ⚡ Requests: XX,XXX
  💵 Avg/request: $0.000XXX

  Top endpoints:
    /chat: $XX.XXXX (XX.X%)
    /summarize: $XX.XXXX (XX.X%)
    /search: $X.XXXX (X.X%)

  Outliers: XXX (X.X%)

  Acción: Sin cambios significativos. Monitoreo normal.
=======================================================

Este es el reporte que un engineering manager quiere ver: números clave arriba, distribución en medio, acción sugerida al final.


Comparación: Overview dashboards vs Drill-down dashboards

AspectoOverview dashboardDrill-down dashboard
AudienciaEngineering managers, stakeholdersAI Engineers, SREs
Nivel de detalleKPIs, tendencias, distribuciónRequests individuales, traces
Frecuencia de revisiónDiaria/semanalCuando hay alertas o anomalías
Preguntas que responde"¿Cuánto gastamos? ¿Sube o baja?""¿Por qué subió? ¿Qué request causó el spike?"
VisualizacionesKPI cards, line charts, pie chartsTablas detalladas, histogramas
Datos necesariosAgregados (hourly, daily)Granulares (por request)

En la práctica, necesitas ambos. El overview te alerta, el drill-down te explica. Las funciones que construiste generan datos para ambos:

  • 🔹 generate_kpi_cards y get_trend → overview
  • 🔹 find_cost_outliers → drill-down
  • 🔹 generate_distribution_data → ambos

Conexión con el proyecto y stack de monitoring

Los datos que generan estas funciones están diseñados para conectarse con tu stack existente:

Si usas Prometheus + Grafana (Guía #18):
  → Exporta métricas como Prometheus counters/gauges

Si usas Jupyter/Notebooks:
  → Los datos ya están listos para matplotlib/plotly

Si usas un frontend custom:
  → generate_full_dashboard() retorna JSON serializable

El diseño intencionalmente no está atado a una herramienta. Generas los datos en Python con la estructura correcta, y los conectas a lo que ya tengas. El CostDashboardGenerator es el componente central del proyecto Cost Dashboard Integration (cápsula 08).


Troubleshooting

Problema 1: "Los datos generados no muestran variación interesante"

Causa: Datos simulados con poca variabilidad.

Solución: Aumenta los rangos de random.uniform, agrega spikes intencionales (simula un día con 3x tráfico), o inyecta anomalías. Para testing de anomaly detection, agrega requests con costos 5-10x más altos.

Problema 2: "El moving average no refleja cambios recientes"

Causa: El window es demasiado grande. Un window de 7 días suaviza tanto que los cambios de 2-3 días se diluyen.

Solución: Usa window=3 para detección temprana. Muestra dos líneas: window=3 (reactiva) y window=7 (tendencia largo plazo). Si divergen, algo cambió recientemente.

Problema 3: "Demasiados outliers, no puedo distinguir los importantes"

Causa: El método IQR puede ser sensible si la distribución mezcla endpoints baratos y caros.

Solución: Calcula outliers por dimensión, no globalmente. Un request de $0.01 es outlier en /classify pero no en /summarize. Filtra por endpoint primero.

Problema 4: "El dashboard no carga rápido con muchos data points"

Causa: Procesar millones de data points en Python para cada refresh es lento.

Solución: Pre-agrega los datos en intervalos (cada hora). El dashboard consulta agregados, no datos raw. Para drill-down, accede a datos raw solo al investigar un período específico.


Ejercicios

Ejercicio 1: Interpretar un dashboard (Fácil)

Tienes estos datos de KPI cards:

Costo total (7d): $84.50 (+18.2% vs semana anterior)
Proyección mensual: $362.14
Budget mensual: $300
Requests totales: 45,000

¿Qué acciones tomarías?

Ver solución

Análisis:

  1. Costo subió 18.2% — incremento significativo. Investigar.
  2. Proyección $362 vs budget $300 — vas a exceder por ~20%. Acción urgente.

Acciones:

  1. Revisar distribución por endpoint: ¿cuál creció más?
  2. Investigar si se deployó un nuevo feature con modelo más caro.
  3. Si el volumen subió, evaluar rate limiting o caching.
  4. Si el costo/request subió, revisar cambios de modelo o prompt.
  5. Configurar budget alert al 70% ($210) si no lo tienes.

Explicación: Los KPIs generan preguntas que llevan a drill-down. Un +18.2% con proyección sobre budget es señal clara de que algo cambió.

Ejercicio 2: Generar tendencias filtradas por endpoint (Medio)

Usa aggregate() con dimension_filter para generar tendencias separadas de /chat y /summarize. Compara el costo diario.

Ver solución
ts = generate_realistic_data(days=7)

chat_trend = ts.aggregate(interval="daily", dimension_filter={"endpoint": "/chat"})
sum_trend = ts.aggregate(interval="daily", dimension_filter={"endpoint": "/summarize"})

print(f"{'Fecha':<12} {'  /chat':>10} {'  /summarize':>12} {'  Ratio':>8}")
print("-" * 45)
for chat_d, sum_d in zip(chat_trend, sum_trend):
    ratio = sum_d["total_cost"] / chat_d["total_cost"] if chat_d["total_cost"] > 0 else 0
    print(f"{chat_d['period']:<12} ${chat_d['total_cost']:>8.4f} "
          f"${sum_d['total_cost']:>10.4f} {ratio:>7.1f}x")

Explicación: Filtrar por dimensión permite comparar endpoints. Si /summarize crece más rápido que /chat, los resúmenes se usan más o se hacen más largos. El ratio te dice dónde enfocar optimización.

Ejercicio 3: Mini-dashboard de 10 líneas (Medio)

Crea generate_mini_dashboard que imprima: (1) costo total, (2) top 3 endpoints, (3) estado de outliers. Máximo 10 líneas de output.

Ver solución
def generate_mini_dashboard(ts: CostTimeSeriesGenerator) -> str:
    total = sum(p.cost for p in ts.data_points)
    dist = generate_distribution_data(ts, "endpoint")[:3]
    outliers = find_cost_outliers(ts, method="iqr")

    lines = [
        "┌─── Mini Cost Dashboard ───┐",
        f"│ Total: ${total:.2f} ({len(ts.data_points):,} reqs)",
        "│ Top endpoints:",
    ]
    for d in dist:
        lines.append(f"│   {d['label']}: ${d['cost']:.4f} ({d['percentage']}%)")

    status = "⚠️ Investigar" if outliers["outlier_pct"] > 5 else "✅ Normal"
    lines.append(f"│ Outliers: {outliers['outlier_pct']}% — {status}")
    lines.append("└────────────────────────────┘")
    return "\n".join(lines)


print(generate_mini_dashboard(ts_data))

Explicación: Un mini-dashboard es ideal para Slack notifications, logs de CI/CD, o reportes rápidos. La información más importante en el menor espacio posible.

Ejercicio 4: Proyección con tres escenarios (Difícil)

Implementa una función que proyecte costos a 30 días con tres escenarios: optimista (crecimiento se reduce 50%), base (crecimiento actual), pesimista (crecimiento se duplica).

Ver solución
def project_costs(trend_data: list[dict], projection_days: int = 30) -> dict:
    recent = trend_data[-3:]
    daily_avg = statistics.mean(p["total_cost"] for p in recent)
    changes = [p["change_pct"] for p in recent if p["change_pct"] != 0]
    daily_growth = statistics.mean(changes) / 100 if changes else 0

    scenarios = {
        "optimista": daily_growth / 2,
        "base": daily_growth,
        "pesimista": daily_growth * 2,
    }

    results = {}
    for name, growth in scenarios.items():
        total, daily_cost = 0, daily_avg
        for _ in range(projection_days):
            total += daily_cost
            daily_cost *= (1 + growth)
        results[name] = {
            "growth_daily": f"{growth*100:+.2f}%",
            "projected_30d": round(total, 2),
            "final_daily": round(daily_cost, 4),
        }
    return results


projections = project_costs(trend_data)
print(f"{'Escenario':<12} {'Growth/día':>12} {'Total 30d':>12} {'Día 30':>10}")
print("-" * 50)
for name, data in projections.items():
    print(f"{name:<12} {data['growth_daily']:>12} ${data['projected_30d']:>10.2f} ${data['final_daily']:>8.4f}")

Explicación: Las tres proyecciones dan un rango para presupuestar. Si el escenario pesimista excede tu budget, actúa ahora. Si incluso el optimista lo excede, la acción es urgente. La diferencia entre escenarios indica qué tan sensible es tu costo a cambios de uso.


Resumen

  • ✅ Un buen cost dashboard responde 5 preguntas en 30 segundos: cuánto, cómo evoluciona, dónde se concentra, qué es anormal, hacia dónde va
  • ✅ Las KPI cards muestran totales con variación vs período anterior — es lo primero que se ve
  • ✅ La tendencia temporal con moving average revela la dirección real del gasto
  • ✅ La distribución por componente identifica qué endpoint/modelo domina la factura
  • ✅ La detección de outliers encuentra requests inusualmente costosas
  • ✅ Overview dashboards son para revisión diaria; drill-down para investigación
  • ✅ Los generadores en Python se conectan a cualquier herramienta de visualización
  • ✅ La detección automática de tendencias elimina la necesidad de mirar el dashboard constantemente

Próxima cápsula: Budget Alerts y Anomaly Detection — automatizar la detección de problemas para que el dashboard te avise en vez de esperar a que lo revises.


Recursos adicionales

  1. Grafana Dashboard Best Practices - Mejores prácticas oficiales de diseño
  2. Plotly Python - Visualizaciones interactivas en Python/Jupyter
  3. Prometheus Python Client - Exponer métricas a Prometheus
  4. Storytelling with Data (Cole Nussbaumer) - Principios de diseño de visualización
  5. OpenAI Usage Dashboard - Referencia de cost dashboard de OpenAI
  6. LiteLLM Dashboard - Dashboard open-source para cost tracking de LLMs
  7. Matplotlib - Gráficos estáticos en Python
  8. Datadog Cost Management - Referencia de dashboards de costos

Creado: Marzo 2026 / Versión: 1.0