¡Enhorabuena! He ganado el 1er lugar en la categoría Spanish del FP20 Analytics Challenge 2025 🏆. Este reconocimiento no solo refuerza mi pasión por las operaciones logísticas, análisis de datos y la visualización con Power BI, sino que también refleja el esfuerzo detrás de cada modelo, cada fórmula DAX y cada historia contada a través de los datos. Competir con profesionales de todo el mundo ha sido un desafío inspirador, y hoy celebro este logro como un impulso para seguir innovando en el emocionante mundo del análisis de datos..
Ver Publicación en Linkedin¿Cuáles son las rutas de envío más comunes y sus distancias promedio?
¿Qué rutas tienen los tiempos de entrega más largos?
¿Cuáles son las fechas de reserva y entrega con mayor actividad?
¿Qué factores contribuyen a los retrasos en los envíos?
¿Qué clientes reciben la mayoría de los envíos y experimentan retrasos?
¿Qué proveedores manejan la mayoría de los envíos y algunos tienen mayores retrasos?
¿Cuáles son los materiales que se envían con más frecuencia y ciertos materiales tienen tiempos de entrega más largos?
¿Dónde están los retrasos de envío más comunes según los datos de GPS?
'db_logistica'
# Importar librerías
import pandas as pd
import matplotlib.pyplot as plt
from math import radians, sin, cos, sqrt, atan2
# Cargar el archivo Excel
file_path = 'Transportation & Logistics Tracking Dataset_Spanish.xlsx'
df = pd.read_excel(file_path, sheet_name='Datos Primarios')
# Exploración inicial de los datos
df.head()
df.info()
# Verificar valores nulos, NA y celdas vacías
null_values = df.isnull().sum()
na_values = df.isna().sum()
empty_values = (df == '').sum()
# Mostrar el resumen de valores nulos, NA y celdas vacías
print("Valores nulos por columna:")
print(null_values)
print("\nValores NA por columna:")
print(na_values)
print("\nCeldas vacías por columna:")
print(empty_values)
Al detectar campos con datatype incorresctos y valores vacíos como nulos procederé a modificarlos.
# Convertir formatos de fecha
df['ETA Real'] = pd.to_datetime(df['ETA Real'], errors='coerce')
df['ETA Planificada'] = pd.to_datetime(df['ETA Planificada'], errors='coerce')
df['Fecha de Inicio del Viaje'] = pd.to_datetime(df['Fecha de Inicio del Viaje'], errors='coerce')
df['Fecha de Fin del Viaje'] = pd.to_datetime(df['Fecha de Fin del Viaje'], errors='coerce')
#Eliminamos registros sin Proveedor GPS
df = df.dropna(subset=['Proveedor GPS'])
#Cambiamos los valores nulos con la mediana de la columna.
df['Kilómetros Mínimos a Cubrir por Día'] = df['Kilómetros Mínimos a Cubrir por Día'].fillna(df['Kilómetros Mínimos a Cubrir por Día'].median())
#Cambiamos los valores nulos con la moda de la columna.
df['ETA Real'] = df['ETA Real'].fillna(df['ETA Real'].mode()[0])
#Cambiamos los valores nulos con la cadena 'Falta Información'
df['Tipo de Vehículo'] = df['Tipo de Vehículo'].fillna('Falta Información')
#Cambiamos los valores nulos con la cadena 'Falta Información'
df['Nombre del Conductor'] = df['Nombre del Conductor'].fillna('Falta Información')
#Cambiamos los valores nulos con la cadena 'Falta Información'
df['Número de Móvil del Conductor'] = df['Número de Móvil del Conductor'].fillna('Falta Información')
# Segunda verificación de valores nulos, NA y celdas vacías
null_values = df.isnull().sum()
na_values = df.isna().sum()
empty_values = (df == '').sum()
# Mostrar el resumen de valores nulos, NA y celdas vacías
print("Valores nulos por columna:")
print(null_values)
print("\nValores NA por columna:")
print(na_values)
print("\nCeldas vacías por columna:")
print(empty_values)
# Exportamos el DataFrame limpio a un archivo Excel
df.to_excel('db_Transportation_&_Logistics_Tracking.xlsx', index=False, sheet_name='Datos Limpios')
print("Archivo exportado exitosamente como 'db_Transportation_&_Logistics_Tracking.xlsx'")
Importamos archivo limpio, previamente manipulado. Iremos a Power Query para transformar algunos de sus campos.
En PowerQuery, fui a la opción 'Transformar' > 'Columna de texto' > 'Dividir columna' > 'Dividir columna por Delimitador' > Separamos por ',', esto nos permitira reconocer el nombre de la compañia o el nombre de la bodega, luego tendremos la ubicación específica del distrito en donde se encuentra esa bodega y por último tendremos el estado del país indio.
Una vez divididas los campos de Ubicación de Origen y Destino, eliminamos los espacios, para evitar cualquier tipo de error.
A continuación procederé a separar columna con formato de Fecha/Hora a Fecha y Hora por separado para trabajar de mejor manera los análisis de tiempo
Campos separados. Utilizando dividir columna por separador 'Espacio'
Para facilitar el análisis temporal, creé una tabla de calendario (Tabla Calendario
) que contiene fechas iterativas basadas en el rango de fechas de la tabla db_logistica
. Luego, agregué columnas adicionales para desglosar la fecha en componentes útiles como día, mes, año, semestre y semana.
Tabla Calendario = CALENDAR(
MIN(db_logistica[Fecha de Reserva]),
MAX(db_logistica[Fecha de Reserva]))
Columnas adicionales:
Año = YEAR('Tabla Calendario'[Date])
Mes = MONTH('Tabla Calendario'[Date])
Día = DAY('Tabla Calendario'[Date])
Mes Nombre = FORMAT('Tabla Calendario'[Date], "mmmm")
Día Nombre = FORMAT('Tabla Calendario'[Date], "dddd")
Semestre = IF(MONTH([Date]) <= 6, 1, 2)
Día Numérico = WEEKDAY('Tabla Calendario'[Date], 2)
db_logistica
Creé una tabla de medidas (Tabla Medidas
) para calcular métricas claves relacionadas con las Reservas, Transporte y Niveles de Servicio.
Total ID Reserva = COUNT(db_logistica[ID de Reserva])
Total Market = CALCULATE(
COUNTROWS(db_logistica),
db_logistica[Tipo de Envío] = "Market")
Total Regular = CALCULATE(
COUNTROWS(db_logistica),
db_logistica[Tipo de Envío] = "Regular")
SUM Distancia de Transporte (KM) = SUM(db_logistica[Distancia de Transporte (KM)])
Entrega Anticipada = CALCULATE(
COUNTROWS(db_logistica), db_logistica[Estado_Entrega] = "Entrega Anticipada")
Entrega Mismo Día = CALCULATE(
COUNTROWS(db_logistica), db_logistica[Estado_Entrega]="Entrega mismo día")
Entrega Fuera de Plazo = CALCULATE(
COUNTROWS(db_logistica), db_logistica[Estado_Entrega] = "Entrega fuera de plazo")
AVG Días de Entrega = AVERAGE(db_logistica[Días_ETA_Real_vs_Planificada])
AVG Distancia por Ruta =
AVERAGEX(
db_logistica,
db_logistica[Distancia de Transporte (KM)])
AVG_Días_Reserva =
AVERAGE(db_logistica[Días_Reserva_ETA])
AVG_Envios_Dias =
AVERAGEX(
db_logistica,
DATEDIFF(
db_logistica[Fecha de Reserva] + db_logistica[Hora de Reserva],
db_logistica[Fecha ETA Real] + db_logistica[Hora ETA Real],
DAY
)
)
Tiempo de Entrega desde Reserva =
VAR Diferencia = DATEDIFF( db_logistica[Fecha de Reserva] + db_logistica[Hora de Reserva],
db_logistica[Fecha ETA Real] + db_logistica[Hora ETA Real], SECOND)
VAR Dias = QUOTIENT(Diferencia, 86400) // 86400 segundos en un día
VAR Horas = QUOTIENT( MOD(Diferencia, 86400), 3600 ) // 3600 segundos en una hora
VAR Minutos = QUOTIENT( MOD(Diferencia, 3600), 60 ) // 60 segundos en un minuto
RETURN
FORMAT(Dias, "0") & " días, " & FORMAT(Horas, "0") & " horas, " & FORMAT(Minutos, "0") & " minutos"
Estado_Entrega =
VAR FechaPlanificada = db_logistica[Fecha ETA Planificada]
VAR FechaETAReal = db_logistica[Fecha ETA Real]
RETURN
IF(
FechaETAReal = FechaPlanificada,
"Entrega mismo día",
IF(
FechaETAReal > FechaPlanificada,
"Entrega fuera de plazo",
"Entrega Anticipada"
)
)
Rango_Horas_ETA_Real =
" " & FORMAT(TIME(HOUR(db_logistica[Hora ETA Real]), 0, 0), "hh:mm") & " - " &
FORMAT(TIME(HOUR(db_logistica[Hora ETA Real]) + 1, 0, 0), "hh:mm")
Para complementar el análisis, creé una segunda tabla de medidas (Tabla %
) que calcula porcentajes basados en las métricas anteriores. Esto permite entender la proporción de Reservas en cada estado respecto al total.
% Total Regular = DIVIDE([Total Regular],[Total ID Reserva])
% Total Market = DIVIDE([Total Market], [Total ID Reserva])
% Entrega mismo día = DIVIDE([Entrega Mismo Día],[Total ID Reserva])
% Entrega fuera de plazo = DIVIDE([Entrega Fuera de Plazo],[Total ID Reserva])
% Entrega Anticipada = DIVIDE([Entrega Anticipada],[Total ID Reserva])
El modelo de datos lo conectamos con nuestra Tabla Calendario para analizar los datos de la tabla db_logistica
en base al tiempo.
Aquí mostraré algunas imagenes de los paneles creados a partir de las de las medidas creadas anteriormente, pero los paneles finales los mostraré una vez que los alla personalizado con FIGMA para finalizar con una Historytelling con todo lo visto.
Análisis de Reservas .
Aquí mostraré los los diseños que he creado con figma para crear un impacto visual mas potente de los dashboard que he creado. Los dashboard y paneles de mando estaran en la siguiente publicación.
Descripción inicial de la sección CON FIGMA.
El análisis de reservas revela una evolución significativa en la cantidad de solicitudes, con un crecimiento exponencial del 1.768% de 2019 a 2020. Este incremento refleja una mayor demanda y expansión operativa, pero también destaca la importancia de optimizar los procesos logísticos para garantizar que este crecimiento sea sostenible. La gran mayoría de las reservas provienen de clientes regulares, lo que subraya la necesidad de fidelizar a este segmento y garantizar que su experiencia sea eficiente y confiable. Sin embargo, un punto crítico identificado es el cumplimiento en los tiempos de entrega, ya que más del 61% de las reservas se entregaron fuera de plazo. Esto indica un déficit operativo que podría afectar la percepción del servicio y la retención de clientes a largo plazo.
El análisis del transporte muestra un aumento significativo en la utilización de camiones, pasando de 146 en 2019 a 1.188 en 2020, en respuesta al crecimiento de la demanda. No obstante, la eficiencia operativa aún presenta desafíos, ya que en 2019 el 100% de las entregas fueron fuera de plazo, y aunque en 2020 hubo una mejora relativa con un 33% de entregas puntuales, el 56% aún sufrió retrasos. Esto refleja la necesidad de optimizar las rutas y mejorar la planificación de la flota para garantizar una distribución más eficiente. Además, se evidenció que los camiones de 32 y 40 pies tienden a presentar más retrasos, lo que sugiere que es necesario evaluar si el tipo de vehículo utilizado es el más adecuado para ciertas rutas. También se identificaron horarios críticos de retraso, especialmente los lunes, martes y jueves en franjas horarias específicas, lo que permite replantear la programación de despachos para evitar congestionamientos.
El análisis operativo revela que la eficiencia en la entrega sigue siendo un desafío crítico, con un alto porcentaje de retrasos en algunos conductores y ciertos materiales mostrando un desempeño deficiente. La falta de datos sobre puntualidad en muchas entregas dificulta una evaluación precisa de la operación y limita la capacidad de aplicar mejoras efectivas.