# Selección de categoría de servicio para el Datatón ITAM 2026 — análisis del DENUE

## Overview

Documenta el proceso completo con el que se filtró el catálogo de 931 giros económicos del DENUE de la Ciudad de México para elegir la categoría de servicio del Datatón ITAM 2026. Cubre la descarga y exploración de la fuente, los seis scripts escritos, las compuertas aplicadas, los errores cometidos y las decisiones tomadas, con sus números reales.

**Estado: en curso.** Hay una lista corta de cuatro finalistas verificada y estable, pero **dos de las cuatro compuertas del criterio siguen sin resolverse** porque exigen una segunda edición del DENUE que no se pudo conseguir el mismo día.

Fecha de trabajo y de redacción: **10 de septiembre de 2026.**

### Convención de estatus

- `[V]` verificado — computado sobre los datos en esta sesión o leído en fuente primaria.
- `[I]` inferencia — deducción razonable; se indica de qué se deduce.
- `[S]` supuesto — asumido sin evidencia.
- `TODO:` — falta información, visible a propósito.

### Alcance

**Dentro:** selección de la categoría de servicio usando el DENUE. Descarga, exploración, métricas por giro, compuertas, pruebas de robustez, scripts y sus salidas.

**Fuera, explícitamente:**

- El plan general del Datatón — roles, roadmap, arquitectura de la aplicación, rúbrica. Vive en `plan-tecnico-dataton-itam-2026.md`.
- El dashboard HTML producido con estos resultados. Es un artefacto de presentación, no parte del método.
- Toda la capa de demanda demográfica (CONAPO, censos). Solo se menciona donde condiciona la elección de categoría.
- El pipeline de validación de demanda por búsqueda (Serper/DataForSEO). Se evaluó y se descartó para este propósito; el razonamiento está en Decisiones.

---

## Contexto

El reto del Datatón pide una aplicación que estime cambios en la demanda de **un servicio** por zona de la Ciudad de México. Antes de construir nada hay que fijar de qué servicio se habla.

La decisión D-04 del plan técnico establece que **la categoría se elige por disponibilidad de tres momentos históricos limpios, no por interés temático**, porque el requisito de tres momentos comparables es eliminatorio y no se resuelve con esfuerzo si la fuente no existe.

Lo que existía antes de esta sesión: la decisión D-04 escrita, ninguna categoría elegida, cero código, cero datos.

### Restricciones

1. **Tiempo.** [V] La junta de arranque del equipo era el mismo 10 de septiembre. El trabajo tenía que producir evidencia en horas, no días.
2. **Tokens del INEGI no solicitados.** [V] Las APIs de DENUE y Banco de Indicadores requieren registro previo. Ninguno estaba tramitado.
   → Consecuencia: se resolvió con **descarga masiva**, que no requiere token. [V]
3. **Ninguna herramienta de pago.** Todo con el plan gratuito o descarga abierta.
4. **Un solo operador.** André, sin apoyo del resto del equipo.

---

## Objetivo

Producir una tabla de categorías candidatas evaluadas contra criterios explícitos, con una recomendación y sus alternativas, todo respaldado por conteos hechos sobre datos descargados.

**Criterio de éxito escrito antes de empezar:** que cualquiera pueda tomar un renglón de la recomendación y rastrearlo hasta el script y el archivo que lo produjeron.

**Lo que el objetivo NO incluía, y hay que decirlo:**

- No decide si la categoría es interesante ni si tiene impacto social. Solo si es **medible**.
- No prueba que exista demanda insatisfecha. Prueba que hay suficiente materia prima para modelarla.

---

## Qué se hizo

1. Se descargó la edición vigente del DENUE de CDMX (43.3 MB comprimidos, 248 MB de CSV) sin usar la API ni tokens.
2. Se calcularon dos métricas de cobertura para los **931 giros** del catálogo, no para una lista corta.
3. Se probó `fecha_alta` como serie histórica y **se descartó**: registra la fecha del Censo Económico, no la apertura del negocio.
4. Se corrió un **test de permutación** que desmintió la métrica que parecía discriminar entre candidatas.
5. Se definió una **ventana de viabilidad** (piso, techo y contraste) y se aplicó a los 931 giros: **19 pasan**.
6. Se diagnosticó por qué gimnasios queda fuera y qué giros quedaron cerca del piso.
7. Se probó la **sensibilidad al umbral de masa** (1, 2, 3 establecimientos por AGEB): las cuatro finalistas son estables en los tres.

---

## Proceso

### Paso 1 — Descarga de la fuente sin token

**Contexto.** La ruta prevista en el plan era `INEGIpy` sobre la API del DENUE, que exige token no tramitado. Se buscó una vía que no bloqueara el trabajo del día.

**Procedimiento.**

```bash
mkdir -p ~/Code/dataton-itam-2026/data/raw
cd ~/Code/dataton-itam-2026/data/raw

curl -sI https://www.inegi.org.mx/contenidos/masiva/denue/denue_09_csv.zip | head -5
curl -L -o denue_09_cdmx.zip https://www.inegi.org.mx/contenidos/masiva/denue/denue_09_csv.zip
```

**Resultado.** [V]

```
HTTP/1.1 200 OK
Content-Length: 45439249
Content-Type: application/x-zip-compressed
Last-Modified: Wed, 20 May 2026 12:03:55 GMT
```

Descarga completa en 3 min 11 s a ~231 KB/s.

**Decisión.** Usar descarga masiva en vez de la API.
→ **Razón:** no requiere token; el token no estaba tramitado y su aprobación no está bajo control del equipo.
→ **Consecuencia:** solo se obtiene la **edición vigente**. Las ediciones históricas no están en esta ruta — ver Paso 8.

### Paso 2 — Inspección del paquete

```bash
unzip -l denue_09_cdmx.zip | head -20
unzip -q denue_09_cdmx.zip -d denue_09
find denue_09 -name "*.csv" -exec ls -lh {} \;
```

**Contenido del zip.** [V]

