---
name: shopify-afinidad-productos
description: "Analiza el historial de pedidos para encontrar qué productos se compran juntos con más frecuencia y ordena los pares por soporte, confianza y lift. Úsala cuando quieras armar kits, combos o recomendaciones de venta cruzada basadas en datos reales."
---

## Propósito
Aplica un análisis de canasta de compra (market basket analysis) a tu historial de pedidos para encontrar los pares de productos que los clientes compran juntos de forma natural. Para cada par comprado en conjunto calcula el **soporte** (qué tan seguido aparece el par), la **confianza** (si el cliente lleva el producto A, ¿qué tan probable es que lleve B?) y el **lift** (cuánto más probable es que al azar). El resultado sirve directamente para armar kits de productos, bloques de «comprados juntos con frecuencia», flujos de correo de venta cruzada y recomendaciones en la página de inicio. Solo lectura: no ejecuta cambios.

## Requisitos
- Sesión autenticada de Shopify CLI: `shopify store auth --store <tu-tienda>.myshopify.com --scopes read_orders,read_products`. Las consultas se ejecutan con `shopify store execute`.
- Permisos (scopes) de API: `read_orders`, `read_products` (confirmado al validar: el campo `product` de cada línea del pedido recorre los datos del producto)

## Parámetros

| Parámetro | Tipo | Obligatorio | Predeterminado | Descripción |
|-----------|------|-------------|----------------|-------------|
| store | string | sí | — | Dominio de la tienda (p. ej., mitienda.myshopify.com) |
| format | string | no | human | Formato de salida: `human` o `json` |
| dry_run | bool | no | false | Muestra una vista previa de las operaciones sin ejecutar cambios |
| date_range_start | string | sí | — | Fecha de inicio en ISO 8601 (p. ej., `2025-01-01`) |
| date_range_end | string | sí | — | Fecha de fin en ISO 8601 (p. ej., `2025-03-31`) |
| min_support | integer | no | 5 | Cantidad mínima de pedidos en los que el par debe aparecer junto para incluirlo |
| min_confidence | float | no | 0.1 | Umbral mínimo de P(B\|A) (0–1) |
| min_lift | float | no | 1.0 | Incluye solo los pares con un lift mayor que este valor (> 1 significa que no es casualidad) |
| top_n | integer | no | 20 | Cantidad de pares principales a mostrar en el ranking |
| sort_by | string | no | lift | Métrica para ordenar: `lift`, `confidence` o `support` |
| exclude_tags | string | no | — | Etiquetas de producto a excluir, separadas por comas (p. ej., `gift-wrap,donation`) |

## Pasos

1. **OPERACIÓN:** `orders` — query
   **Entradas:** `first: 250`, `query: "created_at:>='<date_range_start>' created_at:<='<date_range_end>'"`, cursor de paginación; selecciona `lineItems` con `product { id, title }` y `quantity`; omite los pedidos de una sola línea
   **Resultado esperado:** Todos los pedidos con varios artículos dentro del rango; pagina hasta `hasNextPage: false`; arma un mapa de frecuencia por producto (`product_id → order_count`) y otro por par (`(product_a_id, product_b_id) → co_occurrence_count`)

2. **Análisis en memoria:**
   - Para cada pedido con ≥ 2 productos distintos, enumera cada par único sin orden y suma 1 al contador del par
   - Calcula las métricas de cada par que cumpla `min_support`:
     - **Soporte** = `pair_count / total_orders`
     - **Confianza A→B** = `pair_count / count(orders containing A)`
     - **Confianza B→A** = `pair_count / count(orders containing B)`
     - **Lift** = `support / (P(A) × P(B))`
   - Filtra por `min_confidence` y `min_lift`; ordena por `sort_by`; recorta a `top_n`

## Operaciones GraphQL

```graphql
# orders:query (análisis de canastas con varios artículos) — validado con api_version 2025-01
query OrdersForAffinityAnalysis($first: Int!, $after: String, $query: String) {
  orders(first: $first, after: $after, query: $query) {
    edges {
      node {
        id
        createdAt
        lineItems(first: 50) {
          edges {
            node {
              quantity
              product {
                id
                title
                tags
              }
            }
          }
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
```

## Seguimiento de la sesión

**Claude DEBE mostrar lo siguiente en cada etapa. Es obligatorio.**

**Al iniciar**, muestra:
```
╔══════════════════════════════════════════════╗
║  SKILL: Afinidad entre productos             ║
║  Tienda: <dominio de la tienda>              ║
║  Inicio: <AAAA-MM-DD HH:MM UTC>              ║
╚══════════════════════════════════════════════╝
```

**Después de cada paso**, muestra:
```
[N/TOTAL] <QUERY|MUTATION>  <NombreDeOperación>
          → Parámetros: <resumen breve de las entradas clave>
          → Resultado: <conteo o resultado>
```

