-
Notifications
You must be signed in to change notification settings - Fork 0
/
main.qmd
2129 lines (1682 loc) · 83.6 KB
/
main.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
---
title: "Caso de estudio | Iflow"
date: "r Sys.Date()" # Inserta la fecha automáticamente
format: html
theme: darkly
toc: true
toc-location: left
number-sections: true
code-fold: true # Permitir plegado de bloques de código
code-tools: true # Mostrar herramientas de copiado en los bloques de código
smooth-scroll: true # Habilitar desplazamiento suave al navegar por el índice
fig-align: center # Alinear las figuras al centro
toc-depth: 3 # Profundidad máxima del índice
lang: es # Establecer idioma del documento
editor: visual # Editor visual habilitado
---
```{r,warning=FALSE,echo=FALSE, output=FALSE}
# Limpieza de datos
library(tidyverse)
library(dplyr)
library(skimr)
library(lubridate)
# Analisis exploratorio
library(DataExplorer)
library(inspectdf)
library(plotly)
library(viridis)
# Análisis de series temporales
library(tseries)
library(forecast)
# Clustering
library(factoextra)
# Claves de APIs
mapbox_token <- "pk.eyJ1IjoibG9yZW5uem8iLCJhIjoiY20xcHYyd3g2MDk0bTJxb2k4YWZvOHlmcSJ9.r4E2pcTSM89NNHBFSmvKHw"
# Cargamos los datos
data <- read_csv("data/raw_iflow_data.csv", show_col_types = FALSE)
```
![](images/banner.png)
# Introducción
En el marco del **Primer Desafío Internacional de la Red Latinoamericana de Ciencia de Datos**, este análisis tiene como propósito abordar un problema práctico del ámbito logístico. El proyecto promueve la colaboración entre estudiantes de diversas universidades latinoamericanas, fomentando el trabajo en equipo y la toma de decisiones basadas en datos reales.
El **objeto de estudio** es un conjunto de datos proporcionado por **iFlow**, una empresa argentina especializada en logística integral, con operaciones tanto nacionales como internacionales dentro del MERCOSUR. iFlow se dedica a la gestión y co-gerencia de cadenas de abastecimiento para sus clientes, buscando optimizar procesos y mejorar la eficiencia operativa.
Este análisis tiene como objetivo:
1. **Comprender y describir** las principales características del conjunto de datos, que incluye registros de entregas realizadas en un período de tres meses.
2. **Identificar patrones y tendencias** que permitan obtener insights relevantes sobre las operaciones de iFlow.
3. **Detectar posibles inconsistencias o errores** en la base de datos, propias de un entorno operativo real, para evaluarlas e integrarlas al análisis.
# Limpieza de datos.
La primera etapa de este análisis consistió en la **limpieza de datos**. Este proceso de limpieza fue clave para preparar los datos para un análisis exploratorio robusto y la generación de insights confiables sobre la operación logística de iFlow.
En primer lugar realizamos algunos cambios para **facilitar el trabajo** con los datos.
- Tratar Columnas innecesarias
La columna InicioVisitaPlanificado y FinVisitaPlanificado contienen los mismos valores por lo que las unificamos en una nueva columna.
```{r,warning=FALSE,echo=FALSE}
data <- data %>%
# Nueva columna para almacenar el horario planificado
mutate(visita_planificada = InicioVisitaPlanificado) %>%
# Eliminamos InicioVisitaPlanificado y FinVisitaPlanificado
dplyr::select(-InicioVisitaPlanificado, -FinVisitaPlanificado)
```
- Formatear correctamente las variables
```{r,warning=FALSE,echo=FALSE}
# Convertir columnas correspondientes a formato de fecha y hora
data$InicioVisitaReal <- as.POSIXct(data$InicioVisitaReal,
format="%Y-%m-%d %H:%M:%OS")
data$FinVisitaReal <- as.POSIXct(data$FinVisitaReal,
format="%Y-%m-%d %H:%M:%OS")
data$visita_planificada <- as.POSIXct(data$visita_planificada,
format="%Y-%m-%d %H:%M:%OS")
# Las columnas InicioHorario1, FinHorario1, las pasamos a caracter para categorizarlas facilmente.
data$InicioHorario1 <- as.character(data$InicioHorario1)
data$FinHorario1 <- as.character(data$FinHorario1)
# Pasamos variables categóricas a factores.
data$cliente <- as.factor(data$cliente)
```
- Renombrar las columnas por nombres intuitivos.
```{r ,warning=FALSE,echo=FALSE}
# Renombrar columnas específicas con dplyr
data <- data %>%
rename( id_orden = iddomicilioorden,
inicio_horario = InicioHorario1,
fin_horario = FinHorario1,
bultos = Bultos,
peso = Peso,
unidades = Unidades,
inicio_visita = InicioVisitaReal,
fin_visita = FinVisitaReal)
# Reorganizar columnas.
data <- data %>%
dplyr::select(id_orden, cliente, localidad, direccion, latitud, longitud,
bultos, unidades, peso, inicio_horario, fin_horario, visita_planificada, inicio_visita, fin_visita)
```
Con estos cambios realizados pasamos a modificaciones y **arreglos necesarios** para un análisis correcto de los datos.
- Eliminación de filas duplicadas.
```{r,warning=FALSE,echo=FALSE}
data <- data %>%
distinct()
```
- Arreglo de coordenadas faltantes en algunas entregas.
El dato de coordenadas en algunas filas estaba vacio o indicaba "0". En algunos de estos casos pudimos rellenar estas coordenadas con datos existentes del domicilio (21 filas). En caso de que esto no sea posible las filas fueron eliminadas (19 filas) y no serán tomadas en cuenta para el análisis.
```{r,warning=FALSE,echo=FALSE}
# Filtrar las filas donde latitud o longitud son NA
cordenadas_vacias <- data %>%
filter(
is.na(latitud) | is.na(longitud) | latitud == 0 | longitud == 0
)
# cordenadas_vacias # dim 43 x 14
# Filtrar las observaciones donde id_orden está en cordenadas_vacias
observaciones_id_orden <- data %>%
filter(id_orden %in% cordenadas_vacias$id_orden) %>%
group_by(id_orden) %>%
summarise(count = n())
# Mostrar el resultado
# observaciones_id_orden
# Contar las apariciones de cada id_orden en cordenadas_vacias
apariciones_cordenadas_vacias <- cordenadas_vacias %>%
group_by(id_orden) %>%
summarise(na_count = n())
# Unir las tablas por id_orden
resultado <- observaciones_id_orden %>%
left_join(apariciones_cordenadas_vacias, by = "id_orden") %>%
# Si no hay coincidencias en cordenadas_vacias, establecer na_count en 0
mutate(na_count = ifelse(is.na(na_count), 0, na_count)) %>%
# Restar las apariciones de cordenadas_vacias del total
mutate(count_diff = count - na_count) %>%
# Filtrar solo los id_orden donde count_diff es mayor a 0
filter(count_diff > 0)
# Mostrar el resultado
#resultado
```
```{r,warning=FALSE,echo=FALSE}
# Definir la función que revisa y sobrescribe latitud y longitud
reparar_lat_long <- function(dataset, ids) {
# Iterar sobre cada id de la lista
for (id in ids) {
# Filtrar las observaciones válidas de latitud y longitud para este id_orden
observaciones_validas <- dataset %>%
filter(id_orden == id & !is.na(latitud) & !is.na(longitud) & latitud != 0 & longitud != 0)
# Si existen observaciones válidas, tomar la primera ocurrencia
if (nrow(observaciones_validas) > 0) {
latitud_valida <- observaciones_validas$latitud[1]
longitud_valida <- observaciones_validas$longitud[1]
# Sobrescribir las observaciones con latitud o longitud nulos o 0
dataset <- dataset %>%
mutate(
latitud = ifelse(id_orden == id & (is.na(latitud) | latitud == 0), latitud_valida, latitud),
longitud = ifelse(id_orden == id & (is.na(longitud) | longitud == 0), longitud_valida, longitud)
)
}
}
# Retornar el dataset reparado
return(dataset)
}
```
```{r,warning=FALSE,echo=FALSE}
# Ejecutar la función usando los id_orden de la columna resultado
ids_a_reparar <- resultado$id_orden
# Aplicar la función a data
data <- reparar_lat_long(data, ids_a_reparar)
```
Por último creamos algunas nuevas columnas para distintos análisis. Entre estas algunas columnas para facilitar la interacción con fechas y horarios de entregas.
```{r,warning=FALSE,echo=FALSE, output=FALSE}
# Asegurar que los días se generen en español
Sys.setlocale("LC_TIME", "es_ES.UTF-8")
# Crear la columna 'dia_str' con normalización de caracteres
data <- data %>%
mutate(
dia = as.integer(format(fin_visita, "%d")),
mes = as.integer(format(fin_visita, "%m")),
hora = as.integer(format(fin_visita, "%H")),
diferencia_minutos = as.numeric(
difftime(fin_visita, visita_planificada, units = "mins")),
dia_str = tolower(iconv(weekdays(fin_visita, abbreviate = FALSE),
to = "UTF-8")),
duracion_visita_min = as.numeric(
difftime(fin_visita, inicio_visita, units = "mins")),
duracion_visita_horas = as.numeric(
difftime(fin_visita, inicio_visita, units = "hours"))
)
# Guardamos los datos limpios
# write.csv(x = data, file = "iflow_clean.csv")
```
```{r,warning=FALSE,echo=FALSE}
data <- read.csv("data/clean_iflow_data.csv")
```
# Vista general.
En esta sección ofrecemos una **visión superficial de los datos**, brindando un panorama inicial que permite familiarizarnos con su estructura y contenido.
```{r,warning=FALSE,echo=FALSE, output=FALSE}
# Cuantas entregas tenemos en total?
dim(data)
dim(data %>% filter(cliente==20))
dim(data %>% filter(cliente==70))
```
![](images/clipboard-2661057119.png)
Se registraron 27.419 entregas de dos clientes distintos: 16,545 del cliente 20 y 10.874 del cliente 70.
Todas estas entregas fueron realizadas entre el 3 de mayo y 8 de agosto de 2024, con un promedio de 9.005 entregas por mes
```{r,warning=FALSE,echo=FALSE, output=FALSE}
# Ensure fin_visita is in the correct POSIXct format
data$fin_visita <- as.POSIXct(data$fin_visita, format = "%Y-%m-%d %H:%M:%OS")
# Filter the row with the maximum fin_visita
data %>%
filter(fin_visita == min(fin_visita, na.rm = TRUE))
data %>%
filter(fin_visita == max(fin_visita, na.rm = TRUE))
```
![](images/timelien.png)
::: panel-tabset
## Entregas por cliente
```{r,warning=FALSE,echo=FALSE}
# Crear una columna con el primer día del mes correspondiente
data <- data %>%
mutate(mes = as.Date(floor_date(fin_visita, "month"))) # Asegurar que 'mes' sea Date
# Agrupar por mes y contar la cantidad de entregas
entregas_por_mes <- data %>%
group_by(mes, cliente) %>%
summarise(n = n(), .groups = "drop")
entregas_por_mes$cliente <- as.factor(entregas_por_mes$cliente)
# Crear el gráfico de barras
ggplot(entregas_por_mes, aes(x = mes, y = n, fill=cliente)) +
geom_bar(stat = "identity", position="dodge") +
scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") +
labs(title = "Cantidad de Entregas por Mes por cliente",
x = "Mes",
y = "Número de Entregas") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
```
Este gráfico de barras muestra la cantidad de entregas realizadas cada mes, separadas por cliente. Las barras están agrupadas por mes y se distinguen por colores para cada cliente.
La cantidad de entregas es constante entre mayo y julio, con algunas variaciones entre los clientes. Esto indica un comportamiento predecible en la demanda mensual. Planificar recursos en función de estos patrones puede mejorar la eficiencia operativa.
## Entregas por mes
```{r,warning=FALSE,echo=FALSE}
# Crear una columna con el primer día del mes correspondiente
data <- data %>%
mutate(mes = as.Date(floor_date(fin_visita, "month"))) # Asegurar que 'mes' sea Date
# Agrupar por mes y contar la cantidad de entregas
entregas_por_mes <- data %>%
group_by(mes) %>%
summarise(n = n(), .groups = "drop")
# Crear el gráfico de barras
ggplot(entregas_por_mes, aes(x = mes, y = n)) +
geom_bar(stat = "identity", fill = "#94C11F") +
scale_x_date(date_labels = "%b %Y", date_breaks = "1 month") +
labs(title = "Cantidad de Entregas por Mes",
x = "Mes",
y = "Número de Entregas") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1))
```
En este gráfico se visualiza la cantidad total de entregas realizadas por mes, sin importar el cliente.
n patrón estable en el número de entregas permite predecir la demanda mensual. Las caídas pueden ser analizadas más adelante para identificar feriados o problemas en la operación.
:::
## ¿Cuales son los horarios de entrega? {#horarios-entrega}
```{r, warning=FALSE, echo=FALSE}
# Agrupar los datos por día y hora para contar ocurrencias
resumen <- data %>%
group_by(dia_str, hora) %>%
summarise(n = n(), .groups = "drop") %>%
ungroup()
# Asegurar el orden correcto de los días (Lunes a Domingo)
resumen$dia_str <- factor(resumen$dia_str,
levels = c("lunes", "martes", "miércoles",
"jueves", "viernes", "sábado", "domingo"))
resumen <- resumen %>% filter(!is.na(dia_str), dia_str != "domingo")
ggplot(resumen, aes(x = hora, y = dia_str, fill = n)) +
geom_tile(color = "white") +
scale_fill_viridis(option = "C", direction = 1) +
labs(title = "Entregas por día y hora",
x = "Hora del día",
y = "Día de la semana",
fill = "Cantidad") +
theme_minimal()
```
Este gráfico de calor muestra la cantidad de entregas realizadas según la hora del día y el día de la semana. Cada celda del gráfico representa un cruce entre una hora y un día, con colores más intensos indicando una mayor cantidad de entregas.
Mayor actividad: El miércoles alrededor de las 15:00 es el momento con más entregas. Patrón semanal: Hay menos actividad los fines de semana, especialmente los domingos, donde casi no se registran entregas.
```{r,warning=FALSE,echo=FALSE}
# Convertir la columna dia_str en un factor ordenado
data <- data %>%
mutate(dia_str = factor(dia_str,
levels = c("lunes", "martes", "miércoles",
"jueves", "viernes", "sábado","domingo")))
# Agrupar por el nombre del día y contar las entregas
entregas_por_dia <- data %>%
group_by(dia_str) %>%
summarise(n = n())
# Crear el gráfico de barras
ggplot(entregas_por_dia, aes(x = dia_str, y = n)) +
geom_bar(stat = "identity", fill = "#94C11F") +
labs(title = "Cantidad de Entregas por Día de la Semana",
x = "Día de la Semana",
y = "Número de Entregas") +
theme_minimal()
```
Vemos que el domingo hay solo 3 entregas. Esto podría deberse a un error por lo que las revisamos.
```{r,warning=FALSE,echo=FALSE, output=FALSE}
data %>% filter(dia_str == "domingo")
```
| Orden | Localidad | Fecha y hora de entrega |
|--------|-----------------|-------------------------|
| 81943 | CAPITAL FEDERAL | 2024-07-21 23:51:00 |
| 100968 | CAPITAL FEDERAL | 2024-07-21 23:51:00 |
| 100968 | CAPITAL FEDERAL | 2024-07-21 23:51:00 |
Al revisar estas entregas encontramos que las tres fueron entregadas a las 23:51. Esto podría deberse a un error en la carga de datos o a entregas especiales. Al ser las últimas entregas del día puede deberse a un cierre automático del sistema despues de haber dejado entregas inciadas el día anterior sin marcar su finalización.
Si bien este parece ser un caso puntual existen dos grandes problemas respecto a los horarios y duración de las entregas. Estos problemas son.
1. **Duración de las entregas:** Muchas entregas tienen el mismo horario de inicio y fin. Esto puede deberse a errores en la carga de datos o a un sistema de carga poco eficiente.
2. **Entregas consecutivas:** Muchas entregas consecutivas fueron cargadas con el horario de la entrega anterior. Esto dificulta la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.
## Errores de carga en horarios de entrega.
Notamos que en las entregas muy cercanas geograficamente, en la misma cuadra, suelen tener el mismo horario de finalización. Esto se puede deber a que los operarios olvidan hacer la carga individual o consideran mas rapido completar ambas entregas antes de registrarlo en el sistema.
Junto con los errores de carga en los horarios de entrega este puede ser un segundo indicador de que el sistema de carga puede ser mejorado para no recolectar datos erroneos en el futuro.
Horarios cargados de forma incorrecta podrian causar:
1. Mala estimación sobre tiempos muertos.
2. Dificulta optimizar los procesos de entrega.
3. Perjudica la proyección de horarios de entregas o ventanas horarias.
Algunas sugerencias e ideas para mejorar esto incluyen:
- Mejoras de la interfaz en el sistema de carga para facilitar y fomentar su uso.
- Implementación de un sistema de validación de los datos para evitar duplicados.
- Desarrollo y uso de hardware específico para la carga de datos.
> ¿Cuál es el tiempo promedio entre el inicio y fin de las visitas de entrega?
```{r, warning=FALSE, echo=FALSE}
# dim(data %>% filter(inicio_visita != fin_visita)) # 17142
# data %>% filter(inicio_visita == fin_visita) # 10255
# Calcular los totales y proporciones
total <- nrow(data) # Total de filas
dif_visit <- nrow(data %>% filter(inicio_visita != fin_visita)) # 17142 filas diferentes
igual_visit <- nrow(data %>% filter(inicio_visita == fin_visita)) # Filas iguales
# Crear un dataframe con los resultados
resumen <- data.frame(
Categoria = c("Inicio ≠ Fin", "Inicio = Fin"),
Conteo = c(dif_visit, igual_visit)
)
# Calcular el porcentaje para cada categoría
resumen$Porcentaje <- round((resumen$Conteo / total) * 100, 2)
# Crear el gráfico de barras
ggplot(resumen, aes(x = Categoria, y = Conteo, fill = Categoria)) +
geom_bar(stat = "identity", width = 0.6) +
geom_text(aes(label = paste0(Porcentaje, "%")),
vjust = -0.5, size = 5) + # Etiquetas con porcentaje arriba de las barras
labs(title = "Comparación de visitas",
x = "Categoría",
y = "Cantidad de visitas") +
theme_minimal()
```
Bien cargados 17142 vs mal cargados 10255 mal. Pero encontramos que son incluso más. Una forma de solucionarlo podría haber sido ordenar las entregas en orden cronologico e intentar estimar la duración real segun el tiempo entre las entregas y la distancia entre ellas pero al intentar esto encontramos que muchas entregas consecutivas arrastran errores. Dificultando la identificación de rutas, estimación de tiempos muertos y duraciones reales entre entregas.
```{r, warning=FALSE, echo=FALSE, output=FALSE}
# Ordenar los datos cronológicamente (asumo que tienes una columna de timestamp)
data_ordenada <- data %>%
arrange(inicio_visita)
# Crear una columna que identifique si el fin_visita es igual al de la fila anterior
data_secuencia <- data_ordenada %>%
mutate(
consecutivo = (fin_visita == lag(fin_visita, default = first(fin_visita)))
)
# Crear un identificador para cada grupo consecutivo con el mismo fin_visita
data_secuencia <- data_secuencia %>%
mutate(
grupo = cumsum(!consecutivo) # Incrementar grupo cuando cambia fin_visita
)
# Resumir el número de filas en cada grupo consecutivo
resumen_secuencias <- data_secuencia %>%
group_by(fin_visita, grupo) %>%
summarise(cantidad = n(), .groups = "drop")
# Mostrar las secuencias más largas
resumen_secuencias %>%
arrange(desc(cantidad))
filtered_amounts <- resumen_secuencias %>% filter(cantidad != 1)
```
Como referencia, el día 2024-06-27 16:06:00 hay 23 entregas graficadas el mismo día.
::: panel-tabset
## Ejemplo 50 entregas
```{r, warning=FALSE, echo=FALSE}
#Cargar librerías necesarias
library(leaflet)
library(dplyr)
# Filtrar las entregas según el fin_visita elegido
fin_visita_elegido <- "2024-06-12 16:12:00" # Cambia este valor por el deseado
entregas_filtradas <- data %>%
filter(fin_visita == fin_visita_elegido)
# Verificar si hay datos para graficar
if (nrow(entregas_filtradas) == 0) {
print("No hay entregas con el fin_visita seleccionado.")
} else {
# Crear el mapa interactivo con Leaflet
leaflet(data = entregas_filtradas) %>%
addTiles() %>% # Añadir un mapa base (OpenStreetMap)
addCircleMarkers(
lng = ~longitud, lat = ~latitud, # Coordenadas
radius = 6, color = "blue", stroke = FALSE,
fillOpacity = 0.8, fillColor = "red", # Estilo de los marcadores
label = ~paste("Lat:", latitud, "<br>Lng:", longitud), # Etiquetas al pasar el mouse
popup = ~paste0("Entrega en: ", latitud, ", ", longitud) # Popup al hacer clic
)
}
```
## Ejemplo 23 entregas
```{r, warning=FALSE, echo=FALSE}
#Cargar librerías necesarias
library(leaflet)
library(dplyr)
# Filtrar las entregas según el fin_visita elegido
fin_visita_elegido <- "2024-06-27 16:06:00" # Cambia este valor por el deseado
entregas_filtradas <- data %>%
filter(fin_visita == fin_visita_elegido)
# Verificar si hay datos para graficar
if (nrow(entregas_filtradas) == 0) {
print("No hay entregas con el fin_visita seleccionado.")
} else {
# Crear el mapa interactivo con Leaflet
leaflet(data = entregas_filtradas) %>%
addTiles() %>% # Añadir un mapa base (OpenStreetMap)
addCircleMarkers(
lng = ~longitud, lat = ~latitud, # Coordenadas
radius = 6, color = "blue", stroke = FALSE,
fillOpacity = 0.8, fillColor = "red", # Estilo de los marcadores
label = ~paste("Lat:", latitud, "<br>Lng:", longitud), # Etiquetas al pasar el mouse
popup = ~paste0("Entrega en: ", latitud, ", ", longitud) # Popup al hacer clic
)
}
```
:::
¿Que tan frecuente es este error? Muy frecuentes
```{r, warning=FALSE, echo=FALSE}
# Asegurarse de que no haya NAs en la columna 'cantidad'
resumen_secuencias <- resumen_secuencias %>%
filter(!is.na(cantidad), cantidad != 1)
# Crear la columna 'categoria' con las condiciones bien definidas
resumen_secuencias <- resumen_secuencias %>%
mutate(
categoria = case_when(
cantidad == 2 ~ "2",
cantidad >= 3 & cantidad <= 5 ~ "De 3 a 5",
cantidad >= 6 & cantidad <= 10 ~ "De 6 a 10",
cantidad >= 11 & cantidad <= 20 ~ "De 11 a 20",
cantidad > 20 ~ "Más de 20"
)
)
# Verificar si hay NAs en la columna 'categoria'
resumen_secuencias <- resumen_secuencias %>%
filter(!is.na(categoria))
# Crear el gráfico de barras con las categorías corregidas
ggplot(resumen_secuencias, aes(x = categoria)) +
geom_bar(fill = "#94C11F", color = "black", alpha = 0.8) +
labs(
title = "Distribución de Secuencias por Categoría",
x = "Categoría de cantidad",
y = "Frecuencia"
) +
theme_minimal() +
theme(axis.text.x = element_text(angle = 45, hjust = 1)) # Rotar etiquetas del eje X
```
Se registraron **12,799 entregas con horarios incorrectos**, lo que representa el **46.67%** de los datos cargados de forma errónea.
Con casi la mitad de los datos poco confiables, cualquier análisis sobre la **eficiencia operativa** pierde precisión. Esto puede generar errores importantes al evaluar la **duración de las entregas**, los **periodos de actividad** y las **rutas utilizadas**.
Corregir la recopilación de estos datos es una **oportunidad clave** para mejorar el crecimiento de la empresa, optimizar las **ventanas de entrega**, identificar **cuellos de botella** reales y facilitar una toma de decisiones más precisa y efectiva.
> "Lo que no se mide, no se puede mejorar." - Peter F. Drucker
A continuación, se presentan posibles causas y oportunidades para resolver esta situación.
1. Manual de uso y capacitación sobre el sistema [UNIGIS](https://www.unigis.com/ "Sitio web oficial de Unigis")
Basandonos en el [Manual de transportistas - Elementos de seguridad y APPS](https://transportes.iflow21.com/portal/es/kb/articles/manual-de-transportistas-elementos-de-seguridad-y-apps) encontramos la siguiente referencia sobre el uso de la aplicación.
![](images/user_manual.png)
Capacitar mejor al personal con mayor cantidad de recursos, claridad en los instructivos y videos demostrando el uso correcto del sistema [videos demostrando el uso correcto del sistema](https://www.youtube.com/watch?v=JnEHVHhs6V4 "Video grabado por Corporación Aceros Arequipa para su equipo") mejoraría la precision de la carga de los datos en el futuro.
2. Creación de una interfaz personalizada para Unigis.
El error de carga de horarios iguales en entregas consecutivas puede deberse principalmente a la dificultad de uso del sistema o poca practicidad del mismo por la que los transportistas podrían saltear los pasos del instructivo.
Si migrar a un nuevo sistema más moderno es una alternativa muy costosa podrían considerar hacer una inversión en desarrollo frontend para, utilizando la API del sistema actual, puedan tener una interfaz más amena a los transportistas.
[![Referencia del Uso de la API cloud de Unigis](images/postman_reference.png)](https://www.postman.com/irampoldi/unigis/request/8833s18/reportedeviajes)
El desarrollo de una interfaz personalizada para interactuar con su sistema actual podría ser una inversión considerable pero economica contrastando con la posibilidad de un desarrollo personalizado o la migración a un nuevo sistema.
Algunas consideraciones:
- Inversión en equipo e investigación UX para asegurar el uso intuitivo de los transportistas. Es importante entender como es el uso del sistema en la practica.
3. Migrar a un sistema más moderno o diseñar uno a medida para sus necesidades.
Puede ser la opción mas costosa.
Como conclusión mejorar la capacitación del personal, proporcionando más recursos, instructivos claros y videos demostrativos sobre el uso correcto del sistema, ayudaría a aumentar la precisión en la carga de datos. Además, la creación de una interfaz personalizada para Unigis podría resolver errores comunes, como los horarios idénticos en entregas consecutivas, que probablemente ocurren por la complejidad o poca practicidad del sistema, lo que lleva a los transportistas a omitir pasos. Si migrar a un sistema más moderno resulta demasiado costoso, una opción sería invertir en el desarrollo de un frontend más amigable, utilizando la API del sistema actual, para facilitar su uso y mejorar la experiencia de los transportistas.
## Información de bultos y peso.
En promedio, cada entrega contiene:
- **28.40 unidades**
- **5.75 bultos**
- **41.15 kg de peso**
El **peso elevado por pedido** sugiere la presencia de productos voluminosos o de alto consumo, lo que puede aumentar los **costos de transporte** debido a la mayor carga. Esto también tiene implicaciones importantes para la **planificación de rutas**, ya que es necesario optimizar el uso de los vehículos para evitar sobrecargas y cumplir con los límites de capacidad. Además, la gestión eficiente de estos pedidos requiere ajustar los tiempos de entrega, garantizar la disponibilidad de unidades adecuadas y considerar posibles restricciones en ciertas rutas debido al peso total transportado.
::: panel-tabset
## 🔢 Unidades
::: panel-tabset
Este histograma permite ver la concentración de pedidos en ciertos rangos de unidades. Si se observa una mayor densidad en valores específicos, esto podría indicar patrones de demanda estándar entre los clientes.
:::
```{r,warning=FALSE,echo=FALSE}
# Crear el histograma
ggplot(data, aes(x = unidades)) +
geom_histogram(binwidth = 50, fill = "#94C11F", color = "black") +
labs(title = "Histograma de Unidades",
x = "Bultos",
y = "Frecuencia") +
theme_minimal()
```
```{r,warning=FALSE,echo=FALSE}
mean(data$unidades) # Promedio
```
## 📦 Bultos
::: panel-tabset
Este histograma permite identificar el tamaño típico de los pedidos. Una distribución sesgada hacia valores altos sugiere que la mayoría de los pedidos son de varios bultos, lo que puede influir en la planificación de cargas y en la selección de vehículos de transporte.
:::
```{r,warning=FALSE,echo=FALSE}
# Crear el histograma
ggplot(data, aes(x = bultos)) +
geom_histogram(binwidth = 5, fill = "#94C11F", color = "black") +
labs(title = "Histograma de Bultos",
x = "Unidades",
y = "Frecuencia") +
theme_minimal()
```
```{r,warning=FALSE,echo=FALSE}
mean(data$bultos) # Promedio
```
## ⚖️ Peso
::: panel-tabset
Este histograma permite identificar el tamaño típico de los pedidos. Una distribución sesgada hacia valores altos sugiere que la mayoría de los pedidos son de varios bultos, lo que puede influir en la planificación de cargas y en la selección de vehículos de transporte.
:::
```{r,warning=FALSE,echo=FALSE}
# Crear el histograma
ggplot(data, aes(x = peso)) +
geom_histogram(binwidth = 50, fill = "#94C11F", color = "black") +
labs(title = "Histograma de Peso",
x = "Unidades",
y = "Frecuencia") +
theme_minimal()
```
```{r,warning=FALSE,echo=FALSE}
mean(data$peso) # Promedio
```
:::
## Distribución geográfica de las entregas.
```{r,echo=FALSE, output=FALSE}
# Geolocalización
library(sf)
library(raster)
```
::: panel-tabset
## Todas las entregas
```{r,warning=FALSE,echo=FALSE}
data$cliente <- as.factor(data$cliente)
# Graficar las entregas por cliente con colores distintos
plot_ly(
data,
lat = ~latitud,
lon = ~longitud,
type = 'scattermapbox',
mode = 'markers',
color = ~cliente, # Asigna un color distinto por cliente
marker = list(size = 7, opacity = 0.3), # Ajusta el tamaño y la transparencia de los marcadores
text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse
) %>%
layout(
mapbox = list(
accesstoken = mapbox_token,
center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires
zoom = 10, # Nivel de zoom
style = "open-street-map" # Estilo del mapa
),
title = "Mapa de Entregas en Buenos Aires por Cliente",
margin = list(r = 0, t = 0, b = 0, l = 0)
)
```
## Entregas por dia de la semana
```{r,warning=FALSE,echo=FALSE}
# Crear un factor ordenado para los días de la semana
data$dia_str <- factor(data$dia_str,
levels = c("lunes", "martes", "miércoles", "jueves", "viernes", "sábado", "domingo"))
# Crear una lista de días únicos (ahora ya estarán ordenados según el factor)
dias_unicos <- unique(data$dia_str)
# Crear una lista de "frames" para cada día
frames <- lapply(dias_unicos, function(dia) {
list(
name = dia,
data = list(
list(
type = "scattermapbox",
lat = data$latitud[data$dia_str == dia],
lon = data$longitud[data$dia_str == dia],
mode = "markers",
marker = list(size = 7, opacity = 0.5),
text = paste("Cliente:", data$cliente[data$dia_str == dia], "<br>Dirección:", data$direccion[data$dia_str == dia]),
color = data$cliente[data$dia_str == dia]
)
)
)
})
# Graficar las entregas por cliente con un slider para cambiar día a día
plot_ly(
data,
lat = ~latitud,
lon = ~longitud,
type = 'scattermapbox',
mode = 'markers',
color = ~cliente, # Asigna un color distinto por cliente
frame = ~dia_str, # Agregar el día como frame para la animación (ahora en orden)
marker = list(size = 7, opacity = 0.6), # Ajusta el tamaño y la transparencia de los marcadores
text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse
) %>%
layout(
mapbox = list(
accesstoken = mapbox_token,
center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires
zoom = 10, # Nivel de zoom
style = "open-street-map" # Estilo del mapa
),
title = "Mapa de Entregas en Buenos Aires por Cliente",
margin = list(r = 5, t = 25, b = 5, l = 5)
) %>%
animation_opts(
frame = 500, # Duración de cada frame en milisegundos
transition = 0, # Sin transiciones entre frames
redraw = TRUE
) %>%
animation_slider(
currentvalue = list(prefix = "Día: ")
)
```
:::
Como podemos ver este mapa de entregas de la ciudad de Buenos Aires muestra visualmente la dispersión de las entregas en la ciudad. La variedad de colores permite ver qué clientes se agrupan en ciertas áreas. Si ciertos clientes concentran entregas en una región específica, eso puede indicar una demanda localizada o preferencia regional de ciertos productos.
```{r,warning=FALSE,echo=FALSE}
# Crear una lista de fechas únicas extraídas de la columna fin_visita
fechas_unicas <- unique(as.Date(data$fin_visita))
# Graficar las entregas por cliente con un slider para cambiar por fecha
plot_ly(
data,
lat = ~latitud,
lon = ~longitud,
type = 'scattermapbox',
mode = 'markers',
color = ~cliente, # Asigna un color distinto por cliente
frame = ~as.Date(fin_visita), # Agregar la fecha como frame para la animación
marker = list(size = 7, opacity = 0.7), # Ajusta el tamaño y la transparencia de los marcadores
text = ~paste("Cliente:", cliente, "<br>Dirección:", direccion) # Información al pasar el mouse
) %>%
layout(
mapbox = list(
accesstoken = mapbox_token,
center = list(lat = -34.6037, lon = -58.3816), # Coordenadas de Buenos Aires
zoom = 10, # Nivel de zoom
style = "open-street-map" # Estilo del mapa
),
title = "Mapa de Entregas en Buenos Aires por Cliente",
margin = list(r = 0, t = 0, b = 0, l = 0)
) %>%
animation_opts(
frame = 500, # Duración de cada frame en milisegundos
transition = 0, # Sin transiciones entre frames
redraw = TRUE
) %>%
animation_slider(
currentvalue = list(prefix = "Fecha: ")
)
```
En este mapa las áreas con mayor densidad de entregas resaltan visualmente. Estas zonas de alta actividad pueden representar zonas comerciales o residenciales clave. Esto es útil para optimizar las rutas de entrega y asignar más recursos a zonas con alta demanda.
Para verlo de forma más resumida;
::: panel-tabset
## 🏘️ Barrios
En base a este mapa se permite ver cómo se distribuyen las entregas a nivel de barrio. Los barrios con mayor intensidad de color son aquellos con más entregas, lo que indica la importancia de estos barrios en el volumen de pedidos.
```{r,warning=FALSE,echo=FALSE}
# Cargar los barrios desde el archivo GeoJSON
barrios.comp <- st_read("maps/barrios.geojson", quiet = TRUE) # Reemplaza con la ruta correcta
barrios <- barrios.comp[, c("BARRIO", "geometry")]
# Ver los nombres de las columnas del GeoDataFrame de barrios
# Filtrar las entregas con coordenadas válidas y crear un objeto sf
data_sf <- data %>%
filter(!is.na(latitud) & !is.na(longitud)) %>%
st_as_sf(coords = c("longitud", "latitud"), crs = 4326) # Sistema de coordenadas WGS 84
# Unir cada entrega con su barrio correspondiente
entregas_por_barrio <- st_join(data_sf, barrios)
# Agrupar por el campo "BARRIO" y contar el total de entregas
entregas_agrupadas <- entregas_por_barrio %>%
group_by(BARRIO) %>%
summarise(total_entregas = n())
# Unir la información agregada de entregas al GeoDataFrame de barrios
barrios <- barrios %>%
st_join(entregas_agrupadas)
# Rellenar valores NA (barrios sin entregas) con 0
barrios$total_entregas[is.na(barrios$total_entregas)] <- 0
# Crear el mapa con ggplot2
ggplot(data = barrios) +
geom_sf(aes(fill = total_entregas)) + # Colorear según la cantidad de entregas
scale_fill_viridis_c(option = "plasma", na.value = "white") + # Paleta de colores
theme_minimal() +
labs(
title = "Cantidad de Entregas por Barrio en Buenos Aires",
fill = "Entregas"
)
```
## 🏙️ Comunas
Similar al mapa por barrio, pero agrupando entregas por comunas. Las comunas que presentan más entregas destacan como puntos de interés para analizar el impacto logístico y la demanda concentrada.
```{r, warning=FALSE, echo=FALSE}
# 1. Cargar los barrios desde el archivo GeoJSON
barrios.comp <- st_read("maps/barrios.geojson", quiet = TRUE) # Ajusta la ruta según corresponda
# 2. Agrupar los polígonos por "COMUNA"
comunas <- barrios.comp %>%
group_by(COMUNA) %>%
summarise(geometry = st_union(geometry)) # Unir los polígonos por comuna
# Asegurarse de que COMUNA sea texto
comunas$COMUNA <- as.character(comunas$COMUNA)
# 3. Filtrar las entregas con coordenadas válidas y convertirlas a un objeto sf
data_sf <- data %>%
filter(!is.na(latitud) & !is.na(longitud)) %>%
st_as_sf(coords = c("longitud", "latitud"), crs = 4326)
# 4. Asignar cada entrega a su comuna correspondiente usando st_join
entregas_por_comuna <- st_join(data_sf, comunas)
# 5. Agrupar por "COMUNA" y contar el total de entregas
entregas_agrupadas <- entregas_por_comuna %>%
group_by(COMUNA) %>%
summarise(total_entregas = n())
# 6. Unir los datos de entregas agregados al GeoDataFrame de comunas
comunas <- comunas %>%
st_join(entregas_agrupadas)
# 7. Rellenar los valores NA (comunas sin entregas) con 0
comunas$total_entregas[is.na(comunas$total_entregas)] <- 0
# 8. Crear el mapa con ggplot2
ggplot(data = comunas) +
geom_sf(aes(fill = total_entregas)) +
scale_fill_viridis_c(option = "plasma", na.value = "white") +
theme_minimal() +
labs(
title = "Cantidad de Entregas por Comuna en Buenos Aires",
fill = "Entregas"
)
```
## 🌡️ Entregas Individuales
Los puntos en el mapa de Buenos Aires representan ubicaciones de entregas, y las áreas con mayor densidad de entregas aparecen con colores más intensos, permitiendo identificar visualmente las zonas con más actividad de entregas.
```{r, warning=FALSE, echo=FALSE}
# Mapa de calor de entregas
heatmap_data <- data %>%
group_by(latitud, longitud) %>%
summarise(total_entregas = n())
# Graficar un mapa de calor para visualizar las zonas con mayor densidad de entregas
heatmap_plot <- plot_ly(
heatmap_data,
lat = ~latitud,
lon = ~longitud,
z = ~total_entregas,
type = 'densitymapbox',
colorscale = 'Viridis',
radius = 10
) %>%
layout(
mapbox = list(
accesstoken = mapbox_token,
center = list(lat = -34.6037, lon = -58.3816),
zoom = 10,
style = "open-street-map"
),
title = "Mapa de Calor de Entregas en Buenos Aires",
showlegend = FALSE, # Ocultar la leyenda
margin = list(r = 0, t = 60, b = 0, l = 0) # Agregar más espacio en la parte superior
)
# Mostrar el mapa de calor
heatmap_plot
```
:::
### Centro de distribución en Mendoza.
::: panel-tabset
## 📍 Mapa
```{r,warning=FALSE,echo=FALSE}
# Crear un gráfico usando Plotly y Mapbox
fig <- plot_ly(
data = data,
type = 'scattermapbox',
mode = 'markers',
lat = ~latitud,
lon = ~longitud,
marker = list(size = 8, color = 'blue', opacity = 0.7)
)
# Configurar el estilo de Mapbox (puedes cambiar el estilo)
fig <- fig %>%
layout(
mapbox = list(
style = 'carto-positron', # Otros estilos: 'open-street-map', 'stamen-terrain', etc.
zoom = 2, # Nivel de zoom
center = list(lat = mean(data$latitud), lon = mean(data$longitud)) # Centrado en los datos
),
margin = list(t = 0, b = 0, l = 0, r = 0) # Margen para ajustar el espacio del gráfico
)
# Mostrar el gráfico