| Archivo | Bytes |
|---|---:|
| `diccionario_de_datos/denue_diccionario_de_datos.csv` | 10,159 |
| `conjunto_de_datos/denue_inegi_09_.csv` | 260,022,563 |
| `metadatos/metadatos_denue.txt` | 1,210 |

Fecha interna de los tres archivos: 12 de mayo de 2026.

### Paso 3 — Estructura del CSV

```bash
cd ~/Code/dataton-itam-2026/data/raw/denue_09/conjunto_de_datos
head -1 denue_inegi_09_.csv | tr ',' '\n' | nl
wc -l denue_inegi_09_.csv
```

**Resultado.** [V] 42 columnas y **462,733 líneas → 462,732 establecimientos** (una línea es encabezado). La suma de altas por año del Paso 5 da exactamente 462,732, lo que confirma el conteo.

Columnas relevantes para este trabajo:

| # | Columna | Uso |
|---:|---|---|
| 5 | `codigo_act` | Código SCIAN del giro — llave de agrupación |
| 6 | `nombre_act` | Nombre del giro |
| 30 | `cve_mun` | Clave de alcaldía |
| 34 | `ageb` | Clave de AGEB, **única solo dentro del municipio** |
| 40-41 | `latitud`, `longitud` | Georreferencia por establecimiento |
| 42 | `fecha_alta` | Formato `YYYY-MM` — ver Paso 5 |

### Paso 4 — Métricas de cobertura para los 931 giros

**Contexto.** El instinto es proponer candidatas y medirlas. Se hizo al revés: medir todo el catálogo y dejar que el filtro proponga. El razonamiento está en la decisión D-C.

**Script:** `src/actividades.py`

```python
import pandas as pd

RUTA = "data/raw/denue_09/conjunto_de_datos/denue_inegi_09_.csv"

df = pd.read_csv(
    RUTA,
    usecols=["codigo_act", "nombre_act", "cve_mun", "ageb"],
    dtype=str,
    encoding="latin-1",
)

df["ageb_id"] = df["cve_mun"] + "-" + df["ageb"]

resumen = (
    df.groupby(["codigo_act", "nombre_act"], as_index=False)
      .agg(establecimientos=("ageb_id", "size"),
           agebs_cubiertas=("ageb_id", "nunique"))
      .sort_values("establecimientos", ascending=False)
)

resumen.to_csv("data/interim/actividades_cdmx.csv", index=False)
print("agebs totales en CDMX:", df["ageb_id"].nunique())
print("actividades distintas:", len(resumen))
```

**Resultado.** [V] **2,439 AGEBs urbanas** y **931 actividades distintas**.

Los cinco giros más numerosos:

| SCIAN | Giro | Establecimientos | AGEBs con ≥1 |
|---|---|---:|---:|
| 461110 | Abarrotes, ultramarinos y misceláneas | 38,996 | 2,286 |
| 812110 | Salones y clínicas de belleza | 23,535 | 2,176 |
| 461130 | Frutas y verduras frescas | 13,165 | 1,581 |
| 465311 | Artículos de papelería | 12,213 | 2,092 |
| 463211 | Ropa, excepto de bebé y lencería | 11,654 | 1,443 |

### Paso 5 — Prueba de `fecha_alta` como serie histórica

**Contexto.** Si `fecha_alta` fuera la fecha de apertura del negocio, una sola edición contendría toda la historia y no haría falta descargar ediciones anteriores.

**Script:** `src/temporal.py`

```python
df = pd.read_csv(RUTA, usecols=["codigo_act", "fecha_alta"],
                 dtype=str, encoding="latin-1")
anio = df["fecha_alta"].str.extract(r"(\d{4})")[0]
print(anio.value_counts().sort_index().to_string())
```

**Resultado.** [V] Formato `YYYY-MM`, 28 valores distintos, **cero nulos**.

| Año | Altas | | Año | Altas |
|---|---:|---|---|---:|
| **2010** | **151,097** | | 2019 | *ver abajo* |
| 2011 | 1,620 | | **2019** | **85,152** |
| 2012 | 1,015 | | 2020 | 3,459 |
| 2013 | 416 | | 2021 | 1,433 |
| **2014** | **54,995** | | 2022 | 235 |
| 2016 | 3,232 | | 2023 | 4,641 |
| 2017 | 1,514 | | **2024** | **133,241** |
| 2018 | 581 | | 2025 | 10,474 |
| | | | 2026 | 9,627 |

No hay registros de 2015.

**Hallazgo.** Los cuatro años en negrita suman **424,485 de 462,732 = 91.7%**. [V] Corresponden a los Censos Económicos de 2009, 2014, 2019 y 2024. [I — se deduce de la periodicidad quinquenal del Censo Económico y de que las ediciones del DENUE se actualizan con sus resultados]

**Decisión.** Descartar `fecha_alta` como proxy de apertura.
→ **Razón:** la concentración en años censales muestra que registra el levantamiento del censo, no la apertura del negocio.
→ **Consecuencia:** la serie histórica exige cruzar dos ediciones del DENUE. No hay atajo.

**Segundo problema, independiente del anterior.** Los establecimientos que **cerraron** no están en el archivo: dejaron de aparecer. [V — por construcción del directorio] Cualquier medida calculada sobre una sola edición mide renovación **entre sobrevivientes**, no rotación real. Es sesgo de supervivencia y solo se corrige con dos ediciones.

### Paso 6 — Cohortes por AGEB y el test de permutación

**Contexto.** Aun sin fecha de apertura, se puede preguntar si la proporción de registros recientes varía entre zonas. Si varía mucho, hay zonas que se transforman y otras no.

**Script 1:** `src/cohortes.py` — para seis candidatas propuestas a mano, calcula por AGEB la proporción de establecimientos con `fecha_alta ≥ 2024`, restringido a AGEBs con ≥3 establecimientos del giro, y reporta su coeficiente de variación.

**Resultado.** [V]

