NomadOptima es un Hybrid Recommender System formulado como Learning to Rank que recomienda ciudades del mundo según el perfil de vida del usuario.
El sistema no responde a la pregunta "¿qué ciudad es mejor?", sino a "¿qué ciudad encaja mejor contigo?". Combina cinco capas algorítmicas: filtrado basado en contenido, clustering de usuarios, clustering de ciudades, un ranker LightGBM LambdaMART y una capa de explicabilidad con SHAP.
| Capa | Tecnología | Versión |
|---|---|---|
| Lenguaje | Python | 3.12.10 |
| Modelo de ranking | LightGBM | 4.6.0 |
| Explicabilidad | SHAP | 0.51.0 |
| Experimentos | MLflow | 3.10.1 |
| API REST | FastAPI | 0.135.2 |
| Demo visual | Streamlit | 1.55.0 |
| Base de datos | PostgreSQL | — |
| Contenedores | Docker | — |
| Clustering | HDBSCAN + UMAP | — |
| Reducción dimensional | scikit-learn (PCA) | 1.8.0 |
Usuario define perfil (26 dimensiones de preferencias)
|
v
[Pre-filtro] Restricciones duras (presupuesto, clima mínimo, coworking, mascotas)
|
v
[Capa 1] Content-Based Filtering — cosine_similarity entre perfil y ciudad
[Capa 2] User Clustering — PCA + UMAP + HDBSCAN sobre 5.000 perfiles
[Capa 3] City Clustering — agrupación de ciudades similares
|
v
[Capa 4] LightGBM LambdaMART — ranking con 175 features
|
v
[Capa 5] SHAP (explicación) + MMR (diversificación del top-N)
|
v
Top-N ciudades con justificación por dimensión
Capa 1 — Content-Based Filtering Calcula la similitud del coseno entre el vector de preferencias del usuario (26 dimensiones) y el vector de features de cada ciudad (148 features). Actúa como señal de apoyo para el ranker, no como fuente de etiquetas.
Capa 2 — User Clustering Agrupa los 5.000 perfiles sintéticos con PCA → UMAP → HDBSCAN. Permite al modelo generalizar por tipo de viajero: nómada digital, familia con hijos, jubilado activo, deportista outdoor, etc.
Capa 3 — City Clustering Agrupa las 54 ciudades por similitud de features. Con 54 ciudades los clusters son orientativos; se migrará a UMAP+HDBSCAN automático cuando haya 200+ ciudades.
Capa 4 — LightGBM LambdaMART Aprende correlaciones no lineales entre las preferencias del usuario y las características de las ciudades. Recibe 175 features: 26 preferencias de usuario + 148 features de ciudad + 1 cosine_sim. Resultado actual: NDCG@5 = 0.9631, 43 árboles, 54 ciudades.
Capa 5 — SHAP + MMR SHAP explica por qué cada ciudad ocupa su posición en el ranking. MMR (Maximal Marginal Relevance) diversifica el top-N para evitar recomendar ciudades casi idénticas.
| Fuente | Qué aporta | Prefijo de features |
|---|---|---|
| Google Places New API | Establecimientos por tipo en radio urbano (~150 tipos) | gp_ |
| Numbeo | Coste de vida, precios, índices de calidad | numbeo_ |
| OpenStreetMap (Overpass) | Infraestructura urbana etiquetada | osm_ |
| wttr.in | Clima puntual | weather_ |
| Speedtest (Ookla) | Velocidad de internet por país | speedtest_ |
| RestCountries | Idioma, zona horaria, espacio Schengen, UE | country_ |
- Python 3.12
- pip
- virtualenv (incluido en Python 3.12 con
venv) - Git
git clone https://github.com/CDiazCapado/NomadOptima.git
cd NomadOptimapython -m venv .venvWindows:
.venv\Scripts\activatemacOS / Linux:
source .venv/bin/activatepip install -r requirements.txtEl repositorio no incluye el archivo .env porque contiene API keys reales. Crea uno en la raíz del proyecto:
cp .env.example .envAbre .env y rellena las siguientes claves:
GOOGLE_PLACES_API_KEY=tu_clave_aqui
NUMBEO_API_KEY=tu_clave_aqui
El archivo
.envestá en.gitignorey nunca se sube al repositorio.
Los datos necesarios para ejecutar la app están incluidos en el repositorio — no hace falta ejecutar ningún script de ingesta ni tener API keys:
| Archivo | Tamaño | Descripción |
|---|---|---|
data/processed/city_features.csv |
30 KB | 54 ciudades × 149 features (ya en el repo) |
data/processed/model_v3/ |
317 KB | 4 artefactos del modelo entrenado (ya en el repo) |
Basta con clonar el repo e instalar dependencias para tener la app funcionando.
Requiere API keys en .env (Google Places, Numbeo) y ejecutar los notebooks en orden:
# 1. Ingesta de datos de 54 ciudades (requiere API keys)
python src/ingestion/fetch_cities.py
# 2. Abrir Jupyter y ejecutar en orden:
jupyter notebook notebooks/
# 02_synthetic_profiles_v3.ipynb → genera user_profiles.csv
# 03_train_model.ipynb → entrena LightGBM, genera model_v3/La ingesta llama a Google Places API y Numbeo, que tienen coste por petición.
streamlit run app/streamlit_app.pyLa app se abre en http://localhost:8501.
- Sube el repositorio a GitHub (puede ser público o privado).
- Ve a share.streamlit.io e inicia sesión con tu cuenta de GitHub.
- Haz clic en New app.
- Selecciona el repositorio, la rama (
main) y el archivo principal:app/streamlit_app.py. - En Advanced settings → Dependencies, usa
requirements_app.txtcomo archivo de dependencias. - Haz clic en Deploy. En 2-3 minutos tendrás una URL pública del tipo:
https://tu-usuario-nomadoptima.streamlit.app
No se necesitan secretos ni variables de entorno para ejecutar la app — los datos están en el repo.
El usuario define su perfil en 20 categorías y el sistema devuelve un ranking de ciudades con puntuación y justificación por dimensión.
nomadoptima/
├── data/
│ ├── raw/ <- JSONs de ingesta (en .gitignore)
│ └── processed/
│ ├── city_features.csv <- 54 ciudades × 149 features (en el repo)
│ ├── user_profiles.csv <- 5.000 perfiles × 26 dims (en .gitignore)
│ ├── training_dataset.csv <- 270.000 filas × 177 cols (en .gitignore)
│ └── model_v3/ <- artefactos del modelo entrenado (en el repo)
│
├── notebooks/
│ ├── 01_eda_ciudades.ipynb <- EDA Fase 1 — fuentes y decisiones
│ ├── 01b_eda_fase2_ciudades.ipynb <- EDA Fase 2 — análisis completo
│ ├── 01b_eda_arquetipos.ipynb <- EDA Fase 3 — validación arquetipos
│ ├── 01c_eda_perfiles_sinteticos.ipynb <- EDA Fase 4 — validación perfiles
│ ├── 02_synthetic_profiles_v3.ipynb <- generación de perfiles sintéticos
│ └── 03_train_model.ipynb <- entrenamiento LightGBM
│
├── src/
│ ├── ingestion/
│ │ └── fetch_cities.py <- ingesta 6 APIs, 54 ciudades
│ ├── processing/
│ │ └── features.py <- CityFeatureBuilder + cosine_sim (Capa 1)
│ └── models/
│ ├── clustering.py <- UserClusterer + CityClusterer (Capas 2+3)
│ ├── ranker.py <- NomadRanker LightGBM (Capa 4)
│ └── explainer.py <- SHAP + MMR (Capa 5, pendiente)
│
├── app/
│ ├── streamlit_app.py <- demo visual conectada a LightGBM
│ └── city_content.py <- contenido editorial de 54 ciudades
│
├── api/
│ └── main.py <- FastAPI /recommend (pendiente)
│
├── scripts/ <- herramientas auxiliares
├── requirements.txt
└── .env <- API keys (NO incluido en el repo)
| Métrica | Valor |
|---|---|
| NDCG@5 | 0.9631 |
| Árboles | 43 |
| Features de entrada | 175 |
| Ciudades en el catálogo | 54 |
| Perfiles de entrenamiento | 5.000 |
| Pares de entrenamiento | 270.000 |
# Activar entorno virtual (Windows)
.venv\Scripts\activate
# Ingesta de datos de las 54 ciudades
python src/ingestion/fetch_cities.py
# Abrir notebooks
jupyter notebook notebooks/
# MLflow — revisar experimentos
mlflow ui
# API REST (cuando esté lista)
uvicorn api.main:app --reload
# Demo visual
streamlit run app/streamlit_app.py| Paso | Descripción | Estado |
|---|---|---|
| 1 | Ingesta de datos — 6 fuentes, 54 ciudades | Completado |
| 2 | EDA — 4 fases (datos, ciudades, arquetipos, perfiles) | Completado |
| 3 | Perfiles sintéticos — 5.000 perfiles × 26 dimensiones | Completado |
| 4 | LightGBM Ranker — NDCG@5=0.9631 | Completado |
| 5 | SHAP explicabilidad | Pendiente |
| 6 | MLflow tracking | Pendiente |
| 7 | FastAPI endpoint /recommend | Pendiente |
| 8 | Streamlit demo conectada | Completado (v3) |
| 9 | Docker Compose | Pendiente |
Cristina Díaz Capado — Proyecto final del bootcamp de Machine Learning, 4Geeks Academy.
Repositorio: github.com/CDiazCapado/NomadOptima