**Al terminar**, muestra:

Para `format: human` (predeterminado):
```
══════════════════════════════════════════════
RESUMEN DEL RESULTADO
  Pedidos analizados:        <n>
  Productos únicos:          <n>
  Pares evaluados:           <n>
  Pares sobre el umbral:     <n>
  Rango de fechas:           <inicio> a <fin>
  Ordenado por:              <lift|confidence|support>
  Errores:                   0
  Archivo:                   product_affinity_<fecha>.csv
══════════════════════════════════════════════
```

Seguido de una tabla en pantalla con el ranking de los `top_n` pares principales:

| Puesto | Producto A | Producto B | Soporte | Conf. A→B | Conf. B→A | Lift |
|--------|------------|------------|---------|-----------|-----------|------|
| 1 | ... | ... | ... | ...% | ...% | ... |

Para `format: json`, muestra:
```json
{
  "skill": "shopify-afinidad-productos",
  "store": "<dominio>",
  "started_at": "<ISO8601>",
  "completed_at": "<ISO8601>",
  "dry_run": false,
  "steps": [
    { "step": 1, "operation": "OrdersForAffinityAnalysis", "type": "query", "params_summary": "<date_range_start> a <date_range_end>", "result_summary": "<n> pedidos, <n> canastas con varios artículos", "skipped": false }
  ],
  "outcome": {
    "orders_analysed": 0,
    "unique_products": 0,
    "pairs_evaluated": 0,
    "pairs_above_threshold": 0,
    "date_range_start": "<date_range_start>",
    "date_range_end": "<date_range_end>",
    "sort_by": "lift",
    "results": [],
    "errors": 0,
    "output_file": "product_affinity_<fecha>.csv"
  }
}
```

## Formato de salida
Archivo CSV `product_affinity_<AAAA-MM-DD>.csv` con una fila por cada par que califique:

| Columna | Descripción |
|---------|-------------|
| `rank` | Puesto en el ranking |
| `product_a_id` | GID de Shopify del primer producto |
| `product_a_title` | Nombre del producto A |
| `product_b_id` | GID de Shopify del segundo producto |
| `product_b_title` | Nombre del producto B |
| `co_occurrence_count` | Cantidad de pedidos que contienen ambos productos |
| `support` | `co_occurrence_count / total_orders` |
| `confidence_a_to_b` | P(B\|A): probabilidad de que lleve B si A está en el carrito |
| `confidence_b_to_a` | P(A\|B): probabilidad de que lleve A si B está en el carrito |
| `lift` | Cuánto más probable es que aparezcan juntos frente al azar |
| `recommendation_type` | `bundle_candidate` si el lift es > 2; si no, `cross_sell` |

## Manejo de errores
| Error | Causa | Solución |
|-------|-------|----------|
| `THROTTLED` | Límite de solicitudes de la API al paginar un historial de pedidos grande | Espera 2 s y reintenta hasta 3 veces; si sigue pasando, acorta el rango de fechas |
| `product` es null en una línea del pedido | El producto se eliminó después de la compra | Saca ese artículo del análisis de pares, pero cuenta el pedido en el total |
| Ningún par supera el umbral | La tienda tiene pocos pedidos con varios artículos o los umbrales son muy estrictos | Baja `min_support` a 2 y `min_confidence` a 0.05, o amplía el rango de fechas |
| Explosión combinatoria | Tiendas con muchísimas líneas por pedido | El ciclo que enumera pares omite los pedidos con más de 20 productos distintos para limitar el crecimiento O(n²) |

## Buenas prácticas
1. Usa al menos 90 días de historial de pedidos: las ventanas cortas dan valores de lift ruidosos porque los denominadores de las probabilidades son pequeños.
2. Un **lift > 2** es una señal fuerte para armar un kit: los clientes compran esos productos juntos al menos el doble de lo que se esperaría por azar.
3. Una **confianza A→B > 30 %** sirve para un bloque confiable de «se compra con frecuencia junto con»: tres de cada diez compradores que llevan A también llevan B.
4. Excluye los accesorios y complementos (como envoltura de regalo o SKU de donación) con `exclude_tags` antes de ordenar: inflan el soporte sin ser pares de venta cruzada que signifiquen algo.
5. Usa la columna `recommendation_type` para dividir el resultado: los pares `bundle_candidate` funcionan mejor como kits armados o descuentos por volumen; los pares `cross_sell` encajan mejor en ventas adicionales dentro del carrito y en recomendaciones por correo después de la compra.
6. Vuelve a correrla cada trimestre: los productos de temporada entran y salen de la lista de pares principales, y si lo ignoras tus recomendaciones quedan desactualizadas.
