SPAB — Clustering: resultado, y por qué el vector del plan no servía
Fecha: 2026-09-14 · Proyecto: ~/nef/estado_turismo/spab-retrieval
Cierra el pendiente #15 (“Clustering KMeans + jerárquico”).
Reemplaza la sección 4 de 2026-09-14-spab-plan-kmeans.md.
1. Lo que corrió
| script | qué hace | salida |
|---|---|---|
scripts/clustering.py | K-means snapshot (plan original, 4 variantes) | data/modelos/clustering.md |
scripts/clustering_estacional.py | K-means sobre perfil estacional | data/modelos/estacionalidad.md |
scripts/exportar_base.py | consolida obs/*.ndjson → base/observaciones.csv con dedup | — |
Semilla fija 20260914, n_init=20, Ward como contraste, estabilidad por
bootstrap (200 remuestreos, ARI contra el ajuste completo).
2. El snapshot NO tiene estructura — resultado negativo, documentado
El vector v1 del plan (precio, rating, reviews_conteo, menciones_caro, vistas_wikipedia) produce un clúster degenerado:
| variante | k | silueta | qué separa |
|---|---|---|---|
| v1 crudo | 2 | 0.52 | CDMX sola — silueta inflada por singleton |
| v1 log10 en conteos | 2 | 0.41 | CDMX sola otra vez |
| v1 log, sin CDMX | 2 | 0.27 | ruido |
| v2 log, sin CDMX (+ocupación) | 2 | 0.21 | ruido |
Causa. Sin CDMX, la varianza útil no existe:
| feature | CV (σ/μ) sin CDMX |
|---|---|
rating | 0.031 — todos entre 4.23 y 4.70 |
precio_hospedaje | 0.136 |
menciones_caro | 0.265 |
vistas_wikipedia | 0.378 |
reviews_conteo | 0.642 |
Con CDMX dentro, el k-means detecta tamaño de ciudad, no turismo: CDMX tiene 10× las vistas de Wikipedia y 5× las reseñas de la mediana. Las features del snapshot miden popularidad y percepción genérica, no saturación.
Se conserva como línea base negativa — es evidencia para el criterio 4 de la rúbrica (“valida, compara y justifica la selección”): se probó, se midió, se descartó con métrica.
3. El perfil estacional SÍ tiene estructura — este es el resultado
Vector = índice estacional: ocupación(mes) / ocupación media anual del
destino, 12 features. Escala-invariante a propósito: agrupa por forma de la
curva, no por nivel. Fuente DataTur (canónica), 2023-01→2026-06, mínimo 24
meses por destino.
k = 3 · silueta 0.370 · Davies-Bouldin 0.58 · estabilidad ARI 0.821 · acuerdo con Ward ARI 1.000 (los dos métodos dan la misma partición).
| clúster | destinos | perfil | amplitud |
|---|---|---|---|
| 0 — playa de invierno | cancún, playa del carmen, puerto vallarta, puerto escondido | pico feb/ene, valle sep | 0.40 |
| 1 — ciudad / cultural | cdmx, oaxaca, san miguel de allende | pico nov/oct, valle jun/ene | 0.33 |
| 2 — nacional de puente | acapulco | pico abr (Semana Santa), valle oct | 0.63 |
Acapulco queda solo (silueta 0.0) y es correcto, no artefacto: es el perfil más estacional del panel y el único que depende de vacaciones nacionales en vez de invierno del norte.
4. Qué implica para la campaña
- El valle es septiembre-octubre y es compartido por todo el clúster playa (Cancún 0.86, Playa del Carmen 0.73, Puerto Escondido 0.66). Ahí está la capacidad ociosa: redistribuir en el tiempo antes que en el espacio.
- Los clústeres 0 y 1 son parcialmente anticíclicos: el clúster ciudad pica en oct/nov, justo cuando la playa toca fondo. Sirve para el diseño de escenarios (criterio 9).
- Bacalar y Tulum siguen sin ocupación DataTur — los dos ejes del plan son los dos huecos. Es el pendiente #4 y ahora bloquea el resultado principal, no un extra.
5. Bug corregido de paso
src/lib/store.ts documentaba que leerObservaciones se quedaba “con la
captura más reciente por (destino, periodo, métrica)” — no lo hacía. Cada
re-ingesta duplicaba todo. La base tenía 3,160 filas donde hay 1,675
observaciones reales (1,485 duplicados, 47%).
Afectaba a api/burbuja, page.tsx y destino/[slug]: el índice de burbuja
se calculaba sobre duplicados. Corregido con colapso en lectura
({ historial: true } para quien quiera el histórico crudo). tsc limpio.
6. Siguiente
- Ocupación de Bacalar y Tulum — deja de ser pendiente cómodo. Proxy “Riviera Maya” para Tulum; para Bacalar, decidir entre proxy Chetumal o declararlo fuera del análisis de ocupación.
- Reponer el clustering estacional con
llegadas_turistas(serie 2012→2026, 14 años — permite detectar cambio de perfil, no sólo perfil). - SESNSP sigue faltando; ya no bloquea este resultado.