flowchart TD
subgraph sources["🌐 FUENTES DE DATOS"]
A1[API Madrid XML<br/>19 estaciones, 10 contaminantes<br/>Actualización: 20 min]
A2[AEMET OpenData<br/>Predicciones meteorológicas<br/>Horarias: 40h futuro]
A3[Portal Open Data Madrid<br/>Históricos 2015-2025<br/>ZIPs/CSV mensuales]
end
subgraph db["💾 POSTGRESQL + PostGIS"]
B1[(dim_estaciones<br/>16 stations + coords)]
B2[(dim_magnitudes<br/>Pollutants metadata)]
B3[(fact_mediciones<br/>11M+ rows hourly)]
B4[(fact_meteo_diaria<br/>Daily weather)]
end
subgraph feature["⚙️ FEATURE ENGINEERING<br/>R + data.table"]
C1[Variables espaciales<br/>UTM coords, distancias]
C2[Variables temporales<br/>hora, día, mes, trimestre]
C3[Variables meteorológicas<br/>VPD, temp/hum ratios]
C4[Baseline estacional<br/>Promedios móviles 30d]
end
subgraph training["🎯 ENTRENAMIENTO<br/>XGBoost GPU/CPU"]
D1[Grid search CV<br/>nrounds, depth, eta]
D2[5 modelos ICA<br/>NO2, PM10, PM2.5, O3, SO2]
D3[Output: .model files<br/>16 MB total]
end
subgraph cicd["🔄 GITHUB ACTIONS<br/>Daily 4:00 AM"]
E1[Pull Docker image<br/>GHCR pre-built]
E2[Download models<br/>GitHub Release 16MB]
E3[Collect realtime<br/>APIs Madrid + AEMET]
E4[Generate predictions<br/>16×5×40 = 3,200 rows]
E5[Deploy Shiny<br/>shinyapps.io auto]
end
subgraph app["🖥️ DASHBOARD SHINY"]
F1[Leaflet maps<br/>Bubbles por concentración]
F2[Plotly charts<br/>Evolución temporal]
F3[Animaciones<br/>10 timesteps]
F4[Export CSV/JSON]
end
sources --> db
db --> feature
feature --> training
training --> D3
D3 --> E2
E1 --> E3
E2 --> E3
E3 --> E4
E4 --> E5
E5 --> app
style sources fill:#e3f2fd
style db fill:#f3e5f5
style feature fill:#fff3e0
style training fill:#e8f5e9
style cicd fill:#fce4ec
style app fill:#e0f2f1
Sistema MLOps de Calidad del Aire de Madrid
De 1.5 GB a 16 MB: Pipeline completa de ML con R, Docker y GPU
Enlaces del Proyecto
Accede al sistema de predicción en tiempo real
Explora el repositorio completo en GitHub
Resumen del Proyecto
En el mundo del análisis de datos, es común ver dashboards que visualizan datos históricos. Pero, ¿qué se necesita para construir un sistema que no solo visualice, sino que prediga activamente el futuro y se actualice solo, día tras día?
Este proyecto es una pipeline completa de Machine Learning en producción para predicción de calidad del aire en Madrid. A diferencia de dashboards estáticos, este sistema:
- Recolecta datos históricos (2015-2025) y en tiempo real de múltiples APIs
- Procesa más de 11 millones de registros con variables meteorológicas y espaciales
- Entrena modelos predictivos (XGBoost) acelerados por GPU
- Genera predicciones a 40 horas vista cada día de forma automática
- Despliega automáticamente actualizaciones en un dashboard web interactivo
Lo extraordinario: El sistema completo está automatizado con GitHub Actions, empaquetado en Docker para reproducibilidad total, y utiliza modelos de solo 16 MB (reducidos desde 1.5 GB) que permiten ejecutar todo en infraestructura gratuita.
El Reto: De 1.5 GB a 16 MB
El Problema Original
La primera versión del sistema (v1.0) utilizaba modelos Random Forest con el paquete ranger. Los resultados eran precisos, pero el sistema era inviable para producción:
- Tamaño de modelos: ~300 MB por contaminante × 5 contaminantes = 1.5 GB total
- Memoria en predicción: ~1.5 GB de RAM requerida
- Consecuencias:
- GitHub Actions (7 GB RAM): Errores de Out-of-Memory
- shinyapps.io (límite 1 GB): Imposible desplegar
- Git LFS: Costes adicionales y complejidad
La Solución: Migración a XGBoost Nativo
La reingeniería completa de la pipeline de ML usando la API nativa de XGBoost (sin CARET) produjo resultados extraordinarios:
| Métrica | Ranger (v1.0) | XGBoost (v2.0) | Mejora |
|---|---|---|---|
| Tamaño 5 modelos | 1.5 GB | 16 MB | ↓ 99% |
| RAM predicción | ~1.5 GB | ~50 MB | ↓ 97% |
| RAM training | 4-9 GB | ~200 MB | ↓ 95% |
| Tiempo training (CPU) | ~15 min | ~5 min | ↓ 67% |
| Tiempo training (GPU) | N/A | ~2 min | - |
| Viabilidad CI/CD | ❌ Imposible | ✅ Funcional | - |
Esta optimización fue el verdadero punto de inflexión que hizo posible todo el flujo de MLOps en infraestructura gratuita.
Arquitectura del Sistema
El sistema funciona como una pipeline de producción completa, dividida en fases claramente diferenciadas:
Stack Tecnológico: R en Producción
¿Por qué R en 2025?
R se mantiene como líder en estadística y análisis espacial donde Python aún está alcanzándolo. Este proyecto demuestra que R, con el conjunto de paquetes adecuado, es un entorno de producción formidable.
1. data.table - Motor de Procesamiento Ultra-Rápido
Por qué elegido: Operaciones zero-copy y modificación por referencia = 10-100x más rápido que pandas o dplyr.
# Procesar 4.36M registros de NO2 en segundos
datos_finales <- datos_finales[
!is.na(prediccion) & prediccion >= 0,
.(prediccion = mean(prediccion, na.rm = TRUE)),
by = .(id_estacion, fecha_hora, contaminante)
]Ventajas clave: - Sin copias en memoria (modificación in-place con :=) - Sintaxis concisa para joins y agregaciones complejas - Optimizado para datasets grandes (>1M filas)
2. xgboost - Machine Learning GPU-Accelerated
Por qué elegido: API nativa C++, soporte GPU CUDA, modelos compactos, producción-ready.
# Configuración GPU para entrenamiento acelerado
params <- list(
tree_method = "hist", # Histogram-based (GPU optimizado)
device = "cuda", # GPU acceleration
max_bin = 256, # Precisión alta
predictor = "gpu_predictor"
)Rendimiento GPU vs CPU (RTX 4070 Ti): - NO2 (4.36M obs): 2.08 min (GPU) vs 5.5 min (CPU) - Total 5 modelos: 6.26 min (GPU) vs 16.6 min (CPU) → 2.65x speedup
3. sf - Spatial Features (GIS en R)
Por qué elegido: Geometrías espaciales compatibles con PostGIS y estándar OGC.
# Convertir coordenadas a objeto espacial
datos_sf <- st_as_sf(datos,
coords = c("lon", "lat"),
crs = 4326 # WGS84
)Ventajas clave: - Integración directa con PostgreSQL/PostGIS - Operaciones espaciales (distancias, buffers, intersecciones) - Compatible con Leaflet para mapas web interactivos
4. Shiny - Dashboard Interactivo Sin JavaScript
Por qué elegido: Crear aplicaciones web reactivas complejas escribiendo únicamente R.
# Reactivity automática - el mapa se actualiza cuando cambian los datos
output$mapa <- renderLeaflet({
datos <- datos_predicciones() # Reactive expression
leaflet(datos) %>% addCircleMarkers(...)
})Alternativas descartadas: - Python Dash: Requiere más código boilerplate - React + API REST: Overhead de mantener frontend/backend separados - PowerBI/Tableau: No permiten predicciones custom con ML
Docker: La Capa Invisible de Reproducibilidad
El Problema: Dependencias Espaciales Complejas
R + dependencias espaciales (GDAL, PROJ, GEOS) son notoriamente difíciles de instalar y varían entre sistemas operativos. Compilar sf desde source puede tomar 45 minutos.
La Solución: Docker + RSPM
Docker asegura el mismo entorno en: - Desarrollo local (Windows/Mac/Linux) - GitHub Actions (Ubuntu) - Cualquier servidor cloud - Raspberry Pi (ARM64)
FROM rocker/r-ver:4.5.1
# RSPM: Binarios precompilados (Ubuntu 24.04 Noble)
# Sin esto: compilar sf/gdal toma 45 minutos
# Con esto: instalar binarios toma 3 minutos
RUN echo 'options(repos = c(
RSPM = "https://packagemanager.posit.co/cran/__linux__/noble/latest",
CRAN = "https://cloud.r-project.org"
))' >> /usr/local/lib/R/etc/Rprofile.siteRStudio Package Manager (RSPM): - Posit compila todos los paquetes CRAN para Ubuntu - Descarga binarios (.so) en vez de compilar desde source (.tar.gz) - Build time: 55 min → 3-4 min (93% mejora)
Estrategia de Layers para Cache Eficiente
# Layer 1: System dependencies (raramente cambia) → cacheado
RUN apt-get install libgdal-dev libproj-dev libgeos-dev
# Layer 2: R packages (cambia cuando se actualiza install_packages.R) → cacheado
COPY install_packages.R /tmp/
RUN Rscript /tmp/install_packages.R
# Layer 3: Application code (cambia frecuentemente) → no cacheado
COPY . /appResultado: Rebuilds posteriores solo recompilan la capa 3 (código de aplicación), reutilizando las capas 1-2 cacheadas.
GitHub Actions: Automatización Diaria
El verdadero poder del sistema está en su autonomía completa. Cada día a las 4:00 AM UTC, sin intervención humana:
sequenceDiagram
participant GA as GitHub Actions
participant GHCR as GitHub Container Registry
participant GHR as GitHub Release
participant API as APIs (Madrid + AEMET)
participant Docker as Docker Container
participant Shiny as shinyapps.io
Note over GA: 🕓 4:00 AM UTC - Cron trigger
GA->>GHCR: Pull Docker image (pre-built 2GB)
GHCR-->>GA: Image cached/downloaded
GA->>GHR: Download XGBoost models (16 MB)
GHR-->>GA: 5 .model files
GA->>Docker: Start container with volume mounts
Docker->>API: Request realtime data + forecast
API-->>Docker: XML/JSON responses
Docker->>Docker: Load models + Generate predictions<br/>16 stations × 5 pollutants × 40 hours
Docker->>GA: Save predicciones_40h_latest.rds (3,200 rows)
GA->>Shiny: rsconnect::deployApp() automated
Shiny-->>Shiny: App updated with new predictions
Note over Shiny: ✅ Users see latest forecasts
Workflow Detallado
# .github/workflows/daily-predictions.yml
jobs:
daily-predictions:
runs-on: ubuntu-latest # 7 GB RAM, 2 cores - GRATIS
steps:
- name: Pull Docker image
run: docker pull ghcr.io/${{ github.repository }}/air-quality-predictor:latest
- name: Download trained models
run: gh release download --pattern "xgboost_nativo_ica_*.model" --dir models/
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Generate predictions (XGBoost Native)
timeout-minutes: 10
run: |
docker run --rm \
-v $(pwd):/app \
-e DB_HOST="${{ secrets.DB_HOST }}" \
-e AEMET_API_KEY="${{ secrets.AEMET_API_KEY }}" \
ghcr.io/${{ github.repository }}/air-quality-predictor:latest \
Rscript R/07_predicciones_horarias.R
- name: Deploy to shinyapps.io
run: |
docker run --rm -v $(pwd):/app \
ghcr.io/${{ github.repository }}/air-quality-predictor:latest \
Rscript -e "
library(rsconnect)
deployApp(
appDir = 'app',
appName = 'madrid-air-quality',
account = '${{ secrets.SHINYAPPS_NAME }}',
token = '${{ secrets.SHINYAPPS_TOKEN }}',
secret = '${{ secrets.SHINYAPPS_SECRET }}'
)"Por qué funciona ahora (antes fallaba): - Antes (Ranger): Modelos 1.5 GB → GitHub Actions OOM (7 GB RAM insuficiente) - Ahora (XGBoost): Modelos 16 MB + predicciones ~50 MB RAM → Ejecuta sin problemas
Base de Datos: PostgreSQL + PostGIS
Esquema Estrella (Data Warehouse Design)
El sistema no lee archivos CSV directamente. Toda la data histórica y en tiempo real se almacena en PostgreSQL con la extensión PostGIS, permitiendo consultas espaciales complejas.
erDiagram
dim_estaciones ||--o{ fact_mediciones : "id_estacion"
dim_magnitudes ||--o{ fact_mediciones : "id_magnitud"
fact_mediciones }o--|| fact_meteo_diaria : "fecha"
dim_estaciones {
int id_estacion PK
varchar nombre_estacion
geometry coords "PostGIS Point WGS84"
varchar tipo_estacion "Urbana/Tráfico/Fondo"
float utm_x "Coordenada UTM 30N"
float utm_y "Coordenada UTM 30N"
}
dim_magnitudes {
int id_magnitud PK
varchar nombre_magnitud "Dióxido de Nitrógeno"
varchar unidad "µg/m³"
varchar tipo_ica "ICA/No-ICA"
}
fact_mediciones {
int id_estacion FK
int id_magnitud FK
timestamp fecha_hora
numeric valor_medio
char valor_validado "V/N"
}
fact_meteo_diaria {
date fecha PK
numeric temp_media_c
numeric humedad_media_pct
numeric precipitacion_mm
numeric vel_viento_media_ms
numeric dir_viento_grados
}
Transformación Wide-to-Long
Datos originales Madrid (formato CSV):
ESTACION,MAGNITUD,ANO,MES,DIA,H01,H02,...,H24,V01,V02,...,V24
28079004,8,2025,01,15,25.3,24.1,...,30.2,V,V,...,V
Problema: 48 columnas (H01-H24 valores + V01-V24 flags) → imposible de consultar eficientemente.
Solución en R (R/utils.R):
procesar_y_cargar_lote <- function(datos_wide, con) {
# Pivot de H01-H24 a formato long
datos_long <- melt(datos_wide,
id.vars = c("id_estacion", "id_magnitud", "ano", "mes", "dia"),
measure.vars = patterns("^H"),
variable.name = "hora",
value.name = "valor_medio"
)
# Crear timestamp
datos_long[, fecha_hora := as.POSIXct(
paste(ano, mes, dia, gsub("H", "", hora), "00", "00"),
format = "%Y %m %d %H %M %S",
tz = "Europe/Madrid"
)]
# Bulk insert a PostgreSQL (ultra-rápido)
dbWriteTable(con, "fact_mediciones", datos_long, append = TRUE)
}Ventajas Long Format: - Queries SQL simples: WHERE fecha_hora BETWEEN ... AND ... - Joins eficientes con meteorología - Compatible con XGBoost (requiere 1 fila = 1 observación)
PostGIS: Consultas Espaciales
# Calcular distancias entre estaciones (UTM projection para metros)
consulta <- "
SELECT
a.id_estacion AS est_origen,
b.id_estacion AS est_vecina,
ST_Distance(
ST_Transform(a.coords, 25830), -- WGS84 → UTM 30N
ST_Transform(b.coords, 25830)
) AS distancia_metros
FROM dim_estaciones a
CROSS JOIN dim_estaciones b
WHERE a.id_estacion != b.id_estacion
"EPSG Codes: - 4326: WGS84 (lat/lon, usado en GPS/Leaflet) - 25830: ETRS89 UTM Zone 30N (Madrid, metros, para cálculos de distancia precisos)
Workflow Completo: Del Dato a la Predicción
Fase 1: Setup Inicial (Una Vez)
# 1. Crear tablas dimensionales desde catálogos oficiales
source("R/01_setup_dimension_tables.R")
# Output: dim_estaciones (16 stations), dim_magnitudes (10 pollutants)
# 2. Descargar históricos 2015-2025 (proceso largo: ~2-3 horas)
source("R/02_collect_historical_data.R")
# - Scraping de ~120 archivos ZIP (1 por mes)
# - Descomprimir + parsear CSV
# - Transformar wide → long format
# - Bulk insert a PostgreSQL
# Output: fact_mediciones (~11M filas)
# 3. Añadir predictores espaciales
source("R/03_create_predictors.R")
# - Calcular coordenadas UTM
# - Crear campos: distancia_centro, elevacion, zona_urbana
# Output: Columnas adicionales en dim_estaciones
# 4. Colectar meteorología AEMET (requiere API key gratuita)
source("R/04_collect_meteo_data.R")
# - API requests a AEMET OpenData (límite: 1,000 requests/day)
# - Datos diarios: temp, humedad, viento, presión
# Output: fact_meteo_diaria (~3,650 filas = 10 años)
# 5. Crear baseline estacional
source("R/05_crear_baseline_estacional.R")
# - Calcular promedios móviles 30 días por contaminante/estación
# - Detectar patrones estacionales (verano: O3 ↑, invierno: NO2 ↑)
# Output: Tabla baseline_estacionalFase 2: Entrenamiento Modelos (GPU Recomendado)
# 6. Entrenar XGBoost (5 contaminantes ICA)
source("R/06_modelo_xgboost_ica.R")Detalle del proceso de entrenamiento:
library(data.table)
library(xgboost)
# Conectar a BD y cargar datos (ejemplo: NO2)
con <- dbConnect(RPostgres::Postgres(), ...)
query <- "
SELECT
m.fecha_hora,
m.id_estacion,
m.valor_medio,
e.utm_x, e.utm_y,
meteo.temp_media_c,
meteo.humedad_media_pct,
baseline.valor_baseline
FROM fact_mediciones m
LEFT JOIN dim_estaciones e ON m.id_estacion = e.id_estacion
LEFT JOIN fact_meteo_diaria meteo ON DATE(m.fecha_hora) = meteo.fecha
LEFT JOIN baseline_estacional baseline ON ...
WHERE m.id_magnitud = 8 -- NO2
AND m.valor_validado = 'V'
AND m.fecha_hora >= '2015-01-01'
"
datos <- setDT(dbGetQuery(con, query)) # 4.36M rows
# Feature engineering
datos[, `:=`(
hora_dia = hour(fecha_hora),
dia_semana = wday(fecha_hora),
mes = month(fecha_hora),
trimestre = quarter(fecha_hora),
vpd = calcular_vpd(temp_media_c, humedad_media_pct), # Vapor pressure deficit
temp_hum_ratio = temp_media_c / (humedad_media_pct + 1)
)]
# Preparar matriz XGBoost
features <- c("hora_dia", "dia_semana", "mes", "trimestre",
"temp_media_c", "humedad_media_pct", "vpd",
"valor_baseline", "utm_x", "utm_y")
dmatrix <- xgb.DMatrix(
data = as.matrix(datos[, ..features]),
label = datos$valor_medio
)
# Grid search con xgb.cv (5-fold cross-validation)
param_grid <- expand.grid(
nrounds = c(100, 200, 300),
max_depth = c(6, 8, 10),
eta = c(0.01, 0.05, 0.10)
)
mejor_rmse <- Inf
for (i in 1:nrow(param_grid)) {
params <- list(
tree_method = "hist",
device = "cuda", # GPU
max_depth = param_grid$max_depth[i],
eta = param_grid$eta[i]
)
cv_result <- xgb.cv(
params = params,
data = dmatrix,
nrounds = param_grid$nrounds[i],
nfold = 5,
metrics = "rmse",
verbose = 0
)
rmse_mean <- min(cv_result$evaluation_log$test_rmse_mean)
if (rmse_mean < mejor_rmse) {
mejor_rmse <- rmse_mean
mejores_params <- params
mejores_nrounds <- param_grid$nrounds[i]
}
}
# Entrenar modelo final con mejores hiperparámetros
modelo_final <- xgb.train(
params = mejores_params,
data = dmatrix,
nrounds = mejores_nrounds
)
# Guardar en formato nativo XGBoost (binario compacto)
xgb.save(modelo_final, "models/xgboost_nativo_ica_Dióxido_de_Nitrógeno.model")
# Tamaño: 3.5 MB (vs 300 MB con Ranger)Rendimiento GPU vs CPU:
| Contaminante | Observaciones | GPU (RTX 4070 Ti) | CPU (16 cores) |
|---|---|---|---|
| NO2 | 4.36M | 2.08 min | 5.5 min |
| PM10 | 2.28M | 1.21 min | 3.2 min |
| PM2.5 | 1.26M | 0.80 min | 2.1 min |
| O3 | 2.47M | 1.35 min | 3.6 min |
| SO2 | 1.31M | 0.82 min | 2.2 min |
| Total | 11.68M | 6.26 min | 16.6 min |
Fase 3: Predicciones Operacionales (Automatizado)
# 7. Generar predicciones 40h (ejecutado por GitHub Actions)
source("R/07_predicciones_horarias.R")Output: 3,200 predicciones con estructura espacial completa:
# predicciones_xgb_nativo_40h_latest.rds
str(predicciones_sf)
# Classes 'sf' and 'data.table'
# 3200 obs. of 15 variables:
# $ id_estacion : int 4 4 4 ...
# $ nombre_estacion : chr "Pza. de España" ...
# $ contaminante : chr "Dióxido de Nitrógeno" ...
# $ fecha_hora : POSIXct "2025-11-01 13:00:00" ...
# $ prediccion : num 28.5 29.1 30.2 ...
# $ temp_media_c : num 18.5 19.2 20.1 ...
# $ humedad_media_pct: num 65 63 61 ...
# $ geometry : sfc_POINT (lon, lat WGS84)Deployment: Producción Sin Servidores
Infraestructura 100% Gratuita
El sistema completo funciona en infraestructura gratuita:
- GitHub: Repositorio privado/público, Actions (2,000 min/mes gratis)
- GitHub Container Registry (GHCR): Almacenamiento de Docker images
- GitHub Releases: Almacenamiento de modelos (16 MB)
- shinyapps.io: Plan Free (5 apps, 25 horas activas/mes)
Coste mensual real: $0.00
Monitoreo y Observabilidad
flowchart LR
A[GitHub Actions Logs] -->|Build status| B[✅/❌ Notificaciones]
C[shinyapps.io Dashboard] -->|Uptime, usuarios| D[📊 Métricas]
E[Shiny App] -->|Errores runtime| F[🔍 Debug]
style A fill:#e3f2fd
style C fill:#f3e5f5
style E fill:#fff3e0
Monitoreo incluye: - GitHub Actions: Build status, errores de ejecución, duración de workflows - shinyapps.io: Uptime, usuarios activos, crashes de aplicación - Logs de R: Errores de predicción, warnings de modelos
Consideraciones de Hardware
Desarrollo Local
Mínimo requerido: - CPU: 4 cores, 8 GB RAM - Disco: 20 GB (BD + outputs) - OS: Windows/Mac/Linux con Docker
Recomendado: - CPU: 16+ cores, 32 GB RAM - GPU: CUDA-capable (RTX 3060+, 8+ GB VRAM) - Disco: SSD 50 GB
Training de Modelos
Sin GPU (CPU-only): - 16 cores → ~5 min total (5 modelos) - 8 cores → ~12 min total - 4 cores → ~30 min total
Con GPU: - RTX 4070 Ti (12 GB VRAM) → ~2 min total - RTX 3060 (8 GB VRAM) → ~3 min total - Requiere: CUDA 11.x+, cuDNN
Setup GPU en R:
# Instalar XGBoost con GPU support (Windows/Linux)
install.packages("xgboost",
repos = "https://xgboost-builds.s3.amazonaws.com")
# Verificar GPU disponible
library(xgboost)
xgb.DMatrix.save(xgb.DMatrix(matrix(1)), "test.buffer")
# Si no error → GPU OKProducción (GitHub Actions)
GitHub-hosted runner: - ubuntu-latest: 2 cores, 7 GB RAM, 14 GB SSD - Sin GPU (predicciones solo CPU) - Suficiente para predicciones (no training)
Workflow actual (timing promedio): - Pull Docker image: ~2 min - Download models: ~5 sec - Predicciones: ~10 sec - Deploy Shiny: ~30 sec - Total: ~3-4 min
Raspberry Pi: Edge Computing (Opcional)
El sistema está diseñado para funcionar también en Raspberry Pi 5:
Specs Raspberry Pi 5: - CPU: ARM Cortex-A76 (4 cores, 2.4 GHz) - RAM: 8 GB LPDDR4 - Sin GPU (solo CPU)
Casos de uso: - ✅ Dashboard local (Shiny en puerto 3838) - ✅ Predicciones (load modelos pre-trained) - ⚠️ Training: Lento pero posible (~30 min)
Ventajas Edge Computing: - Bajo consumo (5W vs 300W PC) - Siempre encendido (servidor local 24/7) - Sin dependencia de cloud - Latencia cero para usuarios locales
Setup Raspberry Pi:
# Instalar Docker en Raspberry Pi OS (ARM64)
curl -sSL https://get.docker.com | sh
sudo usermod -aG docker pi
# Pull imagen multi-arch
docker pull ghcr.io/michal0091/madrid-air-quality-system/air-quality-predictor:latest
# Ejecutar predicciones locales
docker run --rm -v $(pwd):/app \
-e DB_HOST="192.168.1.100" \
ghcr.io/.../air-quality-predictor:latest \
Rscript R/07_predicciones_horarias.R
# Shiny local
docker run -d -p 3838:3838 \
-v $(pwd)/app:/srv/shiny-server \
rocker/shiny:latestLecciones Aprendidas y Conclusiones
1. R es Viable para ML en Producción
Este proyecto demuestra que R no es solo para análisis exploratorio. Con el stack correcto:
data.tablesupera apandasen velocidadxgboostGPU es tan rápido como cualquier implementación Pythonsf+ PostGIS ofrecen capacidades GIS de nivel profesionalShinyelimina la necesidad de frontend/backend separados
2. La Infraestructura Invisible Es Lo Que Importa
Las tecnologías que hacen funcionar todo:
- RSPM: Sin binarios precompilados, Docker builds serían 93% más lentos
- GHCR: Sin registry gratuito, necesitaríamos Docker Hub paid o AWS ECR
- GitHub Releases: Sin storage separado, modelos de 16 MB inflarían repo con Git LFS
- XGBoost nativo: Sin modelos compactos, GitHub Actions fallaría por OOM
3. Optimización ≠ Solo Algoritmos
La reducción de 1.5 GB a 16 MB no fue cambiar hiperparámetros. Fue:
- Eliminar abstracción innecesaria (CARET)
- Usar formato binario nativo (
.modelvs.rds) - Arquitectura gradient boosting (árboles secuenciales vs paralelos)
- Histogram binning de XGBoost
4. MLOps Es Accesible
Todo el stack MLOps (CI/CD, containerización, deployment) funciona en infraestructura 100% gratuita:
- GitHub Actions: 2,000 min/mes
- GHCR: Ilimitado para repos públicos
- shinyapps.io: 25 horas activas/mes
- Coste mensual: $0
Impacto y Aplicaciones
Contribución Open Source
- Código abierto: Todo el código disponible en GitHub
- Documentación completa: Workflow técnico de 10,000+ palabras
- Reproducible: Cualquiera puede clonar y ejecutar localmente
- Educativo: Ejemplo real de MLOps end-to-end en R
Aplicabilidad Profesional
Casos de uso transferibles:
- Análisis de series temporales: Predicción de demanda, forecasting financiero
- Datos espaciales: GIS, análisis de movilidad, urban planning
- Procesamiento masivo: ETL de millones de registros en R
- Deployment automático: CI/CD para aplicaciones R en producción
Sectores aplicables:
- Medio ambiente: Calidad del aire, agua, suelo
- Salud pública: Epidemiología espacial, predicción de brotes
- Smart cities: Tráfico, energía, residuos
- Finanzas: Predicción de riesgo espacial, fraude geográfico
Recursos y Enlaces
Proyecto
Tecnologías Clave
Datos Abiertos
Este proyecto representa la convergencia de análisis de datos, ingeniería de software y MLOps, demostrando que el ecosistema R es una opción viable, eficiente y cost-effective para sistemas de Machine Learning en producción.
Lo extraordinario no son solo los modelos predictivos, sino la infraestructura automatizada que los soporta día tras día, sin intervención humana, en hardware gratuito.