DEV Community

Cover image for De datos abiertos a producción: construyendo FuelScope con Python, Docker y GitHub Actions
Kiara H Murillo
Kiara H Murillo

Posted on Edited on

De datos abiertos a producción: construyendo FuelScope con Python, Docker y GitHub Actions

Un archivo CSV puede parecer una fuente perfecta para crear un dashboard: se descarga, se carga con Pandas y se dibujan algunos gráficos. Pero, cuando empecé a trabajar con datos de precios de combustibles en Perú, descubrí que lo complicado no era dibujar la línea de tendencia. Era asegurarme de qué estaba midiendo realmente.

¿La fecha del archivo es la fecha del precio? ¿Todas las categorías utilizan la misma unidad? ¿Qué pasa si la fuente cambia o deja de estar disponible?

Así nació FuelScope, una aplicación que desarrollé con Python, Streamlit y Plotly para explorar precios históricos publicados por OSINERGMIN. Quise ir un poco más allá del dashboard: organizar mejor el código, validar los datos, automatizar las comprobaciones y poder reproducir el proyecto con Docker.

Si quieres explorar el resultado o revisar la implementación, aquí están los enlaces:

El verdadero problema no era crear gráficos

Al principio podía plantear una pregunta muy amplia: «¿Dónde es más barato comprar combustible?». Sin embargo, el archivo utilizado no incluye la ubicación geográfica de los establecimientos ni documenta la unidad física de cada precio. Con esa información habría sido incorrecto construir un mapa de estaciones o comparar directamente los valores de combustibles distintos.

Por eso acoté la pregunta que sí podía responder:

Para un tipo de combustible y un período determinados, ¿cómo evolucionan la mediana de los precios publicados y la cantidad de observaciones disponibles?

Esta decisión condicionó toda la aplicación. El usuario elige primero el combustible y después el rango de fechas. Además, FuelScope distingue entre precios reportados y ventas reales: las medianas no están ponderadas por volumen vendido ni indican necesariamente cuánto cuesta abastecerse hoy en una estación.

Primero entendí el CSV; después escribí el dashboard

La fuente es el conjunto de datos abiertos de OSINERGMIN, acompañado por su diccionario y metadatos. El archivo incorporado en el proyecto contiene 497 156 filas y 11 columnas. Seis de esas columnas corresponden a posibles precios de combustibles.

Algunos campos clave son:

Campo Cómo lo utilizo
fe_eval Fecha de vigencia tomada como fecha de observación.
g_regular, g_premium, diesel, gnv, glp_g, glp_e Columnas con precios reportados por tipo de combustible.
ANON_CO_LOCAL_VENTA Identificador anonimizado del establecimiento.
fecha_corte Fecha de generación del archivo, distinta de la fecha observada.

Aquí encontré una de las primeras lecciones del proyecto: el nombre de un archivo no garantiza la cobertura temporal de su contenido. Aunque el recurso incluye 20260301 en su nombre, el snapshot procesado abarca 59 días, entre el 1 de enero y el 28 de febrero de 2026.

Después de pasar los datos a formato largo y excluir celdas sin precio, obtuve 1 061 899 observaciones válidas. El resultado tiene más observaciones que filas originales porque un mismo registro puede contener valores para varios combustibles.

También hubo límites que preferí mostrar antes que ocultar. La versión del CSV que utilicé no incluye departamento, provincia, distrito ni coordenadas; por eso no implementé un análisis territorial. Y como el diccionario no especifica las unidades físicas de los precios, la interfaz advierte «unidad no documentada».

Dejé los detalles de esta revisión en la auditoría de datos del repositorio.

Una estructura que pueda mantener sin perderme en app.py

En proyectos pequeños es tentador concentrarlo todo en un archivo. Para FuelScope preferí distribuir las responsabilidades: la entrada de Streamlit por un lado, la lógica de transformación y métricas por otro, y la lectura de datos validados en otro módulo.