| SCIAN | Categoría | Estab. | AGEBs ≥1 | % nuevos | AGEBs ≥3 | cv |
|---|---|---:|---:|---:|---:|---:|
| 722515 | Cafeterías y neverías | 9,860 | 1,850 | 41.5 | 1,133 | 0.58 |
| 621211 | Consultorios dentales | 7,420 | 1,811 | 30.4 | 1,075 | 0.77 |
| 464111 | Farmacias sin minisúper | 6,572 | 1,838 | 34.4 | 1,042 | 0.72 |
| 812210 | Lavanderías y tintorerías | 5,229 | 1,754 | 23.3 | 860 | 0.95 |
| 621111 | Consultorios medicina general | 2,853 | 1,141 | 30.8 | 264 | 0.93 |
| 624411 | Guarderías del sector privado | 340 | 298 | 23.8 | 4 | 2.00 |

**Observación que motivó el paso siguiente.** El `cv` está ordenado casi inversamente al número de AGEBs con masa. Con menos establecimientos por zona, una proporción varía más por azar. La métrica parecía medir estructura y podía estar midiendo tamaño de muestra.

**Script 2:** `src/permutacion.py` — baraja al azar la etiqueta `nuevo` entre los establecimientos de cada giro, 200 veces, **conservando cuántos hay en cada AGEB**, y compara el `cv` observado contra el `cv` de ese nulo.

```python
rng = np.random.default_rng(0)
codes = pd.factorize(s["ageb_id"])[0]
k = codes.max() + 1
n = np.bincount(codes, minlength=k)
y = s["nuevo"].to_numpy(dtype=float)
cv = lambda v: (np.bincount(codes, weights=v, minlength=k) / n).std() / \
               (np.bincount(codes, weights=v, minlength=k) / n).mean()
obs = cv(y)
nulo = np.array([cv(rng.permutation(y)) for _ in range(200)])
```

**Resultado.** [V]

| Categoría | AGEBs | cv observado | cv por azar | exceso | p |
|---|---:|---:|---:|---:|---:|
| Medicina general | 264 | 0.931 | 0.733 | 1.27 | 0.000 |
| Cafeterías | 1,133 | 0.579 | 0.520 | 1.11 | 0.000 |
| Dentales | 1,075 | 0.768 | 0.691 | 1.11 | 0.000 |
| Farmacias | 1,042 | 0.718 | 0.652 | 1.10 | 0.000 |
| Lavanderías | 860 | 0.946 | 0.905 | 1.05 | 0.015 |

**Interpretación.** Los excesos van de 1.05 a 1.27 — entre 5% y 27% sobre el azar. **Lavanderías, que tenía el `cv` más alto, resultó ser 95% ruido de muestreo.** La métrica no discrimina entre categorías.

**Trampa a evitar al leer esto.** Los `p` de 0.000 no significan que el efecto sea grande. Con más de mil AGEBs, cualquier desviación mínima sale significativa. **La columna que decide es `exceso`, no `p`.**

### Paso 7 — La ventana de viabilidad sobre los 931 giros

**Contexto.** El paso 6 dejó claro que la señal temporal disponible no sirve para ordenar. Quedaba la disponibilidad estructural: qué giros tienen forma utilizable para un mapa de 2,439 zonas.

**Umbrales, todos criterio propio [S]:**

| Condición | Valor | Razón |
|---|---|---|
| Piso de cobertura | ≥25% de las AGEBs con ≥3 establecimientos | Por debajo, el mapa sale mayormente vacío y el control del juez no cambia nada |
| Techo de densidad | ≤10 establecimientos por AGEB en promedio | Un giro presente en cada esquina no tiene zonas de oportunidad que señalar |
| Contraste mínimo | cv de la densidad ≥0.5 | Sin relieve en la distribución no hay nada que predecir |

**Script:** `src/ventana.py`

**Resultado.** [V] **19 de 931 giros pasan** — 98% de descarte.

| SCIAN | Giro | Estab. | AGEBs ≥3 | Cobertura | Densidad | cv |
|---|---|---:|---:|---:|---:|---:|
| 465311 | Artículos de papelería | 12,213 | 1,534 | 0.629 | 5.84 | 1.87 |
| 722514 | Tacos y tortas | 10,295 | 1,260 | 0.517 | 5.40 | 1.03 |
| 722513 | Antojitos | 9,550 | 1,203 | 0.493 | 5.04 | 1.10 |
| 722515 | Cafeterías y neverías | 9,860 | 1,133 | 0.465 | 5.33 | 1.17 |
| 621211 | Consultorios dentales | 7,420 | 1,075 | 0.441 | 4.10 | 0.97 |
| 464111 | Farmacias sin minisúper | 6,572 | 1,042 | 0.427 | 3.58 | 0.76 |
| 722518 | Otros alimentos para llevar | 7,400 | 992 | 0.407 | 4.20 | 1.24 |
| 311830 | Tortillas de maíz y nixtamal | 6,131 | 956 | 0.392 | 3.54 | 0.73 |
| 461122 | Carne de aves | 7,123 | 921 | 0.378 | 4.42 | 1.07 |
| 461160 | Dulces y repostería | 7,149 | 903 | 0.370 | 4.25 | 1.84 |
| 812210 | Lavanderías y tintorerías | 5,229 | 860 | 0.353 | 2.98 | 0.68 |
| 461130 | Frutas y verduras frescas | 13,165 | 848 | 0.348 | 8.33 | **10.99** |
| 467111 | Ferreterías y tlapalerías | 6,200 | 847 | 0.347 | 3.68 | 2.14 |
| 811111 | Reparación mecánica de autos | 5,153 | 800 | 0.328 | 3.23 | 0.77 |
| 463211 | Ropa | 11,654 | 772 | 0.317 | 8.08 | 4.85 |
| 467115 | Artículos para la limpieza | 5,125 | 766 | 0.314 | 3.41 | 0.99 |
| 722511 | Comida a la carta / corrida | 7,781 | 743 | 0.305 | 5.34 | 1.41 |
| 461121 | Carnes rojas | 6,411 | 647 | 0.265 | 4.64 | 2.36 |
| 461150 | Leche, lácteos y embutidos | 4,796 | 640 | 0.262 | 3.31 | 1.78 |

