Saltar al contenido
Todos los insights
Tutorial13 min de lectura

Recomendaciones para una tienda online: primero un baseline de co-visualizaciones, y two-tower retrieval solo cuando un test demuestre que compensa

Un plan en dos etapas para recomendaciones de producto, con el caso de negocio, código pandas funcional para el baseline, la arquitectura que viene después y cómo demostrar cualquiera de las dos en un test A/B.

Las recomendaciones de producto son uno de los pocos sistemas de ML que una tienda online puede probar de forma limpia: se muestra una versión a la mitad de los visitantes, otra a la otra mitad, y se comparan los ingresos. Eso las convierte en un buen primer proyecto de ML, y también en uno fácil de sobredimensionar. Esta nota describe el orden en que las construimos: un baseline de co-visualizaciones que se entrega en semanas, después recuperación de candidatos y ranking si los números lo justifican, con código para las partes que caben en una página.

Para el lector de negocio

Cuánto valen las recomendaciones. Salesforce informó de que durante la Cyber Week de 2025 la IA y los agentes influyeron en alrededor del 20% de los pedidos globales, unos 67 mil millones de dólares en ventas, mediante recomendaciones de producto personalizadas y atención al cliente conversacional. Lea esa cifra con cuidado: «influyeron» es atribución en la propia plataforma de comercio de Salesforce, no una mejora medida, y mezcla recomendaciones con chat. Le dice que las recomendaciones ya son estándar en las tiendas grandes. No le dice qué añadirían a la suya. Solo un test sobre su tráfico lo hace.

Dónde se ubican. Cada ubicación responde a una pregunta distinta y debe medirse por separado.

Ubicación Pregunta que responde Fuente típica
Página de producto ¿Qué otra cosa podría encajar, si no es esta? Artículos similares y co-visualizados
Carrito ¿Qué va con lo que ya se ha elegido? Artículos comprados juntos, accesorios
Inicio y páginas de categoría ¿Qué es probable que quiera hoy este comprador? Ranking personalizado
Resultados de búsqueda ¿Qué artículos relevantes es probable que compre este comprador? Ranking sobre la relevancia de búsqueda
E-mail y push ¿Qué hace volver a este comprador? Los mismos modelos, ejecutados en batch

Qué medir. Acuerde una métrica principal antes de escribir código: ingresos por sesión o pedidos por sesión en toda la web, no clics en el widget. Los clics en recomendaciones pueden subir mientras los ingresos se quedan igual, porque los compradores hacen clic en un artículo recomendado que habrían encontrado de todos modos. El click-through del widget es un diagnóstico útil, no un criterio de éxito.

Cuándo no vale la pena. Dos casos. Un catálogo pequeño, de unos cientos de productos que los compradores pueden recorrer enteros, rara vez necesita un modelo; una lista curada y una ordenación por popularidad bastan. El tráfico bajo es el segundo caso, y la aritmética no perdona. Para detectar una mejora relativa del 5% sobre una tasa de conversión del 2% (del 2,0% al 2,1%) con los parámetros habituales (5% de significación, 80% de potencia), se necesitan unas 315.000 sesiones en cada grupo. Una tienda con 100.000 sesiones al mes esperaría más de seis meses para obtener una respuesta. Una mejora del 10% necesita unas 81.000 sesiones por grupo. Si su tráfico no puede responder la pregunta, no se puede demostrar que un modelo ayude, y está pagando por una creencia.

Coste y plazos. Nuestra franja para un sistema de recomendación es de 14k–42k € en 2–4 meses, según cuánto vaya el proyecto más allá del baseline. Al final de esta nota hay un reparto ilustrativo.

Etapa 1: co-visualizaciones con alternativa por popularidad

El baseline es item-to-item: los productos que aparecen en las mismas sesiones probablemente están relacionados. La idea es antigua, y un artículo de 2003 en IEEE Internet Computing de ingenieros de Amazon describió el enfoque item-to-item a escala: calcular los artículos similares offline, para que el coste de la consulta online no crezca con el número de clientes o productos. Sigue siendo el primer paso correcto, porque no necesita nada más que el log de eventos, es fácil de explicar a un responsable de merchandising y da a cada modelo posterior un número que superar.