FuelScope/
├── app.py                         # Punto de entrada
├── fuelscope/
│   ├── dashboard.py               # Interfaz y visualizaciones
│   ├── data.py                    # Procesamiento y métricas
│   └── snapshot.py                # Lectura y controles de integridad
├── scripts/                       # Ingesta y revisión de la fuente
├── data/raw/                      # Archivos originales y documentación
├── data/processed/                # Parquet, manifiesto y calidad
├── tests/                         # Pruebas automatizadas
├── .github/workflows/             # CI y verificación de datos
├── Dockerfile
└── requirements.txt
Enter fullscreen mode Exit fullscreen mode

El flujo general quedó así:

CSV + diccionario + metadatos
              ↓
     Contrato y validación
              ↓
    Parquet + manifiesto
              ↓
      Streamlit + Plotly
Enter fullscreen mode Exit fullscreen mode

Esta organización evita reprocesar el CSV original cada vez que alguien interactúa con un filtro. La aplicación consume una versión preparada, verifica los checksums del archivo Parquet y del informe de calidad y mantiene separadas la ingesta y la visualización.

La parte menos visible: limpiar sin inventar certezas

La ingesta convierte las seis columnas de combustibles en observaciones individuales, valida las fechas y conserva precios numéricos, finitos y positivos. En el archivo analizado, las exclusiones procedieron de celdas sin precio; no se encontraron precios iguales a cero ni negativos.

También aparecieron valores extremos, como 2 000 y 2 500 en algunos registros de gasolina regular y diésel. En lugar de eliminarlos con un umbral arbitrario, decidí documentarlos para revisión y usar la mediana como indicador principal, ya que es menos sensible a los extremos que la media.

La transformación genera tres componentes: un archivo Parquet para consultar los datos, un manifiesto que identifica la versión y un informe de calidad. Los hashes del archivo de origen y del contrato ayudan a identificar exactamente qué se procesó.

Otro detalle que me pareció importante: el proceso prepara los archivos antes de actualizar el puntero current.json. Si la actualización falla, el dashboard conserva la versión anterior validada en lugar de quedarse sin datos.

El procesamiento también puede ejecutarse manualmente desde el proyecto:

python -m scripts.update_data --csv RUTA.csv --contract data/source_contract.json
Enter fullscreen mode Exit fullscreen mode

En ese comando, RUTA.csv debe sustituirse por la ruta del archivo fuente descargado.

Qué puede explorar alguien en FuelScope

La interfaz está construida con Streamlit y los gráficos con Plotly. Busqué que cada visualización respondiera algo concreto, no solo que ocupara espacio.

El usuario puede seleccionar el combustible, ajustar el período y alternar entre modo claro y oscuro. Las tarjetas muestran precio mediano, número de observaciones, rango intercuartílico y días con datos. Todos estos indicadores se recalculan al aplicar filtros.

El gráfico principal combina dos perspectivas: la mediana diaria de precios y, debajo, el número de reportes por día. Mostrar ambas importa: una variación en la línea no se interpreta del mismo modo si ese día hay muchas o muy pocas observaciones.

Un ejemplo del snapshot es la gasolina o gasohol regular: entre el 1 de enero y el 28 de febrero de 2026 se registraron 256 680 precios válidos, con una mediana de 13,99 en la unidad no documentada por el diccionario. No presento ese valor como un precio vigente ni como una cantidad comparable directamente con GNV o GLP.

Además de los gráficos, la app permite revisar una tabla y exportar los resultados filtrados junto con un archivo de metadatos. Esto ayuda a conservar el contexto de los datos cuando alguien los utiliza fuera del dashboard.

Incorporé también la opción de cargar un CSV propio durante la sesión. En ese caso, quien lo sube debe relacionar las columnas y declarar las unidades; ese archivo no sustituye el snapshot oficial ni recibe automáticamente la misma validación de procedencia.

Docker, pruebas y automatización: llevarlo más allá del prototipo

Quería que el proyecto se pudiera ejecutar y revisar sin depender únicamente de mi computadora. Por eso incorporé pruebas automatizadas y una forma de levantar la aplicación con Docker.

Para ejecutarlo directamente con Python:

git clone https://github.com/Kiara1616/FuelScope.git
cd FuelScope
python -m venv .venv
Enter fullscreen mode Exit fullscreen mode

