Caso de estudio · Proyecto personal

Organizador Logístico de Traslados

Una app en Python y Streamlit que reemplaza el armado manual de la planilla diaria de transporte de una empresa: agrupa empleados por cercanía geográfica y horario, y asigna choferes respetando la capacidad de cada vehículo — con el objetivo de reducir kilómetros recorridos, combustible y tiempo operativo.

Python Streamlit SQLite scikit-learn · DBSCAN Geopy / Nominatim

El problema

En una empresa, cada día los empleados informan por una encuesta de WhatsApp a qué hora se retiran al día siguiente. Alguien tiene que anotar esa información a mano y armar los grupos de transporte, decidiendo qué chofer lleva a quién, sin ningún criterio sistemático de cercanía.

Ese proceso manual es lento, propenso a errores y — lo más relevante para el negocio — no optimiza el recorrido: si dos empleados que viven a 15 km de distancia terminan en el mismo viaje por una asignación al azar, ese desvío se traduce directamente en más kilómetros, más combustible y más tiempo de viaje para todos los que van en ese vehículo.

AspectoAntes (manual)Con el organizador
Criterio de agrupaciónNinguno, a criterio de quien arma la planillaCercanía geográfica + horario
Tiempo de armadoManual, todos los díasAutomático, en segundos
Capacidad del vehículoSe controla a ojoRestricción dura del algoritmo
TrazabilidadPapelBase de datos + planilla exportable

Cómo funciona

El sistema se piensa como un recorrido de datos, con una única geocodificación por empleado:

01
Encuesta WhatsApp
Horario informado por cada empleado
02
Carga a Excel
Plantilla de empleados y choferes
03
Geocodificación
Nominatim: dirección → lat/lon
04
SQLite
Se cachean coordenadas y distancias
05
Asignación
Algoritmo en dos pasadas
06
Planilla
Exportable en CSV

La geocodificación es el paso más costoso (una consulta a la API por dirección, con límite de frecuencia), así que se ejecuta una única vez por empleado y el resultado se guarda para no repetirla — el tiempo de geocodificación crece O(n) con la cantidad de empleados nuevos, no con el total de la base.

Enfoque analítico: explorar antes de asignar

Antes de escribir el algoritmo de asignación, exploré la distribución geográfica de los empleados con DBSCAN, para entender si existían zonas de alta concentración y qué casos quedaban aislados.

Distribución geográfica de empleados coloreada por distancia a la planta
Ejemplo ilustrativo (datos sintéticos) — cada punto representa un empleado, el color indica distancia a planta.
Agrupamiento de empleados con DBSCAN, coloreado por cluster
Ejemplo ilustrativo (datos sintéticos) — los puntos grises marcados con "x" son los que DBSCAN no pudo agrupar.

Elegí DBSCAN en vez de K-means porque no hace falta definir de antemano cuántos grupos existen, y porque distingue empleados aislados ("ruido") de zonas densas. Ese concepto de "ruido" terminó siendo clave para diseñar el algoritmo de asignación: los casos aislados que DBSCAN no agrupa son, en esencia, los mismos casos que después resuelve la segunda pasada de rescate.

El algoritmo de asignación

El clustering exploratorio no alcanza para asignar choferes: no sabe nada de la capacidad de cada vehículo ni de que dos empleados del mismo cluster pueden tener horarios distintos. Por eso el algoritmo productivo (realizar_asignacion en src/asignaciones.py) usa una estrategia greedy en dos pasadas:

Construí una app completa en Streamlit con 5 pantallas (configuración de la planta, base de empleados, base de choferes, alta individual de personal y el panel de cálculo de viajes del día), y migré la persistencia de Supabase a un módulo propio en SQLite (src/db.py), con las operaciones de sincronización masiva envueltas en transacciones — el proyecto quedó sin credenciales externas, se puede clonar el repo y correrlo directo.

Decisiones técnicas

¿Por qué DBSCAN y no K-means?

No hace falta definir de antemano cuántos grupos existen, y distingue empleados aislados de zonas densas — justo el caso que después resuelve la segunda pasada del algoritmo.

¿Por qué dos pasadas y no un solo cálculo?

Un único radio fijo o deja gente sin viaje o fuerza desvíos innecesarios. Separar "casos densos" de "casos de rescate" evita ambos problemas.

¿Por qué SQLite en vez de Supabase?

Para un proyecto de portfolio, depender de una cuenta y credenciales externas es fricción innecesaria. SQLite mantiene toda la lógica relacional sin pedirle nada al usuario.

¿Por qué distancia geodésica?

Es una aproximación rápida y sin costo de API. El siguiente paso natural es sumar rutas reales (Google Maps u OSRM) para afinar los radios de búsqueda y los KPIs de kilometraje.

Próximos pasos

Código y análisis completo

El repositorio incluye la app de Streamlit y el notebook de análisis exploratorio con DBSCAN (justificación de parámetros, complejidad del proceso de geocodificación y la conexión entre el clustering y el algoritmo de asignación).