Entrada: una fila por visualización de producto (o añadido al carrito, o compra) con session_id, item_id y ts.

import numpy as np
import pandas as pd

def co_view_scores(events, max_items_per_session=50, min_pairs=3):
    # one row per session and item; long sessions are capped to keep the join small
    s = (events.sort_values("ts")
               .drop_duplicates(["session_id", "item_id"], keep="last")
               .groupby("session_id").tail(max_items_per_session)[["session_id", "item_id"]])
    pairs = s.merge(s, on="session_id", suffixes=("_a", "_b"))
    pairs = pairs[pairs.item_id_a != pairs.item_id_b]
    co = pairs.groupby(["item_id_a", "item_id_b"]).size().rename("co").reset_index()
    co = co[co.co >= min_pairs]                     # drop pairs seen in too few sessions
    n_item = s.groupby("item_id").size()            # sessions that viewed each item
    co["score"] = co.co / np.sqrt(co.item_id_a.map(n_item) * co.item_id_b.map(n_item))
    return co

def top_k(co, k=20):
    return (co.sort_values(["item_id_a", "score"], ascending=[True, False])
              .groupby("item_id_a").head(k))

Tres decisiones de este código importan más de lo que importará nunca el modelo.

  • Normalización. Los recuentos brutos de coocurrencia recomiendan los más vendidos junto a todo. Dividir por la raíz cuadrada del número de sesiones de ambos artículos (una puntuación coseno) premia los pares que aparecen juntos más a menudo de lo que predice su popularidad. Una puntuación lift, co * n_sessions / (n_a * n_b), empuja aún más hacia pares de nicho y necesita un recuento mínimo más estricto.
  • Soporte mínimo. Sin min_pairs, dos artículos vistos juntos una vez por una persona obtienen una puntuación perfecta. Ajuste el umbral con datos reservados, no a ojo.
  • Ventana y señal. Use una ventana móvil (60–90 días es un punto de partida habitual) para que los artículos de temporada se desvanezcan. Las co-visualizaciones encuentran alternativas para la página de producto; las compras conjuntas encuentran complementos para el carrito. Construya ambas con la misma función y distintos tipos de evento.

Para artículos con muy pocos vecinos, rellene los huecos con artículos populares de la misma categoría:

def recommend(item_id, recs, catalog, popular_by_cat, k=10):
    own = recs.loc[recs.item_id_a == item_id, "item_id_b"].tolist()[:k]
    if len(own) < k:                                # fallback: popular in the same category
        cat = catalog.set_index("item_id").at[item_id, "category"]
        own += [i for i in popular_by_cat[cat] if i != item_id and i not in own][: k - len(own)]
    return own

En producción esto se convierte en un proceso batch nocturno que escribe los 20 vecinos principales de cada artículo en un almacén clave-valor, y una API que los lee. Es suficiente para ejecutar el primer test A/B frente a lo que muestre hoy la tienda.

Etapa 2: recuperar candidatos y luego ordenarlos

Item-to-item ignora quién es el comprador. La personalización necesita un modelo que puntúe artículos para una persona, y puntuar todo un catálogo en cada visualización de página es demasiado lento. La respuesta estándar son dos etapas, descritas en el artículo sobre las recomendaciones de YouTube de Covington, Adams y Sargin (RecSys 2016): una etapa de generación de candidatos reduce un corpus de millones a cientos de artículos, y un modelo de ranking separado ordena esos cientos con un conjunto de features mucho más rico.

Generación de candidatos con un modelo two-tower. Una red genera el embedding del comprador (visualizaciones recientes, carrito, contexto como el dispositivo y la página de entrada), otra el del artículo (categoría, marca, precio, texto). El entrenamiento acerca el embedding de un comprador a los artículos con los que interactuó. En el momento de servir, los embeddings de artículos están precalculados e indexados para búsqueda aproximada de vecinos más cercanos, así que la recuperación es un cálculo de embedding y una consulta al índice.