Después de activar el entorno virtual —\.venv\Scripts\Activate.ps1 en PowerShell o source .venv/bin/activate en Linux/macOS—:

python -m pip install -r requirements-dev.txt
streamlit run app.py
Enter fullscreen mode Exit fullscreen mode

También se puede iniciar con Docker Compose:

docker compose up --build
Enter fullscreen mode Exit fullscreen mode

En ambos casos, la aplicación local se consulta en http://localhost:8501. Docker facilita reproducir el entorno y comprobar el arranque; el hosting público, en cambio, utiliza Streamlit Community Cloud.

En GitHub Actions separé dos responsabilidades:

1. Revisar el código. En pushes y pull requests, el workflow de integración continua ejecuta Ruff, pytest, compilación de Python, pip-audit y una prueba de arranque con Docker. Esa comprobación también verifica que el snapshot integrado tenga 1 061 899 observaciones válidas.

2. Revisar la fuente de datos. Un segundo workflow consulta semanalmente las URLs directas conocidas del CSV, el diccionario y los metadatos. Si no hay cambios, no genera un commit. Si detecta nuevos archivos, valida esquema, cobertura, volumen e integridad antes de actualizar el snapshot.

Revisión semanal
       ↓
Descargar recursos conocidos
       ↓
Verificar documentos y datos
       ↓
¿Hay un cambio válido?
  ├── No → conservar la versión actual
  └── Sí → procesar y publicar una nueva versión
Enter fullscreen mode Exit fullscreen mode

En una ejecución manual, el resultado fue no_change: los recursos consultados eran los mismos. Una comprobación semanal no significa que los precios se actualicen cada semana, ni que este pipeline obtenga los precios actuales de Facilito. Si el organismo publica un recurso bajo otra URL o con una estructura diferente, habrá que auditarlo antes de incorporarlo.

Del repositorio a una aplicación pública

FuelScope está desplegado en Streamlit Community Cloud, conectado al repositorio público, a la rama main y al archivo app.py.

El proceso que sigo para cambios de código es:

Cambio de código → Pull request → GitHub Actions → Merge a main
                                                      ↓
                                            Streamlit actualiza la app
Enter fullscreen mode Exit fullscreen mode

Es importante distinguir las dos automatizaciones: GitHub Actions verifica, mientras Streamlit Community Cloud actualiza la aplicación al detectar cambios en la rama conectada. Ejecutar pruebas no bloquea por sí solo el despliegue si no se configuran las protecciones correspondientes en la rama.

Lo que aprendí y lo que todavía mejoraría

La lección principal no tuvo que ver con un framework: un dashboard es tan confiable como las definiciones y límites de los datos que muestra.

En este proyecto aprendí a diferenciar la fecha del archivo de la fecha observada, a no asumir unidades que el diccionario no documenta y a acompañar cada tendencia con su cobertura de datos. También confirmé que modularizar la aplicación y automatizar pruebas evita que todo dependa de revisar los gráficos manualmente.

Si continuara ampliando FuelScope, mis siguientes pasos serían incorporar nuevos recursos oficiales, siempre después de comprobar su compatibilidad, y buscar una fuente que documente tanto las unidades como la ubicación de los establecimientos. Solo entonces tendría sentido añadir comparaciones territoriales o mapas respaldados por datos.

Para cerrar

FuelScope comenzó como una exploración de precios históricos y terminó siendo una oportunidad para trabajar el flujo completo: datos abiertos → validación → transformación → visualización → pruebas → despliegue.

Más que encontrar una cifra llamativa, mi objetivo fue construir una aplicación donde el usuario pueda entender qué está viendo, de qué fechas son los datos y qué conclusiones no debería sacar.

Si te interesa revisar el código o probar los filtros, puedes visitar el repositorio y la aplicación pública. También me interesaría conocer cómo abordarías la actualización de una fuente pública cuyos recursos pueden cambiar con el tiempo.

Fuente y atribución:
Datos publicados por OSINERGMIN en la Plataforma Nacional de Datos Abiertos, según la licencia indicada en el catálogo (Open Data Commons Attribution License). FuelScope es un proyecto independiente y no implica respaldo de OSINERGMIN.

Top comments (0)