**Dos observaciones sobre esta tabla que el filtro no puede ver:**

1. **Once de los 19 son alimentos** (tacos, antojitos, cafeterías, para llevar, tortillas, carne de aves, dulces, frutas y verduras, comida corrida, carnes rojas, lácteos). No son once candidatas: son **una familia con once etiquetas**. Elegir una implica que las otras diez son variantes del mismo fenómeno urbano.
2. **Frutas y verduras tiene cv de 10.99** contra 0.68–4.85 del resto. Es un outlier de más de dos veces el siguiente. [I] Probablemente refleja la Central de Abasto: cientos de puestos en un solo AGEB. Cualquier categoría con esa forma tendría una zona dominando el modelo entero. `TODO: confirmar inspeccionando las AGEBs de mayor conteo del giro 461130.`

### Paso 8 — Diagnóstico: gimnasios y los que quedaron cerca

**Contexto.** Gimnasios estaba en la lista de categorías tratables del plan original y no apareció entre las 19. Había que saber si fallaba por ausencia real o por un umbral mal calibrado.

**Script:** `src/diagnostico.py` — recalcula las métricas para los 931 guardando **todos**, con una columna `falla` que indica cuál condición no se cumplió. Salida completa en `data/interim/catalogo_metricas.csv`.

**Resultado para gimnasios.** [V]

| SCIAN | Giro | Estab. | AGEBs ≥3 | Cobertura | Densidad | cv | Falla |
|---|---|---:|---:|---:|---:|---:|---|
| 713943 | Centros de acondicionamiento físico, privado | 2,008 | 212 | 0.087 | 1.80 | 0.68 | cobertura |
| 713944 | Centros de acondicionamiento físico, público | 267 | 7 | 0.003 | 1.17 | 0.42 | cobertura, cv |
| 465215 | Artículos y aparatos deportivos | 2,226 | 125 | 0.051 | 3.73 | 4.54 | cobertura |
| 713941 | Clubes deportivos privados | 55 | 0 | 0.000 | 1.04 | 0.19 | cobertura, cv |

**El número que importa no es la cobertura, es la densidad de 1.80.** De ahí se despeja: 2,008 ÷ 1.80 = **1,117 AGEBs con al menos un gimnasio**, el 45.8% de la ciudad. Gimnasios no está ausente — está repartido uno por zona.

**Distribución comparada.** [V, derivada del Paso 9]

| AGEBs con... | Gimnasios | Farmacias |
|---|---:|---:|
| exactamente 1 | 615 | 449 |
| exactamente 2 | 290 | 348 |
| 3 o más | 212 | 1,042 |
| **total con ≥1** | **1,117** | **1,839** |

**Giros entre 15% y 25% de cobertura** — los que se quedaron justo debajo del piso. [V]

| SCIAN | Giro | AGEBs ≥3 | Cobertura |
|---|---|---:|---:|
| 722517 | Pizzas, hamburguesas, hot dogs | 587 | 0.241 |
| 311812 | Panificación tradicional | 581 | 0.238 |
| 465912 | Regalos | 569 | 0.233 |
| 722519 | Otros alimentos consumo inmediato | 518 | 0.212 |
| 461190 | Otros alimentos | 511 | 0.210 |
| 312112 | Purificación y embotellado de agua | 448 | 0.184 |
| 465111 | Perfumería y cosméticos | 427 | 0.175 |
| 462112 | Minisupers | 418 | 0.171 |
| 813210 | Asociaciones y organizaciones religiosas | 417 | 0.171 |
| 468211 | Refacciones para automóviles | 416 | 0.171 |

### Paso 9 — Sensibilidad al umbral de masa

**Contexto.** El umbral de "3 o más establecimientos por AGEB" lo fijó el operador sin justificarlo. Si la lista corta depende de ese número, no es un resultado, es una elección de parámetro.

**Script:** `src/sensibilidad.py` — repite el filtro completo con masa ≥1, ≥2 y ≥3.

**Resultado.** [V]

| Categoría | masa ≥1 | masa ≥2 | masa ≥3 |
|---|---:|---:|---:|
| Papelerías | 0.858 ✓ | 0.736 ✓ | 0.629 ✓ |
| Cafeterías | 0.759 ✓ | 0.586 ✓ | 0.465 ✓ |
| Farmacias | 0.754 ✓ | 0.570 ✓ | 0.427 ✓ |
| Dentales | 0.743 ✓ | 0.569 ✓ | 0.441 ✓ |
| Gimnasios | 0.458 ✓ | 0.206 ✗ | 0.087 ✗ |
| Guarderías | 0.122 ✗ | 0.016 ✗ | 0.002 ✗ |
| **Giros que pasan en total** | **80** | **30** | **19** |

**Conclusión.** Las cuatro finalistas quedan sobre el piso en los tres umbrales. La lista corta **no depende del parámetro elegido**. Gimnasios entra solo con masa ≥1; guarderías no entra nunca.

---

## Herramientas implementadas

### curl

- **Propósito.** Descargar el zip del DENUE y verificar ligas antes de bajar.
- **Por qué esta.** Viene con macOS, no requiere instalación, y `-I` permite inspeccionar headers sin descargar el cuerpo.
- **Uso crítico.** `curl -sI <url> | grep -i -E "content-type|content-length"` **antes** de cualquier descarga. Ver Problema 1.
- **Limitación encontrada.** El código de estado HTTP no basta contra el servidor del INEGI.

### unzip

- **Propósito.** Inspeccionar y extraer el paquete.
- **Uso.** `unzip -l` para listar sin extraer; `unzip -q ... -d <carpeta>` para extraer en su propio directorio.

### Python 3.13 + pandas 3.0.5