El truco de entrenamiento habitual son los negativos dentro del batch: para cada comprador de un batch, los artículos de los demás compradores sirven como negativos. Ese truco tiene un sesgo. Los artículos populares aparecen en muchos batches, así que se usan en exceso como negativos y el modelo aprende a infravalorarlos. Yi et al. (RecSys 2019) mostraron cómo corregirlo restando de la puntuación de cada artículo el logaritmo de su frecuencia de muestreo estimada, y desplegaron el modelo corregido en YouTube sobre un corpus de decenas de millones de vídeos. En PyTorch, el núcleo es corto:

import torch
import torch.nn as nn
import torch.nn.functional as F

class Tower(nn.Module):
    def __init__(self, n_ids, dim=64):
        super().__init__()
        self.emb = nn.Embedding(n_ids, dim)
        self.mlp = nn.Sequential(nn.Linear(dim, dim), nn.ReLU(), nn.Linear(dim, dim))

    def forward(self, ids):
        return F.normalize(self.mlp(self.emb(ids)), dim=-1)

user_tower, item_tower = Tower(n_users), Tower(n_items)

def in_batch_loss(user_ids, item_ids, log_q, temperature=0.05):
    u = user_tower(user_ids)                 # [B, d]
    v = item_tower(item_ids)                 # [B, d]
    logits = u @ v.T / temperature           # other items in the batch act as negatives
    logits = logits - log_q[item_ids]        # logQ correction for popular items
    labels = torch.arange(len(user_ids))     # the diagonal holds the true pairs
    return F.cross_entropy(logits, labels)

Aquí log_q es el logaritmo de la proporción de interacciones de entrenamiento de cada artículo. Las torres reciben IDs para que el ejemplo sea pequeño; en una tienda real la torre de usuario recibe los últimos N artículos vistos y el contexto en lugar de un ID de usuario, lo que además significa que un visitante nuevo obtiene un embedding tras un solo clic.

Mantenga las listas de co-visualizaciones de la etapa 1 como segunda fuente de candidatos. La recuperación two-tower y las co-visualizaciones fallan de formas distintas, y el ranker puede usar ambas.

Ranking. El ranker ve unos cientos de candidatos por petición y puede permitirse muchas features: las puntuaciones de cada fuente de candidatos, el precio respecto a la franja de precios habitual del comprador, si el artículo comparte categoría con las visualizaciones recientes, la tendencia reciente de visualizaciones del artículo, los días desde que se añadió. Un modelo learning-to-rank con gradient boosting es un primer ranker habitual, y sus importancias de features son fáciles de inspeccionar:

import lightgbm as lgb

# one row per (request, candidate); label 1 if the candidate was clicked or bought
features = ["covis_score", "two_tower_score", "price_vs_user_median",
            "same_category_as_viewed", "item_views_7d_trend", "days_since_added"]
train = train.sort_values("request_id")
ranker = lgb.LGBMRanker(objective="lambdarank", n_estimators=300, learning_rate=0.05)
ranker.fit(train[features], train["label"],
           group=train.groupby("request_id", sort=False).size().to_numpy())

Reglas de negocio después del modelo. El stock, el margen y las políticas van después del ranking, en código simple que el negocio pueda leer y cambiar: descartar artículos que no se pueden enviar, limitar el número de artículos por marca en un bloque, excluir categorías que no deben venderse cruzadas y, si el margen importa, aplicar un impulso acotado en lugar de reentrenar el modelo con el margen. Mantener las reglas fuera del modelo significa que un responsable de merchandising puede cambiarlas un martes sin un release de ML.

Evaluación offline, y por qué no es la respuesta

Divida por tiempo, no al azar: entrene con todo lo anterior a una fecha de corte y evalúe con la semana siguiente. Una división aleatoria filtra el futuro al entrenamiento e infla todas las métricas.

Mida cada etapa con su propia métrica. Para la generación de candidatos, recall@k: de los artículos con los que el comprador interactuó a continuación, cuántos estaban en la lista de candidatos. Para el ranking, NDCG@k: si esos artículos quedaron cerca de arriba.

def recall_at_k(recommended, relevant, k=20):
    return len(set(recommended[:k]) & relevant) / len(relevant)

