0. Librerías

library(readr)
library(dplyr)
library(knitr)
library(kableExtra)
library(ggplot2)
library(e1071)

1. Leer Datos

variables <- read.csv("C:/Users/dellh/Downloads/GlobalWeatherRepository.csv")

2. Depuración y Selección de Variables

O_3 <- na.omit(variables$air_quality_Ozone)
n_total <- length(O_3)

3. Frecuencias

3.1 Max y Min

valor_min <- min(O_3, na.rm = TRUE)
valor_max <- max(O_3, na.rm = TRUE)
rango <- valor_max - valor_min

3.2 Regla de Sturges

K_sturges <- floor(1 + 3.322 * log10(n_total))
A_sturges <- rango / K_sturges

3.3 Intervalos

Li1 <- seq(valor_min, valor_max - A_sturges, by = A_sturges)
Ls1 <- Li1 + A_sturges

3.4 Bucle para las columnas de la tabla

ni1 <- numeric(length(Li1))
for(i in 1:length(Li1)){
  if(i == length(Li1)){
    ni1[i] <- sum(O_3 >= Li1[i] & O_3 <= Ls1[i], na.rm = TRUE)
  } else {
    ni1[i] <- sum(O_3 >= Li1[i] & O_3 < Ls1[i], na.rm = TRUE)
  }
}
hi1 <- (ni1 / sum(ni1)) * 100
Ni_asc1 <- cumsum(ni1)
Hi_asc1 <- cumsum(hi1)
Ni_dsc1 <- rev(cumsum(rev(ni1)))
Hi_dsc1 <- rev(cumsum(rev(hi1)))
MC1 <- (Li1 + Ls1) / 2

3.5.Amplitud para tabla ajustada

amplitud <- 50
valor_min_limpio <- 0  # Inicio limpio en 0

# Crear cortes que cubran hasta el máximo avanzando de 50 en 50
cortes2 <- seq(valor_min_limpio, by = amplitud, to = ceiling(valor_max / amplitud) * amplitud)
K_ajustado <- length(cortes2) - 1

Li2 <- cortes2[1:K_ajustado]
Ls2 <- cortes2[2:(K_ajustado + 1)]

ni2 <- numeric(K_ajustado)
for(i in 1:K_ajustado){
  if(i == K_ajustado){
    ni2[i] <- sum(O_3 >= Li2[i] & O_3 <= Ls2[i], na.rm = TRUE)
  } else {
    ni2[i] <- sum(O_3 >= Li2[i] & O_3 < Ls2[i], na.rm = TRUE)
  }
}
hi2 <- (ni2 / sum(ni2)) * 100
Ni_asc2 <- cumsum(ni2)
Hi_asc2 <- cumsum(hi2)
Ni_dsc2 <- rev(cumsum(rev(ni2)))
Hi_dsc2 <- rev(cumsum(rev(hi2)))
MC2 <- (Li2 + Ls2) / 2

4. Tablas de Distribución de Frecuencias

4.1. Tabla 1

Tabla_Sturges <- data.frame(
  Lim_inf = round(Li1, 2), Lim_sup = round(Ls1, 2), MC = round(MC1, 2),
  ni = ni1, hi = round(hi1, 2), Ni_asc = Ni_asc1, Hi_asc = round(Hi_asc1, 2),
  Ni_dsc = Ni_dsc1, Hi_dsc = round(Hi_dsc1, 2)
)
Tabla_Sturges2 <- Tabla_Sturges
Tabla_Sturges2[] <- lapply(Tabla_Sturges2, as.character)
fila_total <- data.frame(
  Lim_inf = "TOTAL", Lim_sup = "", MC = "",
  ni = as.character(sum(ni1)), hi = as.character(round(sum(hi1), 2)),
  Ni_asc = "", Hi_asc = "", Ni_dsc = "", Hi_dsc = ""
)
Tabla_Sturges2 <- rbind(Tabla_Sturges2, fila_total)

kable(Tabla_Sturges2, align = "c",
      caption = "Tabla N°1: Distribución de frecuencias del Ozono Atmosférico (O_3) del período 2024–2026") |>   
  kableExtra::kable_styling(full_width = TRUE, position ="center", bootstrap_options = c("striped", "hover", "condensed", "responsive")) |>   
  kableExtra::row_spec(0, bold = TRUE, color = "black", background = "#FADBD8") |> # Rosa Pastel
  kableExtra::row_spec(nrow(Tabla_Sturges2), bold = TRUE, background = "#EAEDED") |>   
  footnote(general = "Elaborado por Grupo 2.\nFuente: Global Weather Repository.", general_title = "Nota:", footnote_as_chunk = TRUE, title_format = c("italic","bold"))
