Indicadores

Cargar librerías

library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(tidyr)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.5
## ✔ ggplot2   3.5.1     ✔ stringr   1.5.1
## ✔ lubridate 1.9.3     ✔ tibble    3.2.1
## ✔ purrr     1.0.2
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag()    masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(lubridate)
library(ggthemes)
library(showtext)
## Loading required package: sysfonts
## Loading required package: showtextdb
library(sysfonts)

Cargar archivos

registros_2022 <- read.csv("data_2022.csv")
registros_2023 <- read.csv("data_2023.csv")
registros_2024 <- read.csv("data_2024.csv")

registros <- rbind(registros_2022, registros_2023, registros_2024)

limpiar la maestra

registros <- registros %>% 
  mutate(
    estacion = case_when(
      Sensor_id == "ANL1"  ~ "Guadalupe",
      Sensor_id == "ANL2"  ~ "San Nicolás",
      Sensor_id == "ANL3"  ~ "Santa Catarina",
      Sensor_id == "ANL4"  ~ "San Pedro",
      Sensor_id == "ANL5"  ~ "Universidad",
      Sensor_id == "ANL6"  ~ "García",
      Sensor_id == "ANL7"  ~ "San Bernabé",
      Sensor_id == "ANL8"  ~ "Cadereyta",
      Sensor_id == "ANL9"  ~ "Escobedo",
      Sensor_id == "ANL10" ~ "Apodaca",
      Sensor_id == "ANL11" ~ "Monterrey (sur)",
      Sensor_id == "ANL12" ~ "Obispado",
      Sensor_id == "ANL13" ~ "Juárez",
      Sensor_id == "ANL15" ~ "Pesquería",
      Sensor_id == "ANL16" ~ "San Juan",
      TRUE ~ NA_character_
    )
  )

Base de prueba

prueba <- registros %>%
  select(estacion, Dia, PM10, PM25, O3, CO, NO1, NO2, NOx, SO2) %>% 
  pivot_longer(cols = PM10:SO2, 
               names_to = "contaminante", 
               values_to = "valor") %>% 
  mutate(
    Dia = ymd_hms(Dia),           # Use ymd_hms instead of dmy_hm
    Dia = Dia + hours(6),
    fecha = as.Date(Dia),        # Extract only the date
    hora = format(Dia, "%H:%M:%S"),  # Extract the time in HH:MM:SS
    valor = as.numeric(valor)     # Convert 'valor' to numeric
  ) %>%
  select(estacion, fecha, hora, contaminante, valor)
## Warning: There was 1 warning in `mutate()`.
## ℹ In argument: `valor = as.numeric(valor)`.
## Caused by warning:
## ! NAs introduced by coercion
# Contar registros válidos (no NA) por fecha y contaminante
conteo_validos <- prueba %>%
  filter(!is.na(valor)) %>%  # Filtra valores no nulos
  group_by(estacion, fecha, contaminante) %>%
  summarise(n_registros = n(), .groups = "drop")  # Cuenta registros válidos

# Unir conteo con la base original y asignar "suficiencia"
prueba <- prueba %>%
  left_join(conteo_validos, by = c("estacion", "fecha", "contaminante")) %>%
  mutate(suficiencia = ifelse(n_registros >= 18, TRUE, FALSE)) %>%
  mutate(cumple_norma = ifelse(
    suficiencia == TRUE, TRUE, TRUE
  )) %>% 
  select(estacion, fecha, hora, contaminante, valor, suficiencia)  # Mantener orden

pm2_5 <- prueba %>% 
  filter(contaminante == "PM25") %>%
  mutate(valor = if_else(valor < 0, NA_real_, valor))
pm10_norma <- prueba %>%
  filter(contaminante == "PM10") %>%   # Trabajamos solo con PM10
  group_by(estacion, fecha) %>%         # Agrupamos por estación y fecha
  summarise(
    media_pm10 = mean(valor, na.rm = TRUE),
    # Suponemos que dentro de cada grupo (día y estación para PM10)
    # el valor de 'suficiencia' es el mismo en todas las filas, así que tomamos el primero.
    suficiencia = first(suficiencia),
    .groups = "drop"
  ) %>%
  mutate(
    # Si no se cumplen los 18 registros, se asigna "ND" sin evaluar el promedio
    cumple_norma = if_else(
      !suficiencia,
      "ND",
      # Si suficiencia es TRUE, se evalúa el promedio:
      if_else(media_pm10 <= 60, "CUMPLE", if_else(media_pm10 > 60, "NO CUMPLE", NA_character_))
    )
  )

