Saltar al contenido
hg.Héctor GarridoCiencia de datos y economía
Volver al cuaderno
Ecommerce Analytics

Del pipeline al dashboard: Django, React y una segunda base de datos

Este artículo describe cómo conectar un pipeline de datos con una aplicación Django existente para crear un dashboard de análisis. El pipeline escribe datos de tendencias y productos en una base de datos PostgreSQL separada. Se utiliza Django con `managed=False` y un `DatabaseRouter` para acceder a estos datos sin interferir con la base de datos principal de la aplicación. El frontend, construido con React, permite explorar los datos a través de varios endpoints y filtros interactivos, ofreciendo una interfaz para el análisis de mercado.

El pipeline estaba corriendo. Cada mañana, las tablas de Neon se llenaban con tendencias, productos y atributos frescos. Pero los datos crudos en PostgreSQL no son un producto — son materia prima. Necesitaba un dashboard que me dejara explorar los resultados sin escribir SQL cada vez.

El problema: el pipeline escribe en una base de datos propia, y mi backend Django ya tiene su base de datos con posts, usuarios y el motor RAG. ¿Cómo conectar ambos mundos sin que se pisen?

Una segunda base de datos con managed=False

Django soporta múltiples bases de datos. Configuré la segunda conexión con una variable de entorno separada:

if os.getenv("ECOMMERCE_DATABASE_URL"):
    DATABASES["ecommerce"] = dj_database_url.config(
        env="ECOMMERCE_DATABASE_URL",
        conn_max_age=600
    )

Si la variable no existe, la app de ecommerce simplemente no se activa — los endpoints devuelven 503 y el resto del backend funciona normalmente. Esto fue deliberado: no quería que un error en la BD de analytics tirara abajo el blog.

Los modelos Django apuntan a las tablas que crea el pipeline en R, sin gestionarlas:

class MLProduct(models.Model):
    product_id  = models.CharField(max_length=50, primary_key=True)
    name        = models.TextField()
    domain_id   = models.CharField(max_length=100)
    price       = models.DecimalField(max_digits=12, decimal_places=2, null=True)
    category_id = models.CharField(max_length=20)
    snapshot_date = models.DateField()

    class Meta:
        managed = False
        db_table = "products"

managed = False es la clave. Django no intenta crear ni migrar estas tablas — solo las lee. Un DatabaseRouter dedicado se asegura de que las queries de estos modelos vayan a la BD correcta y de que migrate nunca las toque:

class EcommerceRouter:
    ECOMMERCE_MODELS = {"mlproduct", "mltrend", "googletrend", ...}

    def db_for_read(self, model, **hints):
        if model._meta.model_name in self.ECOMMERCE_MODELS:
            return "ecommerce"
        return None

Rediseño del pipeline: de search a catálogo

Los primeros datos que llegaron tenían un problema: la API de búsqueda devolvía items individuales (publicaciones de vendedores), no productos canónicos. Eso significaba que el mismo producto aparecía decenas de veces con distintos precios y títulos ligeramente diferentes.

Rediseñé la extracción para usar la API de catálogo. En vez de buscar items, ahora busco productos normalizados — cada uno con un ID canónico, atributos estandarizados y un precio representativo. La diferencia fue brutal: pasé de miles de registros ruidosos a cientos de productos limpios con marca, familia y categoría bien definidos.

El precio representativo viene del buy_box_winner. Cuando no existe, hay un fallback a la API de items para obtener el precio del listing principal.

Los cinco endpoints

La app Django expone cinco vistas, todas protegidas con IsEmployeeUser:

urlpatterns = [
    path("overview/",  EcommerceOverviewView.as_view()),
    path("products/",  EcommerceProductsView.as_view()),
    path("attributes/", EcommerceAttributesView.as_view()),
    path("attributes/values/", EcommerceAttributeValuesView.as_view()),
    path("fused-trends/", EcommerceFusedTrendsView.as_view()),
]

El endpoint de overview es el más denso. Agrega productos por categoría, calcula estadísticas por dominio (precio mínimo, promedio, marcas únicas) y lista las top tendencias. Todo computado en una sola query al snapshot más reciente:

latest = MLProduct.objects.aggregate(max_date=Max("snapshot_date"))["max_date"]
products = MLProduct.objects.filter(snapshot_date=latest)

Los filtros de productos soportan categoría, dominio, marca y rango de precios — lo suficiente para responder las preguntas típicas de análisis de mercado sin necesitar SQL directo.

El dashboard React

El frontend es un componente lazy-loaded con cuatro tabs, cada uno con su propia lógica de fetch y estado local:

const EcommerceDashboard = () => {
  const [tab, setTab] = useState(0);
  const [overview, setOverview] = useState(null);
  const [products, setProducts] = useState([]);
  // ...

  useEffect(() => {
    getEcommerceOverview().then(r => setOverview(r.data));
  }, []);

La tab de Overview muestra KPIs globales en cards de colores — total de productos, dominios únicos, marcas, keywords — y un accordion por categoría con el desglose de dominios. Usé recharts para las distribuciones de precio (BarChart horizontal) y MUI para los filtros y tablas.

La tab de Productos es una tabla con filtros interactivos: dropdown de categoría, dropdown de dominio (que se actualiza según la categoría seleccionada), slider de rango de precio. Es el típico patrón de filtros dependientes que en Redux sería verboso y con hooks de React queda en unas pocas líneas de estado local.

Reconexión a Neon

Neon PostgreSQL es serverless. Eso significa que las conexiones idle se cortan después de un rato. El pipeline en R, que puede tardar horas entre la extracción y la escritura, perdía la conexión justo antes de persistir los datos.

La solución fue agregar reconexión automática al NeonConnector:

..ensure_conn = function() {
  if (!DBI::dbIsValid(self$con)) {
    message("[NeonConnector] Reconectando...")
    self$con <- DBI::dbConnect(
      RPostgres::Postgres(),
      dsn = private$..dsn, sslmode = "require"
    )
  }
}

Cada método público llama a ..ensure_conn() antes de operar. Simple, efectivo, y un bug que solo aparece en producción.

El resultado

Con el dashboard funcionando, por primera vez podía ver los datos del pipeline de forma visual. Qué dominios dominan cada categoría, qué marcas tienen más presencia, cómo se distribuyen los precios. Pero faltaba algo: ¿estas tendencias del marketplace reflejan demanda real de los consumidores, o son solo lo que los vendedores deciden publicar?

Para responder eso necesitaba una segunda señal. En la próxima entrega, cruzo los datos del marketplace con volumen de búsqueda real y agrego matching semántico con proveedores.