library(summarytools) # freq(): tabla de frecuencias individuales
library(agricolae) # graph.freq(), polygon.freq(), ogive.freq()
library(knitr) # kable(): tablas con formato
# Paleta personalizada usada en todo el documento
pal_barras <- colorRampPalette(c("#A8DADC", "#457B9D", "#1D3557"))
col_hist <- "#6BAED6" # azul suave para el histograma
col_borde <- "#08519C" # azul oscuro para bordes
col_poli <- "#E63946" # rojo coral para el polígono
col_ojiva <- "#2A9D8F" # verde azulado para las ojivas
col_ecdf <- "#7048E8" # violeta para la ECDF
col_guia <- "#F4A261" # naranja para líneas guía¿Qué se hace aquí? Se cargan los paquetes necesarios y se define una paleta de colores fija. Al centralizar los colores en variables, todas las gráficas quedan visualmente coherentes y basta cambiar una línea para cambiar el estilo de todo el documento.
Datos21 <- c(38, 15, 10, 12, 62, 46, 25, 56, 27, 24, 23, 21,
20, 25, 38, 27, 48, 35, 50, 65, 59, 58, 47, 42, 37, 35,
32, 40, 28, 14, 12, 24, 66, 73, 72, 70, 68, 65, 54, 48,
34, 33, 21, 19, 61, 59, 47, 46, 30, 30)
Datos21## [1] 38 15 10 12 62 46 25 56 27 24 23 21 20 25 38 27 48 35 50 65 59 58 47 42 37
## [26] 35 32 40 28 14 12 24 66 73 72 70 68 65 54 48 34 33 21 19 61 59 47 46 30 30
## [1] 50
¿Qué se hace aquí? Se registra la variable edad de 50 personas. Es una variable cuantitativa continua, por lo que más adelante conviene agruparla en clases.
##
## Freq % % Cum.
## ----------- ------ -------- --------
## 10 1 2.00 2.00
## 12 2 4.00 6.00
## 14 1 2.00 8.00
## 15 1 2.00 10.00
## 19 1 2.00 12.00
## 20 1 2.00 14.00
## 21 2 4.00 18.00
## 23 1 2.00 20.00
## 24 2 4.00 24.00
## 25 2 4.00 28.00
## 27 2 4.00 32.00
## 28 1 2.00 34.00
## 30 2 4.00 38.00
## 32 1 2.00 40.00
## 33 1 2.00 42.00
## 34 1 2.00 44.00
## 35 2 4.00 48.00
## 37 1 2.00 50.00
## 38 2 4.00 54.00
## 40 1 2.00 56.00
## 42 1 2.00 58.00
## 46 2 4.00 62.00
## 47 2 4.00 66.00
## 48 2 4.00 70.00
## 50 1 2.00 72.00
## 54 1 2.00 74.00
## 56 1 2.00 76.00
## 58 1 2.00 78.00
## 59 2 4.00 82.00
## 61 1 2.00 84.00
## 62 1 2.00 86.00
## 65 2 4.00 90.00
## 66 1 2.00 92.00
## 68 1 2.00 94.00
## 70 1 2.00 96.00
## 72 1 2.00 98.00
## 73 1 2.00 100.00
## Total 50 100.00 100.00
¿Qué se hace aquí? freq() lista
cada valor distinto de la edad con su frecuencia
absoluta, su porcentaje y los acumulados. Sirve para detectar valores
repetidos (por ejemplo 12, 21, 24, 25, 27, 30, 35, 38, 46, 47, 48, 59 y
65 aparecen dos veces), pero como hay muchos valores distintos la
lectura es poco práctica: por eso se agrupa.
## Min. 1st Qu. Median Mean 3rd Qu. Max.
## 10.00 25.00 37.50 39.82 55.50 73.00
resumen <- data.frame(
Medida = c("Mínimo", "1er Cuartil", "Mediana", "Media", "3er Cuartil",
"Máximo", "Rango", "Desv. estándar", "Varianza",
"Coef. de variación (%)"),
Valor = round(c(min(Datos21), quantile(Datos21, .25), median(Datos21),
mean(Datos21), quantile(Datos21, .75), max(Datos21),
diff(range(Datos21)), sd(Datos21), var(Datos21),
100 * sd(Datos21) / mean(Datos21)), 2)
)
kable(resumen, caption = "Descriptores numéricos principales", row.names = FALSE)| Medida | Valor |
|---|---|
| Mínimo | 10.00 |
| 1er Cuartil | 25.00 |
| Mediana | 37.50 |
| Media | 39.82 |
| 3er Cuartil | 55.50 |
| Máximo | 73.00 |
| Rango | 63.00 |
| Desv. estándar | 18.09 |
| Varianza | 327.09 |
| Coef. de variación (%) | 45.42 |
¿Qué se hace aquí? Se resume la distribución con medidas de posición (mínimo, cuartiles, mediana, máximo), de tendencia central (media) y de dispersión (rango, desviación estándar, coeficiente de variación). Comparando media y mediana se intuye la simetría: si son parecidas, la distribución es aproximadamente simétrica.
k <- ceiling(1 + 3.322 * log10(n)) # número de clases
R <- max(Datos21) - min(Datos21) # rango
W <- ceiling(R / k) # ancho de clase
cortes <- seq(min(Datos21), min(Datos21) + k * W, by = W)
kable(data.frame(
Elemento = c("n (datos)", "k = 1 + 3.322·log(n)", "Rango R = máx - mín",
"Ancho W = R / k", "Límites de clase"),
Resultado = c(n, k, R, W, paste(cortes, collapse = " – "))
), caption = "Cálculo de las clases mediante la Regla de Sturges")| Elemento | Resultado |
|---|---|
| n (datos) | 50 |
| k = 1 + 3.322·log(n) | 7 |
| Rango R = máx - mín | 63 |
| Ancho W = R / k | 9 |
| Límites de clase | 10 – 19 – 28 – 37 – 46 – 55 – 64 – 73 |
¿Qué se hace aquí? Se aplica la regla de
Sturges para decidir en cuántos intervalos dividir los datos:
k = 1 + 3.322·log(50) ≈ 6.64 → 7 clases. Con un rango de 63
años, el ancho de cada clase es W = 63/7 ≈ 9 años,
generando los intervalos [10–19), [19–28), … , [64–73].
par(mar = c(5, 4, 4, 2))
boxplot(Datos21,
horizontal = TRUE,
col = "#A8DADC",
border = col_borde,
medlwd = 3,
outpch = 19, outcol = col_poli,
main = "Diagrama de caja y bigotes — Edad (años)",
xlab = "Edad (años)",
cex.main = 1.2, col.main = "#1F4E79")
stripchart(Datos21, method = "jitter", pch = 19, cex = .8,
col = adjustcolor(col_borde, .45), add = TRUE)
abline(v = mean(Datos21), col = col_poli, lty = 2, lwd = 2)
legend("topright", legend = c("Media", "Datos individuales"),
col = c(col_poli, col_borde), lty = c(2, NA), pch = c(NA, 19),
bty = "n", cex = .85)¿Qué se hace aquí? La caja abarca el 50 % central de las edades (del Q1 al Q3), la línea gruesa es la mediana y la línea punteada roja es la media. Los puntos superpuestos muestran la ubicación real de cada dato. Si no aparecen puntos fuera de los bigotes, no hay valores atípicos.
h <- graph.freq(Datos21,
breaks = cortes,
col = col_hist,
border = col_borde,
frequency = 1,
main = "Histograma de edades (Regla de Sturges)",
xlab = "Edad (años)",
ylab = "Frecuencia (fᵢ)",
axes = FALSE,
col.main = "#1F4E79")
axis(1, at = cortes, cex.axis = .9)
axis(2, las = 1, cex.axis = .9)
grid(nx = NA, ny = NULL, col = "gray85", lty = 3)
text(h$mids, h$counts, labels = h$counts, pos = 3, font = 2, col = col_borde)
box(col = "gray60")¿Qué se hace aquí? El histograma muestra cuántas personas caen en cada intervalo de edad. Las barras son contiguas porque la variable es continua, y la altura de cada una es su frecuencia absoluta (escrita sobre la barra). Se aprecia que la clase más numerosa es [19–28) con 11 personas.
¿Qué se hace aquí? summary() aplicado
al objeto del histograma devuelve automáticamente la tabla de
frecuencias agrupadas: clases, marca de clase, frecuencia absoluta,
relativa y acumulada. Es la versión “automática” de la tabla que se
construye a mano en la siguiente sección.
xi <- h$mids # marca de clase
fi <- h$counts # frecuencia absoluta
hi <- fi / n # frecuencia relativa
pi <- 100 * hi # frecuencia relativa porcentual
Fi <- cumsum(fi) # frecuencia absoluta acumulada
Hi <- cumsum(hi) # frecuencia relativa acumulada
Pi <- cumsum(pi) # porcentaje acumulado
clases <- paste0("[", head(cortes, -1), " - ", cortes[-1], ")")
clases[length(clases)] <- sub("\\)$", "]", clases[length(clases)])
tabla_9A <- data.frame(
`Edad (Clase)` = clases,
`Marca xi` = xi,
`Frecuencia fi` = fi,
`Relativa hi` = round(hi, 4),
`Relativa (%)` = round(pi, 2),
`Acumulada Fi` = Fi,
`Acumulada (%)` = round(Pi, 2),
check.names = FALSE
)
# Fila de totales
tabla_9A <- rbind(tabla_9A,
data.frame(`Edad (Clase)` = "TOTAL", `Marca xi` = NA,
`Frecuencia fi` = sum(fi), `Relativa hi` = 1,
`Relativa (%)` = 100, `Acumulada Fi` = NA,
`Acumulada (%)` = NA, check.names = FALSE))
kable(tabla_9A, align = "c", row.names = FALSE,
caption = "Tabla de frecuencias agrupadas y acumuladas")| Edad (Clase) | Marca xi | Frecuencia fi | Relativa hi | Relativa (%) | Acumulada Fi | Acumulada (%) |
|---|---|---|---|---|---|---|
| [10 - 19) | 14.5 | 5 | 0.10 | 10 | 5 | 10 |
| [19 - 28) | 23.5 | 11 | 0.22 | 22 | 16 | 32 |
| [28 - 37) | 32.5 | 8 | 0.16 | 16 | 24 | 48 |
| [37 - 46) | 41.5 | 5 | 0.10 | 10 | 29 | 58 |
| [46 - 55) | 50.5 | 8 | 0.16 | 16 | 37 | 74 |
| [55 - 64) | 59.5 | 6 | 0.12 | 12 | 43 | 86 |
| [64 - 73] | 68.5 | 7 | 0.14 | 14 | 50 | 100 |
| TOTAL | NA | 50 | 1.00 | 100 | NA | NA |
¿Qué se hace aquí? Se construye paso a paso la tabla
completa: la marca de clase \(x_i\) (punto medio del intervalo), la
frecuencia absoluta \(f_i\), la
relativa \(h_i = f_i/n\), el
porcentaje, y las columnas acumuladas con
cumsum(), que son la materia prima de las ojivas. La fila
TOTAL verifica que \(\sum f_i = 50\) y
\(\sum h_i = 100\%\).
x_pol <- c(min(cortes) - W/2, xi, max(cortes) + W/2) # se cierra en el eje
y_pol <- c(0, fi, 0)
plot(x_pol, y_pol, type = "o", pch = 19, lwd = 2.5, lty = 1,
col = col_poli, bg = col_poli,
main = "Polígono de frecuencias — Edad (años)",
xlab = "Marca de clase (años)", ylab = expression(f[i]),
xaxt = "n", las = 1, col.main = "#1F4E79",
panel.first = grid(col = "gray88", lty = 3))
axis(1, at = round(x_pol, 1), cex.axis = .85)
polygon(x_pol, y_pol, col = adjustcolor(col_poli, .15), border = NA)
text(xi, fi, labels = fi, pos = 3, font = 2, col = col_poli, cex = .9)par(mfrow = c(1, 2), mar = c(4.5, 4.5, 3.5, 1))
# Frecuencias absolutas
h1 <- graph.freq(Datos21, breaks = cortes, col = "#C7E9C0", border = "#238B45",
frequency = 1, main = "Absolutas", xlab = "Edad (años)",
ylab = "Frecuencia", axes = FALSE, col.main = "#1F4E79")
axis(1, at = cortes, cex.axis = .8); axis(2, las = 1, cex.axis = .8)
polygon.freq(h1, col = col_poli, frequency = 1, lwd = 2.5)
points(h1$mids, h1$counts, pch = 19, col = col_poli)
# Frecuencias relativas (%)
h2 <- graph.freq(Datos21, breaks = cortes, col = "#C7E9C0", border = "#238B45",
frequency = 2, main = "Relativas (%)", xlab = "Edad (años)",
ylab = "Frecuencia relativa (%)", axes = FALSE, col.main = "#1F4E79")
axis(1, at = cortes, cex.axis = .8); axis(2, las = 1, cex.axis = .8)
polygon.freq(h2, col = col_poli, frequency = 2, lwd = 2.5)¿Qué se hace aquí? El polígono une las marcas de clase con segmentos y se cierra en cero a ambos lados, de modo que el área bajo el polígono equivale al área del histograma. Permite ver la forma de la distribución (aquí, un pico en las edades jóvenes y una cola hacia las edades mayores) y comparar distribuciones sin el “ruido” de las barras.
x_oj <- cortes[-1] # límite superior de cada clase
y_oj <- c(0, Pi)
x_oj <- c(cortes[1], x_oj)
plot(x_oj, y_oj, type = "o", pch = 19, lwd = 2.5, col = col_ojiva,
main = "Ojiva manual — frecuencia porcentual acumulada",
xlab = "Edad (años)", ylab = "Frecuencia acumulada (%)",
ylim = c(0, 105), xaxt = "n", las = 1, col.main = "#1F4E79",
panel.first = grid(col = "gray88", lty = 3))
axis(1, at = x_oj, cex.axis = .85)
text(x_oj, y_oj, labels = round(y_oj), pos = 4, cex = .8, col = "#1D3557")
# Lectura gráfica: ¿qué edad deja por debajo al 75 % de las personas?
p <- 75
edad_p <- approx(y_oj, x_oj, xout = p)$y
segments(cortes[1], p, edad_p, p, col = col_guia, lty = 2, lwd = 2)
segments(edad_p, 0, edad_p, p, col = col_guia, lty = 2, lwd = 2)
points(edad_p, p, pch = 19, col = col_guia, cex = 1.3)
legend("topleft", legend = sprintf("P75 ≈ %.1f años", edad_p),
col = col_guia, lty = 2, lwd = 2, bty = "n", cex = .9)¿Qué se hace aquí? La ojiva se traza sobre los límites superiores de cada clase (no sobre las marcas) porque acumula “hasta” ese valor. Es siempre creciente, empieza en 0 % y termina en 100 %. Las líneas punteadas naranjas muestran su uso práctico: entrar por el 75 % en el eje vertical y leer el percentil 75 ≈ 55.8 años.
par(mfrow = c(1, 3), mar = c(4.5, 4.5, 3.5, 1))
plot(Fi, type = "o", pch = 19, lwd = 2.5, col = "#E76F51",
main = "Ojiva — absolutas", xlab = "Clases", ylab = "Fi",
las = 1, col.main = "#1F4E79", panel.first = grid(col = "gray88", lty = 3))
text(seq_along(Fi), Fi, labels = Fi, pos = 3, cex = .8, font = 2, col = "#E76F51")
plot(Hi, type = "o", pch = 19, lwd = 2.5, col = "#457B9D",
main = "Ojiva — relativas", xlab = "Clases", ylab = "Hi",
las = 1, col.main = "#1F4E79", panel.first = grid(col = "gray88", lty = 3))
text(seq_along(Hi), Hi, labels = round(Hi, 2), pos = 3, cex = .8, font = 2, col = "#457B9D")
plot(Pi, type = "o", pch = 19, lwd = 2.5, col = "#2A9D8F",
main = "Ojiva — porcentuales", xlab = "Clases", ylab = "Pi (%)",
las = 1, col.main = "#1F4E79", panel.first = grid(col = "gray88", lty = 3))
text(seq_along(Pi), Pi, labels = round(Pi), pos = 3, cex = .8, font = 2, col = "#2A9D8F")¿Qué se hace aquí? Las tres ojivas contienen la misma información en distintas escalas: conteo acumulado (0 a 50), proporción acumulada (0 a 1) y porcentaje acumulado (0 a 100 %). Se usa la porcentual cuando se quieren comparar grupos de distinto tamaño.
plot(ecdf(Datos21), col = col_ecdf, lwd = 2.5, pch = 19, cex = .6,
main = "Función de distribución empírica — F(x)",
xlab = "Edad (años)", ylab = "F(x)", las = 1, col.main = "#1F4E79",
panel.first = grid(col = "gray88", lty = 3))
abline(h = c(0, 1), col = "gray55", lty = 3)
abline(h = .5, col = col_guia, lty = 2, lwd = 2)
abline(v = median(Datos21), col = col_guia, lty = 2, lwd = 2)
legend("bottomright", legend = sprintf("Mediana = %.1f años", median(Datos21)),
col = col_guia, lty = 2, lwd = 2, bty = "n", cex = .9)¿Qué se hace aquí? La ECDF es la versión sin agrupar de la ojiva: en lugar de unir puntos de clase, da un salto de \(1/n\) en cada dato observado, formando una escalera. \(F(x)\) indica la proporción de personas con edad menor o igual a \(x\); donde cruza el 0.5 se ubica la mediana (37.5 años), marcada con las líneas naranjas.