- **Propósito.** Todos los cálculos por giro y por AGEB.
- **Instalación.** `python3 -m venv .venv` + `pip install pandas`. numpy entra como dependencia y se usa en el test de permutación.
- **Por qué esta y no `cut`/`sort`/`uniq` en terminal.** Las columnas `nom_estab` y `raz_social` **contienen comas dentro de comillas**. Cortar por comas desalinea los campos en un subconjunto de renglones **y no avisa**. Un conteo mal alineado se ve perfectamente creíble. [V — decidido sobre la estructura observada del archivo]
- **Parámetros obligatorios en cada lectura:**
  - `encoding="latin-1"` — el archivo no es UTF-8.
  - `dtype=str` — las claves de AGEB y municipio traen ceros a la izquierda que se pierden como enteros.
  - `usecols=[...]` — leer 4 columnas de 42 en vez de cargar 248 MB completos.

### Herramientas evaluadas y descartadas

| Herramienta | Razón del descarte |
|---|---|
| `INEGIpy` sobre la API del DENUE | Requiere token no tramitado; su aprobación no está bajo control del equipo |
| `DuckDB` | Previsto en el plan para armonizar fuentes. No hizo falta: con una sola edición y cuatro columnas, pandas alcanza |
| Serper.dev / DataForSEO | Miden demanda de **búsqueda**, no demanda de servicio por territorio. Ver decisión D-A |
| `cut` / `sort` / `uniq` en terminal | Comas dentro de campos entrecomillados producen conteos falsos en silencio |

---

## Configuración

### Entorno

| Elemento | Valor |
|---|---|
| Máquina | MacBook Pro (Apple Silicon) |
| Shell | **bash**, no zsh — el prompt es `MacBook-Pro-de-Andre:<dir> andrebutron$` [V] |
| Archivo de configuración permanente | `~/.bash_profile` (no `~/.zshrc`) |
| Raíz del proyecto | `~/Code/dataton-itam-2026` |
| Entorno virtual | `.venv/` con Python 3.13 |
| pandas | 3.0.5 |

### Estructura de directorios

```
~/Code/dataton-itam-2026/
├── .venv/
├── src/
│   ├── actividades.py      # cobertura de los 931 giros
│   ├── temporal.py         # prueba de fecha_alta
│   ├── cohortes.py         # renovación por AGEB, 6 candidatas
│   ├── permutacion.py      # test de nulidad, 200 iteraciones
│   ├── ventana.py          # filtro de viabilidad sobre los 931
│   ├── diagnostico.py      # métricas completas + gimnasios + cercanos
│   └── sensibilidad.py     # robustez ante el umbral de masa
└── data/
    ├── raw/
    │   ├── denue_09_cdmx.zip
    │   └── denue_09/
    │       ├── conjunto_de_datos/denue_inegi_09_.csv   # 248 MB
    │       ├── diccionario_de_datos/
    │       └── metadatos/
    └── interim/
        ├── actividades_cdmx.csv     # 931 giros con cobertura
        ├── candidatas.csv           # 6 candidatas con renovación
        ├── permutacion.csv          # test de nulidad
        ├── ventana.csv              # los 19 que pasan
        ├── catalogo_metricas.csv    # los 931 con columna de falla
        ├── fecha_alta.txt
        ├── diagnostico.txt
        └── sensibilidad.txt
```

### Fuente

| Parámetro | Valor |
|---|---|
| URL | `https://www.inegi.org.mx/contenidos/masiva/denue/denue_09_csv.zip` |
| Entidad | 09 = Ciudad de México |
| Edición | 05/2026, actualizada con Censos Económicos 2024 |
| `Last-Modified` | Wed, 20 May 2026 12:03:55 GMT |
| Tamaño comprimido | 45,439,249 bytes |
| Registros | 462,732 establecimientos |
| Credenciales | **Ninguna.** La descarga masiva no requiere token |

---

## Detalles técnicos

### La clave de AGEB no es única por sí sola

`ageb` es única **solo dentro de su municipio**. [V] Sin concatenar, la AGEB `0123` de Coyoacán y la `0123` de Tlalpan se cuentan como la misma zona.

```python
df["ageb_id"] = df["cve_mun"] + "-" + df["ageb"]
```

**Esta línea aparece en los seis scripts y es obligatoria.** El error no lanza excepción: produce conteos incorrectos que se ven razonables.

### Qué es una AGEB

Área Geoestadística Básica: un conjunto de manzanas —normalmente de 1 a 50— delimitado por calles, avenidas o rasgos identificables en el terreno, asignado solo dentro de localidades urbanas. [V]

Es la unidad más pequeña con la que el Censo publica población, y la misma con la que el DENUE etiqueta establecimientos. **Esa coincidencia es la razón de usarla:** cualquier otra unidad (colonia, por ejemplo) obligaría a construir a mano una correspondencia entre fuentes.

En CDMX: **2,439 AGEBs urbanas** contra 16 alcaldías, del orden de 3,700 habitantes por AGEB. [I — 9.2 millones entre 2,439]

### Definición de las tres métricas

```
cobertura  = AGEBs con ≥ masa establecimientos del giro / 2439
densidad   = establecimientos del giro / AGEBs con ≥1 del giro
cv_dens    = desviación estándar / media del conteo por AGEB
```

La `densidad` usa como denominador las AGEBs **donde el giro existe**, no las 2,439. Por eso permite despejar la cobertura con masa ≥1: `AGEBs con ≥1 = establecimientos / densidad`.

### Codificación del archivo

`latin-1`. Si truena con `UnicodeDecodeError`, la alternativa es `cp1252`. [I — no se probó, no hizo falta]

---

## Decisiones y statements

**D-A — El pipeline de validación por búsqueda (Serper/DataForSEO) no se usa para elegir categoría.**
→ **Razón:** mide demanda de búsqueda en Google, no demanda de un servicio en un territorio. Además, el volumen de búsqueda tiene piso de granularidad a nivel ciudad: no es posible obtener volumen por coordenadas, por restricción de Google Ads. [V, documentación de DataForSEO]
→ **Consecuencia:** el stack de SEO queda reservado como posible cuarta fuente del proyecto, no como criterio de selección.