Tabla N°1: Distribución de frecuencias del Ozono Atmosférico (O_3) del período 2024–2026
Lim_inf Lim_sup MC ni hi Ni_asc Hi_asc Ni_dsc Hi_dsc
0 26.71 13.35 18347 12.95 18347 12.95 141703 100
26.71 53.41 40.06 49740 35.1 68087 48.05 123356 87.05
53.41 80.12 66.76 47384 33.44 115471 81.49 73616 51.95
80.12 106.82 93.47 18003 12.7 133474 94.19 26232 18.51
106.82 133.53 120.17 5436 3.84 138910 98.03 8229 5.81
133.53 160.23 146.88 1801 1.27 140711 99.3 2793 1.97
160.23 186.94 173.59 573 0.4 141284 99.7 992 0.7
186.94 213.64 200.29 215 0.15 141499 99.86 419 0.3
213.64 240.35 227 103 0.07 141602 99.93 204 0.14
240.35 267.06 253.7 35 0.02 141637 99.95 101 0.07
267.06 293.76 280.41 25 0.02 141662 99.97 66 0.05
293.76 320.47 307.11 16 0.01 141678 99.98 41 0.03
320.47 347.17 333.82 11 0.01 141689 99.99 25 0.02
347.17 373.88 360.52 4 0 141693 99.99 14 0.01
373.88 400.58 387.23 3 0 141696 100 10 0.01
400.58 427.29 413.94 2 0 141698 100 7 0
427.29 453.99 440.64 3 0 141701 100 5 0
453.99 480.7 467.35 2 0 141703 100 2 0
TOTAL 141703 100
Nota: Elaborado por Grupo 2.
Fuente: Global Weather Repository.

4.2 Tabla ajustada

TDF_O_3 <- data.frame(
  Lim_inf = round(Li2,2), Lim_sup = round(Ls2,2), MC = round(MC2,2),
  ni = ni2, hi = round(hi2,2), Ni_asc = Ni_asc2, Hi_asc = round(Hi_asc2,2),
  Ni_dsc = Ni_dsc2, Hi_dsc = round(Hi_dsc2,2)
)
TDF_O_3_Texto <- TDF_O_3
TDF_O_3_Texto[] <- lapply(TDF_O_3_Texto, as.character)
TDF_Total <- data.frame(
  Lim_inf = "TOTAL", Lim_sup = "", MC = "",
  ni = as.character(sum(ni2)), hi = as.character(round(sum(hi2),2)),
  Ni_asc = "", Hi_asc = "", Ni_dsc = "", Hi_dsc = "", stringsAsFactors = FALSE
)
TDF_O_3_Final <- rbind(TDF_O_3_Texto, TDF_Total)
colnames(TDF_O_3_Final) <- c("Lim. Inf.", "Lim. Sup.", "MC", "ni", "hi (%)", "Ni Asc", "Hi Asc", "Ni Dsc", "Hi Dsc")