# Se une la clasificación calculada para PM10 a la base original 'prueba'
prueba <- prueba %>%
  left_join(
    pm10_norma %>% select(estacion, fecha, cumple_norma_pm10 = cumple_norma),
    by = c("estacion", "fecha")
  ) %>%
  mutate(
    # Asignamos la clasificación solo para PM10; para otros contaminantes se deja NA
    cumple_norma = if_else(contaminante == "PM10", cumple_norma_pm10, NA_character_)
  ) %>%
  select(estacion, fecha, hora, contaminante, valor, suficiencia, cumple_norma)

PM 2.5

pm25_norma <- pm2_5 %>% 
  group_by(estacion, fecha) %>% 
  summarise(
    media_pm25  = mean(valor, na.rm = TRUE),
    maximo_pm25 = max(valor, na.rm = TRUE),
    n_registros = sum(!is.na(valor)),   # Cuenta solo registros válidos
    suficiencia  = first(suficiencia),
    .groups = "drop"
  ) %>% 
  mutate(
    suficiencia = replace_na(suficiencia, FALSE),
    cumple_norma = if_else(
      !suficiencia,
      "ND",
      if_else(media_pm25 <= 25, "CUMPLE", "NO CUMPLE")
    )
  ) %>% 
  mutate(
    AQI_cat = case_when(
      media_pm25 < 25   ~ "Buena",
      media_pm25 < 45   ~ "Aceptable",
      media_pm25 < 79   ~ "Mala",
      media_pm25 < 147  ~ "Muy Mala",
      TRUE              ~ "Extremadamente Mala"
    )
  )
## Warning: There were 2836 warnings in `summarise()`.
## The first warning was:
## ℹ In argument: `maximo_pm25 = max(valor, na.rm = TRUE)`.
## ℹ In group 283: `estacion = "Apodaca"` and `fecha = 2022-10-25`.
## Caused by warning in `max()`:
## ! no non-missing arguments to max; returning -Inf
## ℹ Run `dplyr::last_dplyr_warnings()` to see the 2835 remaining warnings.
write.csv(pm25_norma, "pm25_max_norma.csv", row.names = FALSE)

Visualizaciones

Visualización 1

pm25_norma %>%
  filter(year(fecha) == 2024, cumple_norma == "NO CUMPLE") %>%
  group_by(estacion) %>%
  summarise(dias_sobre_norma = n()) %>%
  ggplot(aes(x = estacion, y = dias_sobre_norma)) +
  # Gráfica de barras en color verde oscuro
  geom_col(fill = "darkgreen") +
  labs(
    title = "Total de días sobre la norma de PM2.5 por estación (2024)",
    x = "Estación",
    y = "Número de días NO CUMPLE"
  ) +
  theme_minimal() +
  # Rotar etiquetas del eje X para que no se sobrepongan
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1)
  )

Visualización 2

pm25_norma %>%
  filter(year(fecha) == 2024, cumple_norma == "CUMPLE") %>%
  group_by(estacion) %>%
  summarise(dias_cumplen_norma = n()) %>%
  ggplot(aes(x = estacion, y = dias_cumplen_norma)) +
  geom_col(fill = "darkgreen") +
  labs(
    title = "Total de días que SÍ cumplen la norma de PM25 por estación (2024)",
    x = "Estación",
    y = "Número de días CUMPLE"
  ) +
  theme_minimal() +
  theme(
    axis.text.x = element_text(angle = 45, hjust = 1)
  )

Visualización HeatMap versión 1

pm25_norma %>%
  # 1. Filtrar sólo los días que NO cumplen la norma en 2024
  filter(year(fecha) == 2024, cumple_norma == "NO CUMPLE") %>% 
  # 2. Crear una columna con el mes (abreviado)
  mutate(mes = month(fecha, label = TRUE, abbr = TRUE)) %>%
  # 3. Agrupar y contar días
  group_by(estacion, mes) %>%
  summarise(dias_no_cumple = n(), .groups = "drop") %>%
  # 4. Convertir 'estacion' a factor ordenado alfabéticamente
  #    y luego se usa rev() para que la primera quede arriba.
  mutate(estacion = factor(estacion, levels = rev(sort(unique(estacion))))) %>%
  # 5. (Opcional) Ordenar los meses de Jan a Dec, si quieres un orden específico
  mutate(mes = factor(mes, 
                      levels = c("Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"))) %>%
  # 6. Crear el heatmap
  ggplot(aes(x = mes, y = estacion, fill = dias_no_cumple)) +
    geom_tile() +
    geom_text(aes(label = dias_no_cumple), color = "black") +
    scale_fill_gradient(low = "white", high = "tomato") +
    labs(
      x = "Mes",
      y = "Estación",
      fill = "Días NO cumple",
      title = "Total de días sobre la norma para el contaminante PM2.5"
    ) +
    theme_minimal() +
    theme(legend.position = "none")

