El Índice de Condición de Pavimento (PCI por sus siglas en inglés), es una clasificación numérica con rango de 0 a 100, siendo 0 la peor condición posible y 100 la mejor condición posible (ASTM, 2007) cuya asignación se basa en el procedimiento establecido por la norma ASTM D6433-07 “Standard Practice for Roads and Parking Lots Pavement Condition Index Surveys”.
El PCI se utiliza como una medida cuasi-cuantitativa de la condición del pavimento basada en diferentes daños observados (agrietamiento, agrietamiento de bloque, depresiones, parches, entre otros) y su nivel de severidad (bajo, medio, alto) que son indicadores de la integridad estructural y condición operacional de la superficie de pavimento. Sin embargo, el PCI no se debe emplear como una medida de la capacidad estructural de la carretera.
La estructura de pavimento se deteriora con el tiempo hasta alcanzar su vida de diseño, como se ve en la curva principal de la imagen. Este deterioro puede ser acelerado por factores externos (aumento del volumen de tráfico, inclemencias climáticas, intervención a causa de otras estructuras, entre otros); pero también puede ser mitigado por actividades de mantenimiento (bacheo, sello de grietas, recarpeteo, entre otros).
En la imagen se observa que si el mantenimiento preventivo se efectúa cuando el PCI es alto, puede ‘subir’ la curva en pequeños espacios y así garantizar un ahorro a futuro, mientras que si se efectúan esas actividades cuando ya la curva está en una categoría baja, el costo de reparación de la carretera, puede multiplicarse sustancialmente.
Por consiguiente, las entidades públicas gestoras de las carreteras suelen contratar firmas de ingeniería para analizar su condición, con el fin de administrar los fondos disponibles eficientemente. Usualmente, estas firmas utilizan el PCI para esta labor, debido a que es un análisis rápido, barato y confiable.
El dataset que se utiliza en el siguiente documento proviene de un estudio realizado por la municipalidad de Chappel Hill en Carolina del Norte, Estados Unidos; que contrata una firma de ingeniería que realiza este estudio para todas las calles de la comunidad. Hasta el momento lo ha realizado en los años 2007, 2009, 2012, 2014, 2016 y 2018.
La firma a su vez, cada año realiza un análisis con el que presenta a la municipalidad, hasta 4 actividades de mantenimiento (en orden de prioridad) y su costo asociado para cada una de las carreteras, así como la prioridad que posee cada una de arreglarse. Y con base en este informe, la municipalidad puede determinar la gestión de inversión de sus fondos destinados a las carreteras.
El PCI tiene una categorización con base en el valor numérico. A pesar de que la norma establece una, la municipalidad en estudio contiene su propia escala, por lo cual es la que se utiliza en el proyecto (Chappel Hill Open Data, 2018).
| Clasificación | PCI |
|---|---|
| Excelente | 91 - 100 |
| Bueno | 81 - 90 |
| Aceptable | 66 - 80 |
| Pobre | 51 - 65 |
| Muy Pobre | 0 - 50 |
El dataset en su forma original presentaba algunos problemas ya que, los datos del año 2018 tenían diferentes discrepancias con respecto a los de los otros años, por ejemplo:
Realizar esta limpieza por medio de programación era un poco complicado, por lo que se decidió realizar una limpieza manual para corregir los problemas, cambiando los nombres y corrigiéndolos con los datos de los otros años.
Durante el proyecto se utilizaron los siguientes paquetes de R:
library(tidyverse)
library(ggthemes)
library(plotly)
library(GGally)
library(ggpubr)
library(ggrepel)
library(gridExtra)
library(VIM)
library(leaflet)
library(leaflet.extras)
library(htmltools)
library(DT)
library(reshape)
La única caraterística especial del dataset es la ingestión de los na’s. La función read.csv reconoce los na’s como “NA” por defecto, pero en el dataset se introdujeron como espacios en blanco, por lo que na.strings = "".
pavement_ChapelHill <- read.csv("Pavement_New.csv", na.strings = "", header = T)
Para una previsualización de los na’s en el dataset, se utilizó la función aggr del paquete VIM. Este gráfico se subdivide en 3 gráficos para su interpretación:
Gráfico 1. Visualización na’s
Para mayor facilidad en el tratamiento de los datos, se utiliza el paquete tidyverse, por lo que el dataset se convierte al formato de dataframe tibble.
as_tibble(pavement_ChapelHill)
El dataset contiene variables que no son útiles para el análisis que se quiere realizar, debido a que son referenciales para la ubicación visual del inspector o porque son variables que no podrían ser tratadas por métodos de remplazos de na’s, ya que no son parte de un muestreo sino que son características del lugar, y por tanto no se pueden suponer. Por estas razones se deciden eliminar.
#Columnas para eliminar
del_col <- c("Street.Direction", "Block.Number", "Street.Beginning.Description",
"Street.Ending.Description", "Curb.and.Gutter.Location",
"Curb.Type", "Bike.Lanes", "Left.Sidewalk",
"Right.Sidewalk", "One.Way", "Bus.Route", "Slippage")
pavement_ChapelHill[del_col] <- NULL
Las variables de distancia están en el sistema de unidades inglés (ft), por lo que se convierte a unidades del sistema internacional (m) multiplicando por el valor de conversión. También se crea la columna de área (m2) de la carretera.
\[L_m = L_{ft} * 0.3048\]
\[A_{m^2} = L_m * W_m\]pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
Length = round(Length * 0.3048, 2),
Pavement.Width = round(Pavement.Width * 0.3048, 2),
Area_m2 = round(Length * Pavement.Width, 2),
)
El head del dataset muestra que nombres de columnas tienen símbolos o poseen nombres extensos. Entonces se renombran las columnas para las operaciones más adelante.
names(pavement_ChapelHill)[1] <- "Year_data"
names(pavement_ChapelHill)[3] <- "Traffic_Volume"
names(pavement_ChapelHill)[7] <- "A_No_alligator"
names(pavement_ChapelHill)[8] <- "A_L_alligator"
names(pavement_ChapelHill)[9] <- "A_M_alligator"
names(pavement_ChapelHill)[10] <- "A_S_alligator"
names(pavement_ChapelHill)[10] <- "A_S_alligator"
names(pavement_ChapelHill)[18] <- "Utility_cuts"
names(pavement_ChapelHill)[20] <- "PCI"
Las columnas referentes al agrietamiento (alligator) muestran el porcentaje del área total de la carretera con este daño y su severidad (sin daño, daño leve, daño moderado y daño severo), por consiguiente, la suma de estos debe ser igual 100%.
Sin embargo, el dataset posee 605 filas donde por error, en lugar de poner 100 en la columna A_No_alligator se colocó 10, por lo que la suma del 100% no se cumple y suma solo 10%, como se ejemplifica en las dos primeras filas de la tabla, en comparación con las dos últimas.
Por este error, se decide cambiar estos datos a 100 y a su vez, pasar estos números en términos del área total de la carretera, para poder tener el total de área dañada y su severidad.
\(A_{alli} = A_{m^2} * (porc. / 100)\)
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
sum_porc = A_No_alligator + A_L_alligator + A_M_alligator + A_S_alligator,
A_No_alligator = ifelse(sum_porc == 10, 100, A_No_alligator),
A_No_alligator = Area_m2 * A_No_alligator/100,
A_L_alligator = Area_m2 * A_L_alligator/100,
A_M_alligator = Area_m2 * A_M_alligator/100,
A_S_alligator = Area_m2 * A_S_alligator/100
) %>%
select(
-sum_porc
)
Con el fin de homogeneizar las variables en las columnas Traffic_Volumen (tiene nombre largo y símbolos inadecuados), Ride.Quality (se colocan las tres categorías que establece la norma y se corrigen algunos nombres mal introducidos) y Priority (mejor legibilidad).
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
Traffic_Volume = case_when(grepl("High", Traffic_Volume) ~ "High",
grepl("Low", Traffic_Volume) ~ "Low"),
Ride.Quality = case_when(grepl("Average", Ride.Quality) ~ "Good",
grepl("Low", Ride.Quality) ~ "Good",
grepl("Slightly", Ride.Quality) ~ "Poor",
grepl("S", fixed = T, Ride.Quality) ~ "Poor",
grepl("Moderate", Ride.Quality) ~ "Fair"),
Priority = case_when(grepl("HIGH", Priority) ~ "High",
grepl("LOW", Priority) ~ "Low",
grepl("ME", Priority) ~ "Medium",
grepl("NO", Priority) ~ "No Priority")
)
El siguiente paso es la categorización del PCI con base en la tabla utilizada por la municipalidad de Chappel Hill para clasificar el estado general de la carretera.
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
Rating_PCI = case_when(
PCI <= 100 & PCI >= 91 ~ "Excellent",
PCI <= 90 & PCI >= 81 ~ "Good",
PCI <= 80 & PCI >= 66 ~ "Fair",
PCI <= 65 & PCI >= 51 ~ "Poor",
PCI <= 50 ~ "Very Poor"
)
)
Las columnas de mantenimiento fueron tratadas de diferente forma, ya que poseían diversos errores a considerar previo al análisis.
En primera instancia se tienen 4 columnas con las mismas categorías (actividad de mantenimiento) y 4 columnas con el costo asociado, por lo que hacer una categorización es difícil al estar los mismos datos separados en otras variables y, combinados con na’s.
Las actividades escritas de diferentes formas, por ejemplo: ‘Skin Patch’ está como también ‘Skin Patching’ o también ‘Full Depth Patch’ se puede encontrar como ‘Full-Depth Patch’; por lo que en un análisis no se contarían como la misma variable.
Otro aclaración, es que las columnas Total.Maintenance.Cost y Cost.Per.Mile están al revés, es decir el costo total de los mantenimiento (el cual es la suma de los 4 costos de las actividades) está en la columna de Cost.Per.Mile. Esto puede ser comprobado con la cuarta fila, que posee dos actividades de mantenimiento y cuya longitud es 2528 ft:
\(C_{total} = 22450 + 7319 = 29769\)
\(C/mill = C_{total} / (L_{ft} * 0.000189394) = 29769/0.4788=62171\)
Y por último, algunas observaciones que cuentan con solo una actividad de mantenimiento no tiene el costo en la columna de Cost.of.Primary.Maintenance, sino que solo está en la columna de Cost.Per.Mile (ver fila 12).
El primer paso para mejorar la trabajabilidad con estas columnas, es eliminar la variable Total.Maintenance, ya que anteriormente se cambiaron las unidades al SI, por lo que que tener un costo por millas no es necesario. Luego, se procede a estandarizar los nombres de las actividades en cada una de las columnas. La variable Maintenance.Activity tiene los nombres de todas las actividades.
unique(pavement_ChapelHill$Maintenance.Activity)
## [1] <NA>
## [2] Full Depth Patch
## [3] Skin Patch
## [4] 1 in Plant Mix Resurfacing
## [5] Full-Depth Patch
## [6] CP
## [7] Skin Patching
## [8] 1" Plant Mix Resurfacing
## [9] 1 in Plant Mix Resurfacing and BST Seal
## [10] Crack Sealing
## [11] 2" Plant Mix Resurfacing
## [12] SO
## [13] Short Overlay
## [14] 1" Plant Mix Resurfacing+S
## 13 Levels: 1 in Plant Mix Resurfacing ...
Una vez identificados las 7 actividades de mantenimiento (Full Depth Patch, Skin Patch, 1" Plant Mix Resurfacing, 2" Plant Mix Resurfacing, CP, Crack Sealing y Short Overlay), se corrigen los nombres.
pavement_ChapelHill["Total.Maintenance.Cost"] <- NULL
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
Maintenance.Activity = case_when(grepl("Full", Maintenance.Activity) ~ "Full Depth Patch",
grepl("Skin", Maintenance.Activity) ~ "Skin Patch",
grepl("1", Maintenance.Activity) ~ '1" Plant Mix Resurfacing',
grepl("CP", Maintenance.Activity) ~ "CP",
grepl("2", Maintenance.Activity) ~ '2" Plant Mix Resurfacing',
grepl("Crack", Maintenance.Activity) ~ "Crack Sealing",
grepl("Short", Maintenance.Activity) ~ "Short Overlay",
grepl("SO", fixed = T, Maintenance.Activity) ~ "Short Overlay"),
Secondary.Maintenance = case_when(grepl("Full", Secondary.Maintenance) ~ "Full Depth Patch",
grepl("Skin", Secondary.Maintenance) ~ "Skin Patch",
grepl("Crack", Secondary.Maintenance) ~ "Crack Sealing"),
Tertiary.Maintenance = case_when(grepl("Skin", Tertiary.Maintenance) ~ "Skin Patch",
grepl("Crack", Tertiary.Maintenance) ~ "Crack Sealing"),
Quaternary.Maintenance = case_when(grepl("Crack", Quaternary.Maintenance) ~ "Crack Sealing")
)
Para calcular el costo por kilómetro, se determina el costo total de los mantenimientos de la carretera. Las operaciones matemáticas no se pueden efectuar si en la columna existen NA’s, entonces para las 4 columnas de costos, se remplaza estos valores por 0.
Luego se calcula el costo total por unidad (suma de las 4 columnas de costos) como una variable temporal. No obstante, se debe tomar en cuenta el error que tenía la columna de Cost.of.Primary.Maintenance, así que esta suma se compara con el valor de Cost.Per.Mile y se conserva el valor más alto y se crea la columna Total_Maintenance.
Una vez con el costo total establecido, se evalúa el costo por kilómetro, con la longitud dividida entre 1000, para poder convertir la longitud de metros a kilómetros.
Por último, para corregir los costos en Cost.of.Primary.Maintenance, se remplaza el valor con el Total_Maintenance, comprobando siempre que exista una actividad de mantenimiento en Maintenance.Activity. Cuando ya se tiene todo completo, se puede eliminar la columna temporal y Cost.Per.Mile, puesto que los valores ya están almacenado en Total_Maintenance.
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
#reemplazo de na's
Cost.of.Primary.Maintenance = ifelse(is.na(Cost.of.Primary.Maintenance),
0, Cost.of.Primary.Maintenance),
Secondary.Maintenance.Cost = ifelse(is.na(Secondary.Maintenance.Cost),
0, Secondary.Maintenance.Cost),
Tertiary.Maintenance.Cost = ifelse(is.na(Tertiary.Maintenance.Cost),
0, Tertiary.Maintenance.Cost),
Quaternary.Maintenance.Cost = ifelse(is.na(Quaternary.Maintenance.Cost),
0, Quaternary.Maintenance.Cost),
#sumatoria de costos
Total_Maintenance_unit = Cost.of.Primary.Maintenance + Secondary.Maintenance.Cost +
Tertiary.Maintenance.Cost + Quaternary.Maintenance.Cost,
#comparación con el costo total
Total_Maintenance = ifelse(Cost.Per.Mile >= Total_Maintenance_unit, Cost.Per.Mile,
Total_Maintenance_unit),
#costo por km
Cost_maint_km = round(Total_Maintenance / (Length / 1000), 2),
#corregión de valores de costo primario
Cost.of.Primary.Maintenance = ifelse(!is.na(Maintenance.Activity) & Cost.of.Primary.Maintenance == 0,
Total_Maintenance, Cost.of.Primary.Maintenance)
) %>%
#eliminación de columnas repetidas
select(
-c(Total_Maintenance_unit, Cost.Per.Mile)
)
Con los cambios hasta el momento, ya se tienen las columnas con un formato más uniforme tanto en la actividad como en su costo, pero el problema sobre el análisis aún está presente.
Por lo tanto, para unificar en todas las carreteras, tanto la actividad de mantenimiento como el costo de la misma, se decidió que cada actividad de mantenimiento sea una variable independiente que contenga el costo para cada carretera.
Entonces por ejemplo, en la fila 6, se va a tener en la columna denominada 1" Plant Mix Resurfacing el valor de 30900 y en la de Full Depth Patch tendrá 23348; pero en las columnas Skin Patch, 2" Plant Mix Resurfacing, CP, Short Overlay, y Crack Sealing será de 0.
Cuando se crean las columnas de actividades, las 4 de mantenimiento y las 4 de costos son redundantes, por lo cual se eliminan del dataset.
pavement_ChapelHill <- pavement_ChapelHill %>%
mutate(
Full_Depth_Patch = case_when(
Maintenance.Activity == "Full Depth Patch" ~ Cost.of.Primary.Maintenance,
Secondary.Maintenance == "Full Depth Patch" ~ Secondary.Maintenance.Cost,
TRUE ~ 0
),
Skin_Patch = case_when(
Maintenance.Activity == "Skin Patch" ~ Cost.of.Primary.Maintenance,
Secondary.Maintenance == "Skin Patch" ~ Secondary.Maintenance.Cost,
Tertiary.Maintenance == "Skin Patch" ~ Tertiary.Maintenance.Cost,
TRUE ~ 0
),
Plant_Mix_Resurfacing_1in = case_when(
Maintenance.Activity == '1" Plant Mix Resurfacing' ~ Cost.of.Primary.Maintenance,
TRUE ~ 0
),
Plant_Mix_Resurfacing_2in = case_when(
Maintenance.Activity == '2" Plant Mix Resurfacing' ~ Cost.of.Primary.Maintenance,
TRUE ~ 0
),
CP = case_when(
Maintenance.Activity == "CP" ~ Cost.of.Primary.Maintenance,
TRUE ~ 0
),
Short_overlay = case_when(
Maintenance.Activity == "Short Overlay" ~ Cost.of.Primary.Maintenance,
TRUE ~ 0
),
Crack_sealing = case_when(
Maintenance.Activity == "Crack Sealing" ~ Cost.of.Primary.Maintenance,
Secondary.Maintenance == "Crack Sealing" ~ Secondary.Maintenance.Cost,
Tertiary.Maintenance == "Crack Sealing" ~ Tertiary.Maintenance.Cost,
Quaternary.Maintenance == "Crack Sealing" ~ Quaternary.Maintenance.Cost,
TRUE ~ 0
)
) %>%
select(
-c(Maintenance.Activity, Cost.of.Primary.Maintenance,
Secondary.Maintenance, Secondary.Maintenance.Cost,
Tertiary.Maintenance, Tertiary.Maintenance.Cost,
Quaternary.Maintenance, Quaternary.Maintenance.Cost)
)
Por último, las coordenadas de las carreteras están en una sola columna, entonces se separan en dos columnas para latitud y longitud.
pavement_ChapelHill <- pavement_ChapelHill %>%
separate(
map_coords, into = c("Latitude", "Longitude"), sep = ", "
)
Finalmente, el dataset queda de la siguiente manera:
Y se puede observar la diferencia con respecto al Gráfico 1. Se observa que ya no hay combinaciones, debido a que se eliminaron todos los NA’s del dataset.
Gráfico 2. Visualización na’s
Algunas columnas de las que se crearon o se renombraron sus filas no se convirtieron en tipo factor, por lo que se realiza esta conversión para poder trabajar mejor en los gráficos.
options(digits = 9)
pavement_ChapelHill$Year_data <- as.factor(pavement_ChapelHill$Year_data)
pavement_ChapelHill$Traffic_Volume <- as.factor(pavement_ChapelHill$Traffic_Volume)
pavement_ChapelHill$Ride.Quality <- factor(pavement_ChapelHill$Ride.Quality,
levels = c("Good", "Fair", "Poor"))
pavement_ChapelHill$Priority <- factor(pavement_ChapelHill$Priority,
levels = c("No Priority", "Low", "Medium", "High"))
pavement_ChapelHill$Rating_PCI <- factor(pavement_ChapelHill$Rating_PCI,
levels = c("Excellent", "Good",
"Fair", "Poor", "Very Poor"))
pavement_ChapelHill$Number.of.Travel.Lanes <- as.factor(pavement_ChapelHill$Number.of.Travel.Lanes)
pavement_ChapelHill$Latitude <- as.numeric(pavement_ChapelHill$Latitude)
pavement_ChapelHill$Longitude <- as.numeric(pavement_ChapelHill$Longitude)
La tabla a continuación muestra las estadísticas básicas (media, mediana, máximo y mínimo) de PCI, Total_Maintenance y Cost_maint_km agrupado por los años de recolección de datos.
El Gráfico 3 es una matriz de gráficos exploratorios, agrupados por años. En este se relacionan 5 variables: PCI, Area_m2, Rating_PCI, Total_Maintenance y Cost_maint_km.
Gráfico 3. Gráficos exploratorios
El Gráfico 4 muestra un boxplot/scartterplot del PCI en el tiempo en función del volumen de tráfico.
Gráfico 4. PCI por año en función de volumen de tráfico
El Gráfico 5 muestra la relación entre el área inspeccionada y el costo de mantenimiento total, identificado por la clasificación de PCI.
Gráfico 5. Área inspeccionada en función de costo total
El Gráfico 6 muestra la relación entre el valor de PCI y el costo por kilómetro, clasificado por la prioridad de intervención o de mantenimiento de la carretera.
Gráfico 6. Costo de mantenimiento por km en función del PCI
Para finalizar con el estudio del costo del mantenimiento, el Gráfico 7 contiene dos gráficos con las mismas variables: costo de las actividades de mantenimiento y clasificación de PCI.
Skin Patch como medida de mantenimiento preventivo.Full Depth Patch es la actividad con mayor inversión.Full Depth Patch, donde su costo promedio se multiplica 4 veces entre el estado bueno y el crítico.Resurfacing 2in tiene el costo promedio más alto de todas las actividades por lo que solo se utiliza para estados muy críticos.Gráfico 7. Costo total y promedio por actividad de mantenimiento para la clasificación de PCI
Para analizar la cantidad de área agrietada y su severidad, se hizo el Gráfico 8; a través de los años.
Crack Sealing es una actividad en la que no se invierte tanto (entre 890 y 2000 dólares en promedio), por lo que puede ser una de las causas de que hayan un incremento tan desmesurado de la severidad de agrietamiento.Gráfico 8. Estado de agrietamiento en área total inspeccionada por año
Gráfico 9. Mapa de calor de daños