kable(TDF_O_3_Final, align = "c",
      caption = "Tabla N°2: Distribución de frecuencias para Cantidad de Ozono (O3) de los registros meteorológicos,
      período 2024–2026") |>  
  kable_styling(full_width = TRUE, position = "center", bootstrap_options = c("striped", "hover", "condensed", "responsive")) |>  
  row_spec(0, bold = TRUE, color = "black", background = "#E8DAEF") |> # Lila Pastel
  row_spec(nrow(TDF_O_3_Final), bold = TRUE, background = "#EAEDED") |>  
  footnote(general = "Elaborado por Grupo 2.\nFuente: Global Weather Repository.", general_title = "Nota: ", footnote_as_chunk = TRUE, title_format = c("italic","bold"))
Tabla N°2: Distribución de frecuencias para Cantidad de Ozono (O3) de los registros meteorológicos, período 2024–2026
Lim. Inf. Lim. Sup. MC ni hi (%) Ni Asc Hi Asc Ni Dsc Hi Dsc
0 50 25 59381 41.91 59381 41.91 141703 100
50 100 75 71031 50.13 130412 92.03 82322 58.09
100 150 125 9764 6.89 140176 98.92 11291 7.97
150 200 175 1224 0.86 141400 99.79 1527 1.08
200 250 225 211 0.15 141611 99.94 303 0.21
250 300 275 57 0.04 141668 99.98 92 0.06
300 350 325 22 0.02 141690 99.99 35 0.02
350 400 375 6 0 141696 100 13 0.01
400 450 425 3 0 141699 100 7 0
450 500 475 4 0 141703 100 4 0
TOTAL 141703 100
Nota: Elaborado por Grupo 2.
Fuente: Global Weather Repository.

5. Gráficos de Distribución de Frecuencias

5.1. Histograma Original (ni)

hist(
  O_3, 
  breaks = cortes2,
  xaxt = "n",
  main = "Gráfico Nro 1: Histograma de frecuencias absolutas del Ozono (O3)", 
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia (ni)", 
  col = "#FADBD8", border = "black"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
grid()

5.2. Histograma con relación al todo (ni)

hist(
  O_3, 
  breaks = cortes2, 
  ylim = c(0, n_total),
  xaxt = "n",
  main = "Gráfico Nro 2: Histograma global de concentración de Ozono (O3)", 
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia (ni)", 
  col = "#E8DAEF", border = "black"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
grid()

5.3. Histograma original (hi)

# Usamos hist() para mantener barras continuas de histograma real sin separaciones
h_rel <- hist(O_3, breaks = cortes2, plot = FALSE)
h_rel$counts <- (h_rel$counts / sum(h_rel$counts)) * 100

plot(
  h_rel, 
  freq = TRUE,
  xaxt = "n",
  main = "Gráfico Nro 3: Histograma local de frecuencia relativa de Ozono (O3)",
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia Relativa (%)", 
  col = "#FCF3CF", border = "black"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
grid()

5.4. Histograma con relación a todo (hi)

plot(
  h_rel, 
  freq = TRUE, 
  ylim = c(0, 100),
  xaxt = "n",
  main = "Gráfico Nro 4: Histograma global de frecuencia relativa de Ozono (O3) (0% - 100%)", 
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia Relativa (%)", 
  col = "#D5F5E3", border = "black"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
grid()

## 5.5. Polígonos de frecuencias (hi)

plot(
  h_rel, 
  freq = TRUE, 
  ylim = c(0, max(hi2) * 1.2),
  xaxt = "n",
  main = "Gráfico Nro 5: Polígono de frecuencias relativas de Ozono (O3)",
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia relativa (%)", 
  col = "#E8DAEF", border = "black"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)

x_pol <- c(MC2[1] - amplitud, MC2, MC2[length(MC2)] + amplitud)
y_pol <- c(0, hi2, 0)
lines(x_pol, y_pol, type = "o", col = "#9B59B6", lwd = 2, pch = 16)
grid()

5.6. Ojiva ascendente y descendente (ni)

plot(
  Ls2, Ni_asc2, type = "o", pch = 16, lwd = 2, col = "#5499C7", ylim = c(0, max(Ni_asc2)),
  xaxt = "n",
  main = "Gráfico Nro 6: Ojivas de Frecuencia Absoluta de Ozono (O3)", 
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia Acumulada (Ni)"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
lines(Li2, Ni_dsc2, type = "o", pch = 17, lwd = 2, col = "#EC7063")
legend("right", legend = c("Ojiva Ascendente", "Ojiva Descendente"), col = c("#5499C7", "#EC7063"), pch = c(16, 17), lwd = 2, bty = "n")
grid()

5.7. Ojiva ascendente y descendente (hi)

plot(
  Ls2, Hi_asc2, type = "o", pch = 16, col = "#5499C7", lwd = 2, ylim = c(0, 100),
  xaxt = "n",
  xlab = "Concentración de Ozono (O3)", ylab = "Frecuencia Relativa Acumulada (%)", 
  main = "Gráfico Nro 7: Ojivas de frecuencias relativas acumuladas de Ozono (O3)"
)
axis(1, at = cortes2, las = 2, cex.axis = 0.8)
lines(Li2, Hi_dsc2, type = "o", pch = 17, col = "#EC7063", lwd = 2)
legend("right", legend = c("Ojiva Ascendente", "Ojiva Descendente"), col = c("#5499C7", "#EC7063"), pch = c(16, 17), lwd = 2, bty = "n")
grid()

5.8. Boxplot

Q1 <- quantile(O_3, 0.25)
Q3 <- quantile(O_3, 0.75)
RIC <- Q3 - Q1
Lim_inf_tukey <- Q1 - 1.5 * RIC
Lim_sup_tukey <- Q3 + 1.5 * RIC

boxplot(
  O_3, horizontal = TRUE, outline = FALSE,         
  main = "Gráfico Nro 8: Boxplot de la concentración de Ozono (O3)",         
  xlab = "Concentración de Ozono (O3)", col = "#E5E7E9"
)

atipicos <- O_3[O_3 < Lim_inf_tukey | O_3 > Lim_sup_tukey]
if(length(atipicos) > 0) {
  set.seed(123)
  muestra_atipicos <- sample(atipicos, min(length(atipicos), 1000))
  points(muestra_atipicos, rep(1, length(muestra_atipicos)), col = "#E74C3C", pch = 19, cex = 0.5)
}
abline(v = Lim_inf_tukey, col = "#2980B9", lwd = 2, lty = 2)
abline(v = Lim_sup_tukey, col = "#2980B9", lwd = 2, lty = 2)
legend("topright", legend = c("Atípicos (Muestra)", "Límites de Tukey"), col = c("#E74C3C", "#2980B9"), pch = c(19, NA), lty = c(NA, 2), lwd = c(NA, 2), bty = "n")
grid()

6. Indicadores Estadísticos

6.1. Tendencia central

media <- mean(O_3, na.rm = TRUE)
mediana <- median(O_3, na.rm = TRUE)

# Moda basada en Marca de Clase

max_ni <- max(TDF_O_3$ni)
modas_mc <- TDF_O_3$MC[TDF_O_3$ni == max_ni]
moda_texto <- paste(round(modas_mc, 2), collapse = ", ")

6.2. Dispersión

varianza <- var(O_3, na.rm = TRUE)
desv_est <- sd(O_3, na.rm = TRUE)
cv <- (desv_est / media) * 100

6.3. Forma

asimetria <- skewness(O_3, na.rm = TRUE)
curtosis <- kurtosis(O_3, na.rm = TRUE)

6.4. Valores atípicos

n_atipicos <- length(atipicos)
rango_texto <- paste0("[", round(valor_min, 2), " ; ", round(valor_max, 2), "]")

6.5. Tabla de indicadores

# ==========================================
# 6.5. Tabla de Indicadores (Horizontal)
# ==========================================

tabla_indicadores <- data.frame(
  Variable = "Ozono (O_3)", 
  Rango = rango_texto, 
  Media = round(media, 2), 
  Mediana = round(mediana, 2), 
  Moda = round(modas_mc[1], 2), 
  Varianza = round(varianza, 2), 
  Desv_Est = round(desv_est, 2), 
  CV = round(cv, 2), 
  Asimetria = round(asimetria, 2), 
  Curtosis = round(curtosis, 2), 
  N_Atipicos = n_atipicos
)

kable(tabla_indicadores, align = "c", 
      caption = "Tabla N°3: Indicadores estadísticos descriptivos de la variable Ozono (O_3), período 2024–2026") |>
  kable_styling(full_width = TRUE, position = "center", bootstrap_options = c("striped", "hover", "condensed", "responsive")) |>
  row_spec(0, bold = TRUE, color = "black", background = "#FCF3CF")
Tabla N°3: Indicadores estadísticos descriptivos de la variable Ozono (O_3), período 2024–2026
Variable Rango Media Mediana Moda Varianza Desv_Est CV Asimetria Curtosis N_Atipicos
Ozono (O_3) [0 ; 480.7] 57.68 55 75 928.9 30.48 52.84 1.24 5.49 3858

7. Conclusión

El análisis de la Cantidad de Ozono (\(O_3\)) demuestra que la calidad del aire se mantiene en niveles saludables y notablemente estables la mayor parte del tiempo (Mediana = 55 ppm). No obstante, el comportamiento de los datos refleja una asimetría positiva (1.24) y una dispersión moderada (CV = 52.84%) introducida por 3,858 registros atípicos, los cuales evidencian episodios críticos y puntuales de contaminación atmosférica dañinos para la salud humana y la vegetación.