#scale_fill_viridis_c(direction = -1, option = "rocket") +

Visualización HeatMap versión 3

# 1. Calculamos los días NO CUMPLE por estacion y mes
df <- pm25_norma %>%
  filter(year(fecha) == 2024, cumple_norma == "NO CUMPLE") %>% 
  mutate(mes = month(fecha, label = TRUE, abbr = TRUE)) %>%
  group_by(estacion, mes) %>%
  summarise(dias_no_cumple = n(), .groups = "drop")

# 2. Calculamos el total por mes (suma de todas las estaciones)
df_totales_mes <- df %>%
  group_by(estacion) %>%
  summarise(dias_no_cumple = sum(dias_no_cumple)) %>%
  mutate(mes = "Total")  # Etiquetamos la "categoría" de estación como TOTAL

# 3. Unimos
df_final <- bind_rows(df, df_totales_mes)

# 4. Aseguramos que "TOTAL" sea un nivel al final de la variable estacion
df_final$mes <- factor(
  df_final$mes,
  levels = c(levels(df$mes), "Total")
)

df_final$estacion <- factor(
  df_final$estacion,
  levels = rev(sort(unique(df_final$estacion))) 
)

# 5. Graficamos
ggplot(df_final, aes(x = mes, y = estacion, fill = log(dias_no_cumple))) + # transformción logarítmica para mantener 
  # 1. Capa de "tiles" coloreados
  geom_tile() +
  # 2. Capa de texto con la cuenta de días
  geom_text(aes(label = dias_no_cumple), color = "black") +
  # Escala de color (puedes ajustarla a tu gusto)
  scale_fill_viridis_c(direction = -1, option = "rocket", alpha = 0.80) +
  # scale_fill_gradient(low = "white", high = "red") +
  labs(
    x = "Mes",
    y = "Estación",
    fill = "Días NO cumple"
  ) +
  theme_minimal() +
  theme(legend.position = "none")

Función Gráfica

plot_estacion <- function(estacion_param) {
  pm25_norma %>% 
    filter(year(fecha) == 2024) %>%
    filter(suficiencia == TRUE) %>% 
    filter(estacion == estacion_param) %>%
    ggplot(aes(x = fecha, y = media_pm25, fill = AQI_cat)) +
      geom_col() + 
      scale_fill_manual(
        values = c(
          "Buena"              = "green",
          "Aceptable"          = "yellow",
          "Mala"               = "orange",
          "Muy Mala"           = "red",
          "Extremadamente Mala"= "purple"
        )
      ) +
      scale_x_date(
        limits = c(as.Date("2024-01-01"), as.Date("2024-12-31")),
        date_breaks = "1 month", 
        date_labels = "%b"
      ) +
      labs(
        title = paste("Evolución diaria de la calidad del aire (PM2.5) - 2024 -", estacion_param),
        subtitle = "Cada barra representa el valor promedio diario de PM2.5",
        x = "Fecha",
        y = expression(paste("PM"[2.5], " (µg/m³)")),
        fill = "Categoría"
      ) +
      theme_minimal() +
      theme(
        axis.text.x = element_text(angle = 45, hjust = 1),
        legend.position = "none"
      )
}

Gráfica por estación

Apodaca <- plot_estacion("Apodaca")
Cadereyta <- plot_estacion("Cadereyta")
Escobedo <- plot_estacion("Escobedo")
Guadalupe <- plot_estacion("Guadalupe")
Juarez <- plot_estacion("Juárez")  # Asegúrate de que coincida con tu base de datos
Monterrey_sur <- plot_estacion("Monterrey (sur)")
Obispado <- plot_estacion("Obispado")
SanBernabe <- plot_estacion("San Bernabé")
SanNicolas <- plot_estacion("San Nicolás")
SanPedro <- plot_estacion("San Pedro")
SantaCatarina <- plot_estacion("Santa Catarina")
Universidad <- plot_estacion("Universidad")

Matriz de gráficas