**D-B — La descarga masiva sustituye a la API para el trabajo del día 0.**
→ **Razón:** no requiere token y el token no estaba tramitado.
→ **Consecuencia:** solo se obtiene la edición vigente. Las históricas requieren otra vía.

**D-C — Medir los 931 giros antes de proponer candidatas, no después.**
→ **Razón:** la primera versión del trabajo propuso seis candidatas de intuición y les midió la disponibilidad. Eso invierte D-04: elige por interés temático y verifica después. Además hace invisible cualquier categoría que no se le ocurra al operador.
→ **Consecuencia:** apareció papelerías, con la cobertura más alta de las 19, que no estaba en ninguna lista previa. Y se confirmó que gimnasios queda fuera por razón técnica, no por olvido.

**D-D — La cobertura es una compuerta, no un criterio de ordenamiento.**
→ **Razón:** más cobertura no es mejor. Abarrotes está en el 94% de la ciudad con 16 establecimientos por AGEB: si está en todas partes, no hay zona de oportunidad que señalar. La cobertura define una **ventana** con piso y techo.
→ **Consecuencia:** entre las cuatro finalistas, la diferencia de cobertura (42.7% a 62.9%) **no ordena nada**. Presentarlas como ranking induce a error.

**D-E — Descartar `fecha_alta` como fecha de apertura.**
→ **Razón:** 91.7% de los registros cae en cuatro años de Censo Económico.
→ **Consecuencia:** el requisito de tres momentos históricos exige cruzar ediciones. No se resuelve con la edición vigente.

**D-F — Toda métrica de dispersión se valida contra un nulo por permutación antes de usarse para decidir.**
→ **Razón:** el `cv` de renovación por AGEB parecía discriminar y resultó estar dominado por tamaño de muestra.
→ **Consecuencia:** se descartó lavanderías, que era la mejor candidata según esa métrica. Y quedó establecido que **`p` bajo no significa efecto grande** con n grande.

**D-G — Gimnasios se descarta por forma de la distribución, no por ausencia.**
→ **Razón:** 615 de las 1,117 AGEBs con gimnasio tienen exactamente uno. La variable "número de gimnasios" toma pocos valores distintos y no permite graduar un mapa.
→ **Consecuencia:** el mapa saldría con tres colores en vez de un degradado, y el control del juez apenas cambiaría nada.
→ **Nota de honestidad:** la explicación intuitiva —"un gimnasio sirve a un barrio y una farmacia a una cuadra"— **es [S], no se verificó.** Hay explicaciones rivales igual de plausibles: costo de instalación, estrategia de cadenas de farmacia, o clasificación SCIAN. **La conclusión no depende de cuál sea la causa;** la frase defendible es la que cita la distribución, no el radio de servicio.

**D-H — Los umbrales de la ventana son criterio propio y se declaran como tales.**
→ **Razón:** piso 25%, techo 10, contraste 0.5. Ninguno viene del reto ni de la rúbrica.
→ **Consecuencia:** requieren la prueba de sensibilidad del Paso 9 para ser defendibles. Sin esa prueba, la lista corta sería una elección de parámetro disfrazada de resultado.

**D-I — Recomendación: papelerías (465311), con farmacias (464111) como respaldo.**
→ **Razón:** ningún dato separa a las cuatro finalistas. Lo que las separa es si su demanda es **proyectable desde datos demográficos**. Papelerías → niños en edad escolar. Farmacias → adultos mayores y hogares con niños. Dentales → difuso. Cafeterías → no proyectable ("¿quién toma café?": todos). Papelerías tiene además la cobertura más alta y es un giro en declive estructural [I — digitalización escolar y caída de natalidad], y predecir declive es menos común que predecir crecimiento.
→ **Consecuencia:** es un juicio conceptual, **no un veredicto de los datos**, y debe presentarse así. Puede cambiar cuando se resuelvan G1 y G4.

---

## Problemas y troubleshooting

### Problema 1 — El servidor del INEGI nunca devuelve 404

**Qué ocurrió.** Se probaron cinco patrones de URL para ediciones históricas, incluyendo uno inventado (`/historicos/`). **Los cinco devolvieron HTTP 200.**

**Causa.** El servidor responde con una página HTML de error usando código 200, en lugar de 404.

**Cómo se detectó.**

```bash
curl -sI "$u" | grep -i -E "content-type|content-length"
```

| URL | Content-Length | Content-Type |
|---|---:|---|
| `.../2019_11/denue_09_csv.zip` | 2,263 | `text/html` |
| `.../historicos/denue_09_csv.zip` | 2,263 | `text/html` |
| `.../denue_09_csv.zip` | 45,439,249 | `application/x-zip-compressed` |

**Solución.** Validar por `content-type` y por peso, nunca por código de estado.

**Prevención — importante para el pipeline de David.** Un script que valide descargas por status code va a "bajar" las 32 entidades y guardar 32 páginas de error de 2,263 bytes sin lanzar un solo aviso. **Esta regla va en el contrato de datos.**

### Problema 2 — Las ediciones históricas no están en descarga masiva

**Qué ocurrió.** Se buscó la edición 2019 o 2020 del DENUE en el portal de descarga masiva. El filtro de año solo ofrece la edición vigente.

**Causa.** [I] La descarga masiva publica la edición vigente; las anteriores se catalogan en otro sistema.

**Solución parcial.** Las ediciones históricas están catalogadas en la **Red Nacional de Metadatos (RNM)** del INEGI, con un catálogo por edición: DENUE 2020 en `inegi.org.mx/rnm/index.php/catalog/587`, DENUE 2025 en `catalog/1103`. [V — localizados en búsqueda; **no se verificó que tengan descarga directa de microdatos**]

**Prevención.** Verificar la existencia del archivo histórico **antes** de diseñar un análisis que dependa de él. Se perdieron cerca de 40 minutos persiguiendo un archivo cuya ubicación no se había confirmado.

