1 Paleta de colores y librerías

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.


2 Mis datos

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
n <- length(Datos21); n
## [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.


3 Tabla de frecuencias individuales

tabla_ind <- freq(Datos21, report.nas = FALSE, headings = FALSE)
tabla_ind
## 
##               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.


4 Resumen de datos (descriptores numéricos)

summary(Datos21)
##    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)
Descriptores numéricos principales
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.


5 Regla de Sturges: número y ancho de clases

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")
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].


6 Gráfico de caja y bigotes

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.


7 Histograma

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.


8 Tabla agrupada (desde R)

summary(h)

¿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.


9 Tabla de frecuencias acumuladas (construida manualmente)

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")
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\%\).


10 Polígono de frecuencias

10.1 Polígono manual

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)

10.2 Polígono sobre el histograma (absolutas y relativas)

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)

par(mfrow = c(1, 1))

¿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.


11 Ojivas (frecuencias acumuladas)

11.1 Ojiva manual (porcentual) con lectura de percentil

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.

11.2 Ojivas absoluta, relativa y porcentual

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")

par(mfrow = c(1, 1))

¿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.


12 Función de distribución empírica (ECDF)

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.


13 Conclusiones

  • La edad promedio del grupo es 39.82 años y la mediana 37.5 años; al ser valores cercanos, la distribución es aproximadamente simétrica con una ligera concentración en edades jóvenes.
  • El 50 % central de las personas tiene entre 25 y 55.5 años.
  • La clase modal es [19 - 28), con 11 personas.
  • La dispersión es moderada: desviación estándar de 18.09 años y coeficiente de variación del 45.4 %