def ndcg_at_k(recommended, relevant, k=10):
    dcg = sum(1 / np.log2(i + 2) for i, item in enumerate(recommended[:k]) if item in relevant)
    ideal = sum(1 / np.log2(i + 2) for i in range(min(len(relevant), k)))
    return dcg / ideal

Los números offline deciden qué modelo entra en el test. No deciden si se pone en producción. Los logs los produjo el sistema anterior, así que el nuevo modelo se califica según lo que hicieron los compradores cuando se les mostraba otra cosa, y la posición en la página condiciona los clics con independencia de la relevancia. Covington et al. escribieron que los resultados de A/B en vivo no siempre se correlacionan con los experimentos offline, y las dos razones anteriores bastan para esperarlo.

El test A/B

  • Una métrica principal, la acordada al principio, como los ingresos por sesión. Decida la mejora mínima que vale la pena detectar y calcule el tamaño de muestra antes del lanzamiento, como en la aritmética de arriba.
  • Aleatorice por visitante, no por visualización de página, para que un mismo comprador no vea ambas versiones.
  • Ejecute semanas completas, al menos dos, porque los compradores de entre semana y de fin de semana se comportan distinto. No pare antes de tiempo porque la curva se vea bien el cuarto día.
  • Métricas de salvaguarda que no deben empeorar: tiempo de respuesta de la página, proporción de artículos recomendados sin stock al hacer clic, tasa de devolución de pedidos con artículos recomendados e ingresos en las categorías que el nuevo modelo muestra con menos frecuencia.
  • Mantenga el sistema anterior como control hasta que la métrica principal diga lo contrario, y conserve el interruptor para hacer rollback.

Arranque en frío

Los productos nuevos no tienen interacciones, así que ni las co-visualizaciones ni el embedding de ID de la torre de artículos los conocen. Ubíquelos usando atributos (categoría, marca, franja de precio, embeddings de texto e imagen) y reserve un pequeño número de huecos en cada bloque para artículos añadidos recientemente, de modo que reúnan las interacciones que necesitan. Los visitantes nuevos reciben candidatos basados en el contexto (la página a la que llegaron, la categoría, el dispositivo) y artículos populares, y pasan a la recuperación personalizada tras sus primeras visualizaciones en la sesión.

Cuánto cuesta, etapa por etapa

Nuestra franja para un sistema de recomendación es de 14k–42k € en 2–4 meses. La tabla siguiente es un reparto ilustrativo para un proyecto de tamaño medio que pasa por ambas etapas, presupuestado a partir de nuestra tabla de tarifas publicada. No es una oferta ni el registro de un proyecto pasado.

Etapa Roles y horas Coste
Auditoría de datos, métrica, diseño del test A/B Data 30 h a 45 €, Senior ML 20 h a 70 € 2.750 €
Etapa 1: co-visualizaciones, alternativa, API ML 60 h a 50 €, Backend 40 h a 40 € 4.600 €
Etapa 2: two-tower retrieval y ranker Senior ML 80 h a 70 €, ML 80 h a 50 € 9.600 €
Ejecución del test A/B y lectura de resultados Senior ML 15 h a 70 €, Data 20 h a 45 € 1.950 €
Pipelines, monitorización, rollback MLOps 80 h a 55 € 4.400 €
Transversal a todas las etapas QA 30 h a 30 €, PM 50 h a 40 € 2.900 €
Total 505 h 26.200 €

Un proyecto que se detiene tras la etapa 1, porque el primer test respondió la pregunta de negocio, se sitúa en el extremo inferior de la franja. Los catálogos con muchas fuentes de datos, varias ubicaciones probadas por separado o presupuestos estrictos de tiempo de respuesta empujan hacia el extremo superior. La página de sistemas de recomendación describe cómo se desarrolla un proyecto así, y la calculadora de /pricing presupuesta su propio alcance a partir de la misma tabla de tarifas.

Fuentes

¿Tiene un proceso que cuesta demasiado?

Cuéntenoslo. La primera llamada es gratuita y, si la IA no es la respuesta, se lo decimos.

Hablar de su tarea