# Exploración técnica: pyairbnb — Airbnbs en la ZMG

Fecha de exploración: **2026-05-22**
Explorador: Datero
Fuente: <https://github.com/johnbalvin/pyairbnb> (v2.2.1, instalada vía pip).
Objetivo: capturar todos los Airbnbs activos en los 7 municipios de la
Zona Metropolitana de Guadalajara y obtener su precio para tres
ventanas: temporada normal (referencia), partido de México en GDL
(Mundial 2026) y partido de España en GDL (Mundial 2026).

---

## 1. Por qué pyairbnb (y no Airbtics / Inside Airbnb)

Tres fuentes se evaluaron antes de elegir:

| Fuente | Veredicto |
|---|---|
| **Airbtics** (SaaS comercial) | Descartada. Solo devuelve **clusters agregados**, no listings individuales; fusiona CDMX con Edomex sin distinguir jurisdicción; uso del endpoint interno viola sus ToS. Reporte detallado en `airbtics-exploracion.md`. |
| **Inside Airbnb** (snapshots públicos) | Descartada para este proyecto: **no cubre Guadalajara**, solo CDMX en su set mexicano. Verificado por el usuario. |
| **pyairbnb** | Elegida. Fuente primaria directa a Airbnb, listings individuales con `room_id`, `lat`, `lng`, `name`, `price`, `rating`. Código abierto, replicable, alineado con la ética hacker del datero. Riesgo de ToS de Airbnb asumido bajo encuadre de periodismo de datos sobre información pública. |

## 2. Cómo trabaja pyairbnb por dentro

- Expone funciones que pegan directo a los endpoints frontend de Airbnb (`api.airbnb.com/api/v3/...`), usando la misma `api_key` pública que el sitio web usa para usuarios anónimos (extraíble con `pyairbnb.get_api_key("")`).
- No requiere autenticación de usuario, no requiere proxy obligatorio (soporta proxy si se quema la IP).
- Usa `curl_cffi` para emular fingerprint TLS de un browser real → reduce probabilidad de bloqueo automático.

## 3. Hallazgos del piloto

Pruebas iniciales en bbox de 1-10 km dentro de la ZMG arrojaron tres hallazgos críticos que reorientaron la estrategia:

### 3.1 Cap de 280 resultados por query

Cuatro bboxes de distinto tamaño (0.5×0.5 km, 1×1 km, 5×5 km, 10×10 km, todas en zona urbana densa de GDL) devolvieron **exactamente 280 listings**. El 0.5×0.5 km bajó a 135 (porque no había más en la zona, no por cap). El cap es duro y no es de pyairbnb sino del endpoint frontend de Airbnb.

**Solución implementada**: **tiling adaptativo**. Empezamos con la bbox completa de la ZMG; si una query devuelve 280, subdividimos esa celda en 4 cuadrantes y reintentamos cada uno; recursivamente hasta nivel 5 (4⁵ = 1024 celdas máximo por periodo). Deduplicamos por `room_id` al final.

### 3.2 Parser de precios roto en MXN

`pyairbnb.search_all(..., currency='MXN')` devuelve:
```
price.unit: {'qualifier': 'por 3 noches', 'curency_symbol': '$.60 MXN', 'amount': 1.0}
```
El parser confunde separador de miles (coma) con decimal (punto). El valor real probablemente era `$1,060` o similar pero llega como `1.0`.

En **USD** el parser funciona bien:
```
price.unit: {'qualifier': 'for 3 nights', 'curency_symbol': '$ USD', 'amount': 1067.0, 'discount': 758.0}
```

**Solución implementada**: pedir todo en USD; al CSV final agregamos columna MXN convertida con tipo de cambio fijo **17.3213 MXN/USD** (FIX Banxico DOF 2026-05-22), documentado en el script.

### 3.3 `search_all` filtra por disponibilidad de fecha

`search_all` requiere `check_in`/`check_out` y **solo devuelve listings disponibles** para esa ventana. Implicación importante:

- Pedir el precio Mundial con `get_price()` listing por listing era inviable (a 22 noches consecutivas casi nadie tiene disponibilidad; además son ~10-15k requests adicionales).
- En su lugar: hacemos **tres pasadas de `search_all`**, una por cada ventana, y cruzamos por `room_id`. El propio `search_all` ya trae el precio para sus fechas.

## 4. Estrategia final

### Tres ventanas de búsqueda

| Periodo | Fechas | Significado |
|---|---|---|
| `dummy`  | 15-17 sept 2026 | Temporada normal (referencia comparativa) |
| `mexico` | 17-19 jun 2026  | Partido de México en Estadio Akron (Mundial) |
| `espana` | 25-27 jun 2026  | Partido de España en Estadio Akron (Mundial) |

Cada periodo produce su propio JSON crudo en `data/raw/pyairbnb/listings_zmg_<fecha>_<periodo>.json`.

### Merge y filtro espacial

1. **Universo**: unión por `room_id` de los tres periodos (un listing puede aparecer en cualquiera de los tres, según su disponibilidad).
2. **Filtro point-in-polygon** contra los 2,739 polígonos de colonias INEGI de los 7 municipios de la ZMG, vía índice espacial **STRtree** de shapely (instantáneo para 15k puntos × 2.7k polígonos).
3. Cada listing se enriquece con `nom_mun`, `nom_asen` (colonia exacta) y `cvegeo` de la colonia → datos clave para análisis posterior de gentrificación, concentración o regulación municipal.
4. Listings fuera de los 7 municipios (capturados por el bbox envolvente pero pertenecientes a otros municipios de Jalisco) se descartan.

### CSV final

`data/processed/zmg_airbnb_mundial2026.csv` con columnas:
- `room_id, lat, lng` (mapa de puntos)
- `cve_mun, nom_mun, nom_asen, cvegeo_colonia` (tag espacial)
- `name, title, rating, reviews_count` (descriptivos)
- `price_<periodo>_usd, price_<periodo>_mxn` (precios)
- `disponible_<periodo>` (bool: si el listing aparece o no en el set de ese periodo)
- `discount_<periodo>_usd` (descuento si lo trae)

## 5. Limitaciones conocidas

- **No se capturan listings completamente inactivos** (sin disponibilidad en ninguna de las 3 ventanas). El universo es "Airbnbs activos durante mayo 2026 que tengan disponibilidad en al menos una de las 3 ventanas".
- **Tipo de cambio fijo**: el precio en MXN del CSV usa 17.3213 MXN/USD (FIX Banxico DOF del día de la descarga, 2026-05-22); no refleja fluctuación real ni costos de conversión. Para análisis financiero serio, recalcular con la tasa que aplique al caso (interbancario, tarjeta, casa de cambio).
- **Coordenadas con ofuscación**: Airbnb ofusca lat/lng de los listings (corrimientos de hasta ~150 m) para proteger anfitriones. El mapa de puntos resultante refleja zona aproximada, no dirección exacta. Para análisis a escala de colonia esto es adecuado; a escala de manzana no.
- **`reviews_count` viene como string** ("5", "27") porque Airbnb a veces lo manda con formato ("New", "200+") y el parser conserva el string. Convertir con cuidado en análisis posterior.

## 6. Plan B si pyairbnb deja de funcionar

Si en algún momento Airbnb cambia su API y pyairbnb se rompe:

1. **Reactivar Airbtics como complemento puntual** (49 endpoints adyacentes ya mapeados en `airbtics-exploracion.md`), entendiendo sus limitaciones de agregación.
2. **Considerar fuentes secundarias**: SIMR Jalisco (Sistema de Información del Mercado de Renta) si tiene capa de hospedaje no-tradicional; reportes municipales de hospedaje (IMPLAN GDL); padrones de Sectur Jalisco.
3. **Última instancia**: scraping propio del HTML público de búsqueda de Airbnb (más frágil, más costoso de mantener).

---

*Reporte vivo. Se actualiza con cada corrida del extractor.*