`TODO: confirmar en la RNM si los catálogos permiten descarga directa del CSV por entidad o requieren registro.`

### Problema 3 — Heredoc sobrescrito con el contenido equivocado

**Qué ocurrió.** Al crear `src/permutacion.py` se pegó el bloque anterior, sobrescribiendo `src/cohortes.py` con su propio contenido. El script "corrió bien" y produjo el resultado anterior; parecía que el paso nuevo había fallado en silencio.

**Causa.** Bloques largos pegados de corrido, sin verificar la primera línea.

**Solución.** Encadenar verificación y ejecución:

```bash
ls -l src/permutacion.py && python3 src/permutacion.py
```

`&&` solo ejecuta el segundo comando si el primero tuvo éxito. Si el archivo no existe, se detiene antes de correr nada.

**Prevención.** Cualquier heredoc que cree un archivo nuevo se valida con `ls -l <archivo> &&` antes de ejecutarlo.

### Problema 4 — Bloques multilínea se ejecutan de a una línea

**Qué ocurrió.** Al pegar bloques de varias líneas, la terminal ejecuta la primera y deja el resto en el buffer. Se ve en toda la sesión.

**Causa.** [I] Comportamiento del pegado en esta terminal, coherente con el problema conocido de *bracketed paste* de esta máquina.

**Impacto real.** Ninguno mientras los comandos eran independientes. Sí importa con `source` y `pip`, donde el orden es significativo.

**Prevención.** Pegar línea por línea, o desactivar el bracketed paste de forma permanente en `~/.bash_profile`.

### Problema 5 — El umbral de masa sesga contra servicios de radio amplio

**Qué ocurrió.** De los 19 giros que pasaron, **11 son alimentos** y el resto es comercio de proximidad. Ningún servicio de radio amplio sobrevivió.

**Causa.** Exigir 3 establecimientos por AGEB favorece giros de alta rotación y proximidad, y penaliza a los que se instalan uno por barrio.

**Impacto.** Los servicios de radio amplio (gimnasios, guarderías, clínicas) son justamente los que tienen población objetivo proyectable desde datos demográficos, que es la compuerta con la que se decidió la recomendación.

**Solución aplicada.** Prueba de sensibilidad del Paso 9. Confirmó que las cuatro finalistas son estables, pero **no elimina el sesgo**: solo demuestra que no cambia el resultado entre las candidatas evaluadas.

`TODO: si el equipo quiere evaluar servicios de radio amplio en serio, hace falta cambiar la unidad de agregación —vecindad de AGEBs, o distancia a la instalación más cercana— y no solo bajar el umbral.`

### Problema 6 — Presentar la cobertura como ranking

**Qué ocurrió.** La primera versión del dashboard mostró la cobertura como barras ordenadas de mayor a menor, sin umbral visible. Eso comunica "la de arriba gana", que es lo contrario de lo que el análisis dice.

**Causa.** Usar un componente de ranking para representar una compuerta.

**Solución.** Dibujar el piso como línea de referencia y mostrar las tres variantes del umbral en la misma gráfica.

**Prevención.** Antes de elegir un componente visual, decidir si la variable **ordena** o **admite**. No son lo mismo.

---

## Dudas comunes

**¿Por qué no se usó la API del DENUE si estaba en el plan?**
Porque requiere un token que se obtiene por registro previo y no estaba tramitado. La descarga masiva entrega el mismo dato sin credenciales. La API sigue siendo útil para consultas puntuales o actualizaciones, no para bajar el directorio completo una vez.

**¿Más establecimientos significa mejor categoría?**
No. Abarrotes tiene 38,996 establecimientos en el 94% de la ciudad y es una **mala** elección: si está en todas partes, no hay zonas de oportunidad. La cobertura define una ventana con piso y techo, no una escalera.

**¿Por qué no basta con exigir que la categoría exista en muchas zonas?**
Porque importa **cuántos hay en cada zona**, no solo si hay. Gimnasios está en 1,117 AGEBs pero en 615 de ellas hay exactamente uno. Una variable que toma los valores 0, 1 y 2 no permite graduar un mapa.

**El p-valor salió 0.000, ¿no significa que el efecto es fuerte?**
No. Con más de mil AGEBs, cualquier desviación mínima respecto del azar sale significativa. Lo que mide magnitud es el **exceso** sobre el nulo, y ninguna categoría pasó de 1.27.

**¿Por qué exigir 3 establecimientos por AGEB y no 2?**
Es criterio propio. Por eso se probó con 1, 2 y 3: las cuatro finalistas quedan sobre el piso en los tres casos. La respuesta defendible no es "porque 3 es lo correcto" sino "porque lo probamos con los tres valores y el resultado no cambia".

**¿Por qué no se puede medir cierres con este archivo?**
Porque los negocios cerrados dejaron de aparecer en el directorio. Solo se ven los que siguen vivos. Es sesgo de supervivencia y únicamente se corrige comparando dos ediciones: lo que estaba en la edición vieja y no está en la nueva, cerró.

**Si `fecha_alta` está en el archivo, ¿por qué no sirve?**
Porque 91.7% de los registros tiene una de cuatro fechas, que coinciden con los años de Censo Económico. Registra cuándo pasó el encuestador, no cuándo abrió el negocio.

**¿Por qué AGEB y no colonia, si la gente piensa en colonias?**
Porque la colonia no es unidad estadística del INEGI. El Censo y el DENUE usan la misma clave de AGEB, así que se cruzan sin construir una correspondencia a mano. La legibilidad se resuelve en la capa de presentación del mapa, no cambiando la unidad de análisis.

---

## Aprendizajes

**Medir todo el catálogo cuesta lo mismo que medir seis candidatas.** El `groupby` sobre 462,732 registros tarda segundos. Proponer una lista corta a mano no ahorra trabajo y sí introduce el sesgo del operador. Aparecieron categorías que nadie del equipo habría propuesto.

