Caso de estudio · Proyecto personal
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.
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.
| Aspecto | Antes (manual) | Con el organizador |
|---|---|---|
| Criterio de agrupación | Ninguno, a criterio de quien arma la planilla | Cercanía geográfica + horario |
| Tiempo de armado | Manual, todos los días | Automático, en segundos |
| Capacidad del vehículo | Se controla a ojo | Restricción dura del algoritmo |
| Trazabilidad | Papel | Base de datos + planilla exportable |
El sistema se piensa como un recorrido de datos, con una única geocodificación por empleado:
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.
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.
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 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.
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.
Un único radio fijo o deja gente sin viaje o fuerza desvíos innecesarios. Separar "casos densos" de "casos de rescate" evita ambos problemas.
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.
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.
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).