# Con patchwork, concatenas usando + y luego indicas layout
#matriz_graficas <-
#  Apodaca + Cadereyta + Escobedo + Guadalupe +
#  Juarez + Monterrey_sur + Obispado + SanBernabe +
#  SanNicolas + SanPedro + SantaCatarina + Universidad +
#  plot_layout(ncol = 2)  # 4 columnas => 3 filas

# Visualizas la matriz:
#matriz_graficas

# Si usaste patchwork y guardaste en 'matriz_graficas'
#ggsave("matriz_estaciones.png", matriz_graficas, width = 16, height = 14, units = "in")

Estadísticas

1) ¿Cuántos días no cumplen con la suficiencia (por estación)?

dias_no_suficiencia_por_estacion <- pm25_norma %>%
  filter(!suficiencia) %>%
  filter(year(fecha) == 2024) %>%
  group_by(estacion) %>%
  summarise(
    dias_no_suficiencia = n(),
    .groups = "drop"
  )

dias_no_suficiencia_por_estacion
## # A tibble: 15 × 2
##    estacion        dias_no_suficiencia
##    <chr>                         <int>
##  1 Apodaca                         164
##  2 Cadereyta                        51
##  3 Escobedo                         26
##  4 García                          169
##  5 Guadalupe                       209
##  6 Juárez                           29
##  7 Monterrey (sur)                  87
##  8 Obispado                        156
##  9 Pesquería                       364
## 10 San Bernabé                     326
## 11 San Juan                        364
## 12 San Nicolás                      61
## 13 San Pedro                        55
## 14 Santa Catarina                  135
## 15 Universidad                      67

2) Promedio de datos recopilados por día (por estación y general)

promedio_datos_dia_estacion <- pm25_norma %>%
  group_by(estacion) %>%
  filter(year(fecha) == 2024) %>% 
  summarise(
    promedio_datos = mean(n_registros, na.rm = TRUE),
    .groups = "drop"
  )

promedio_datos_dia_estacion
## # A tibble: 15 × 2
##    estacion        promedio_datos
##    <chr>                    <dbl>
##  1 Apodaca                 13.7  
##  2 Cadereyta               21.0  
##  3 Escobedo                21.6  
##  4 García                  14.0  
##  5 Guadalupe               12.7  
##  6 Juárez                  21.9  
##  7 Monterrey (sur)         18.7  
##  8 Obispado                14.8  
##  9 Pesquería                0    
## 10 San Bernabé              3.91 
## 11 San Juan                 0.239
## 12 San Nicolás             20.4  
## 13 San Pedro               20.9  
## 14 Santa Catarina          15.9  
## 15 Universidad             20.3
promedio_datos_dia_general <- pm25_norma %>%
  summarise(
    promedio_datos = mean(n_registros, na.rm = TRUE)
  )

promedio_datos_dia_general
## # A tibble: 1 × 1
##   promedio_datos
##            <dbl>
## 1           16.1

3) Estación con el mayor porcentaje de insuficiencia

estacion_mayor_pct_insuficiencia <- pm25_norma %>%
  group_by(estacion) %>%
  filter(year(fecha) == 2024) %>% 
  summarise(
    total_dias = n(),
    dias_no_suficiencia = sum(!suficiencia),
    pct_insuficiencia = 100 * dias_no_suficiencia / total_dias,
    .groups = "drop"
  ) %>%
  arrange(desc(pct_insuficiencia)) %>%
  slice(1)

estacion_mayor_pct_insuficiencia
## # A tibble: 1 × 4
##   estacion  total_dias dias_no_suficiencia pct_insuficiencia
##   <chr>          <int>               <int>             <dbl>
## 1 Pesquería        364                 364               100
top5_insuficiencia <- pm25_norma %>%
  group_by(estacion) %>%
  filter(year(fecha) == 2024) %>% 
  summarise(
    total_dias = n(),
    dias_no_suficiencia = sum(!suficiencia),
    pct_insuficiencia = 100 * dias_no_suficiencia / total_dias,
    .groups = "drop"
  ) %>%
  arrange(desc(pct_insuficiencia)) %>%
  slice(1:5)

top5_insuficiencia
## # A tibble: 5 × 4
##   estacion    total_dias dias_no_suficiencia pct_insuficiencia
##   <chr>            <int>               <int>             <dbl>
## 1 Pesquería          364                 364             100  
## 2 San Juan           364                 364             100  
## 3 San Bernabé        364                 326              89.6
## 4 Guadalupe          364                 209              57.4
## 5 García             364                 169              46.4