**Una métrica de dispersión sin nulo de referencia no significa nada.** El coeficiente de variación entre zonas parecía medir estructura y estaba midiendo tamaño de muestra. Doscientas permutaciones tardan segundos y desmintieron el ranking completo.

**Toda columna de fecha en un registro administrativo es sospechosa hasta que se grafica su distribución.** Una sola gráfica de frecuencias reveló que `fecha_alta` era fecha de censo. Sin ese gráfico, la serie histórica del proyecto habría estado construida sobre un artefacto.

**Un umbral inventado necesita su prueba de sensibilidad en el mismo entregable.** No como defensa retórica, sino porque es la única forma de saber si el resultado es del dato o del parámetro.

**Un status code HTTP no es evidencia de que un archivo exista.** Validar por `content-type` y peso, siempre.

**Separar el número de su explicación.** El patrón de gimnasios es dato; "un gimnasio sirve a un barrio" es una historia que se inventó para explicarlo y no se verificó. En una presentación, el número aguanta el interrogatorio y la explicación no necesariamente. Marcar la explicación `[I]` o `[S]` y no presentarla como parte del hallazgo.

**Confirmar que un archivo existe antes de diseñar un análisis que dependa de él.** Cuarenta minutos perdidos persiguiendo ediciones históricas del DENUE en el portal equivocado.

---

## Estado actual

Al 10 de septiembre de 2026, 15:00 aproximadamente.

**Funcionando:**

- [V] DENUE 05/2026 de CDMX descargado, extraído y verificado en `~/Code/dataton-itam-2026/data/raw/`.
- [V] Entorno Python operativo: `.venv` con pandas 3.0.5.
- [V] Siete scripts en `src/`, todos ejecutables y reproducibles desde la raíz del proyecto.
- [V] Ocho archivos de resultados en `data/interim/`.
- [V] Métricas de cobertura, densidad y contraste calculadas para los 931 giros.
- [V] Cuatro finalistas identificadas y verificadas como estables ante el umbral de masa.
- [V] Tres candidatas descartadas con evidencia numérica: guarderías, medicina general, lavanderías. Gimnasios descartada con evidencia estructural.

**Sin resolver:**

- **Compuerta G1** (mismo código SCIAN comparable entre ediciones): **abierta**.
- **Compuerta G4** (variación real entre momentos históricos): **abierta**.
- Ambas exigen una segunda edición del DENUE, que no se localizó.

**Decidido pero no ratificado:**

- Recomendación de papelerías con farmacias como respaldo. Pendiente de discusión con el equipo y de que se cierren G1 y G4.

**No hecho:**

- No se descargó ninguna edición histórica del DENUE.
- No se tocó el Marco Geoestadístico ni ningún dato de población.
- No se verificó la comparabilidad de trazos de AGEB entre ediciones.

---

## Pendientes / Próximos pasos

### Bloqueantes para cerrar la decisión

1. `TODO:` Localizar y descargar una edición histórica del DENUE de CDMX (2019 o 2020) desde la RNM. Sin esto, G1 y G4 no se resuelven. **Dueño: David.**
2. `TODO:` Verificar que el código SCIAN de las finalistas no se haya reclasificado entre la edición histórica y la vigente. Un cambio de catálogo produce un salto en el conteo que parece tendencia y no lo es.
3. `TODO:` Cruzar ambas ediciones para obtener **cierres** por AGEB, no solo aperturas.
4. `TODO:` Verificar comparabilidad de trazos de AGEB entre las ediciones que se usen. Si el INEGI partió o fusionó AGEBs, una zona puede "perder" todos sus establecimientos por redibujo y no por cierres.

### Verificaciones de calidad de datos

5. `TODO:` Inspeccionar las AGEBs de mayor conteo del giro 461130 (frutas y verduras, cv 10.99) para confirmar la hipótesis de la Central de Abasto.
6. `TODO:` Auditar a mano una muestra de 20 establecimientos con `ageb` vacía o anómala, si las hay. No se revisó la presencia de nulos en `ageb` ni en `cve_mun`.
7. `TODO:` Confirmar el número oficial de AGEBs urbanas de CDMX contra el Marco Geoestadístico. El 2,439 sale del propio DENUE, es decir, cuenta solo AGEBs **con al menos un establecimiento**. Si hay AGEBs urbanas sin ningún negocio, el denominador real es mayor y todas las coberturas están sobreestimadas. **Este pendiente afecta todos los porcentajes de este documento.**

### Decisiones que el equipo debe tomar

8. `TODO:` Ratificar o rechazar la recomendación de papelerías.
9. `TODO:` Decidir si vale la pena evaluar servicios de radio amplio cambiando la unidad de agregación (Problema 5), o si se acepta el sesgo hacia comercio de proximidad.
10. `TODO:` Decidir si las 11 categorías de alimentos se tratan como una sola familia para la narrativa.

---

## Referencias

**Fuente de datos**

- DENUE, descarga masiva — `https://www.inegi.org.mx/contenidos/masiva/denue/denue_09_csv.zip`
- Portal de descarga masiva del INEGI — `https://www.inegi.org.mx/app/descarga/default.html`
- Red Nacional de Metadatos, catálogo DENUE 2020 — `https://www.inegi.org.mx/rnm/index.php/catalog/587`
- Red Nacional de Metadatos, catálogo DENUE 2025 — `https://www.inegi.org.mx/rnm/index.php/catalog/1103`

**Documentos del proyecto**

- `plan-tecnico-dataton-itam-2026.md` — plan general, roles, roadmap, decisiones D-01 a D-11
- `presentacion-equipo-dataton-2026.md` — versión para Camila y Silvia, sin registro de riesgos

**Salidas de esta sesión**

Todas en `~/Code/dataton-itam-2026/data/interim/`: `actividades_cdmx.csv`, `candidatas.csv`, `permutacion.csv`, `ventana.csv`, `catalogo_metricas.csv`, `fecha_alta.txt`, `diagnostico.txt`, `sensibilidad.txt`.
