Carlos Jimémez-Gallardo
Estadístico | MSc Infórmatica Educativa | Universidad de La
Frontera
carlos.jimenez@ufrontera.cl
Data Scientist| www.innovate.cl
cjimenez@innovate.cl
Muchos métodos estadísticos clásicos (t-test, ANOVA, regresión lineal, correlación de Pearson, entre otros) asumen que los datos —o los residuos del modelo— siguen una distribución normal. Evaluar este supuesto es un paso previo obligatorio antes de aplicar dichas técnicas, o bien para decidir si conviene transformar variables o recurrir a alternativas no paramétricas.
En términos prácticos, la detección de normalidad sirve para:
En este documento usaremos datos simulados a propósito: un conjunto claramente normal, uno claramente no normal, y al final un caso difícil donde distintos métodos no se ponen de acuerdo. La idea es que el lector vea, con ejemplos concretos, cómo se comporta cada método frente a cada tipo de dato.
No existe una única prueba “definitiva”: lo recomendable es combinar métodos gráficos (informales) con pruebas formales, y no depender de un solo criterio.
Datos adecuados
Conocimiento del contexto
Selección de métodos
No confiar en un solo método
Cuidado con el tamaño muestral
Revisar outliers antes de concluir
Evaluar normalidad donde realmente importa
Entender que los métodos pueden discrepar
Documentar el proceso
Usaremos tidyverse para manipulación/visualización,
broom para ordenar los resultados de los tests
(broom::tidy), moments para asimetría/curtosis
y para el Jarque-Bera clásico
(moments::jarque.test), nortest para pruebas
de normalidad adicionales (Lilliefors, Anderson-Darling,
Cramér-von Mises, Shapiro-Francia, Pearson),
Analitica para el test de Jarque-Bera con correcciones de
Glinskiy (JBGTest) y ggpubr para gráficos Q-Q
con estética ggplot2.
library(tidyverse)
library(broom)
library(moments)
library(nortest)
library(Analitica)
library(ggpubr)
Para ilustrar cada método usaremos dos vectores simulados, que se repiten a lo largo de todo el documento:
x_normal: 150 observaciones de una
normal teórica (rnorm), media 50 y desviación estándar 10.
Representa el caso “fácil”: los datos realmente provienen de una
normal.x_no_normal: 150 observaciones de una
exponencial (rexp), claramente asimétrica a la derecha y
con cola pesada. Representa el caso “fácil” opuesto: los datos
claramente no son normales.n <- 150
set.seed(111)
x_normal <- rnorm(n, mean = 50, sd = 10)
set.seed(222)
x_no_normal <- rexp(n, rate = 0.08)
datos_ejemplo <- tibble::tibble(
valor = c(x_normal, x_no_normal),
caso = rep(c("x_normal (normal teórica)", "x_no_normal (exponencial)"), each = n)
)
datos_ejemplo %>%
dplyr::group_by(caso) %>%
dplyr::summarise(
n = dplyr::n(),
media = mean(valor),
de = sd(valor),
minimo = min(valor),
maximo = max(valor),
.groups = "drop"
)
## # A tibble: 2 × 6
## caso n media de minimo maximo
## <chr> <int> <dbl> <dbl> <dbl> <dbl>
## 1 x_no_normal (exponencial) 150 11.7 13.1 0.0495 96.1
## 2 x_normal (normal teórica) 150 50.3 10.9 16.8 77.2
ggplot2::ggplot(datos_ejemplo, ggplot2::aes(x = valor, fill = caso)) +
ggplot2::geom_histogram(bins = 25, color = "white", alpha = 0.8, show.legend = FALSE) +
ggplot2::facet_wrap(~ caso, scales = "free") +
ggplot2::labs(title = "Distribución de los dos casos de ejemplo", x = NULL, y = "Frecuencia")
Lo esperable: x_normal debería verse
como una campana simétrica; x_no_normal debería verse
concentrada hacia la izquierda con una cola larga hacia la derecha.
Comparar el histograma real contra la curva de una normal teórica con la misma media y desviación estándar que los datos es el primer chequeo visual. Si el histograma “calza” con la curva roja, es una señal (no una prueba) de normalidad.
ggplot2::ggplot(datos_ejemplo, ggplot2::aes(x = valor)) +
ggplot2::geom_histogram(ggplot2::aes(y = ggplot2::after_stat(density)),
bins = 25, fill = "#4C72B0", alpha = 0.6, color = "white") +
ggplot2::stat_function(
fun = function(x, m, s) dnorm(x, m, s),
args = list(m = mean(datos_ejemplo$valor), s = sd(datos_ejemplo$valor)),
color = "red", linewidth = 0.8, n = 200
) +
ggplot2::facet_wrap(~ caso, scales = "free") +
ggplot2::labs(title = "Histograma vs. densidad normal teórica", x = NULL, y = "Densidad")
Lectura esperada: en x_normal el
histograma debería seguir de cerca la curva roja; en
x_no_normal el histograma debería quedar notoriamente
desplazado respecto a la curva (asimetría a la derecha).
El gráfico Q-Q compara los cuantiles observados contra los cuantiles teóricos de una normal. Si los puntos siguen la línea de referencia (y se mantienen dentro de la banda de confianza), hay evidencia visual de normalidad; si se “curvan” o se escapan de la banda —sobre todo en los extremos— hay evidencia de no-normalidad.
ggpubr::ggqqplot(x_normal, title = "Q-Q plot: x_normal")
ggpubr::ggqqplot(x_no_normal, title = "Q-Q plot: x_no_normal")
Lectura esperada: en x_normal los
puntos deberían alinearse bien con la diagonal; en
x_no_normal se debería observar una curvatura
característica de asimetría (los puntos se alejan de la línea,
especialmente en la cola derecha).
El boxplot no prueba normalidad, pero permite ver rápidamente asimetría (caja desbalanceada respecto a la mediana) y valores atípicos.
ggplot2::ggplot(datos_ejemplo, ggplot2::aes(x = caso, y = valor, fill = caso)) +
ggplot2::geom_boxplot(alpha = 0.7, show.legend = FALSE) +
ggplot2::labs(title = "Boxplot por caso", x = NULL, y = "Valor")
Lectura esperada: la caja de x_normal
debería verse razonablemente simétrica respecto a su mediana; la de
x_no_normal debería mostrar una caja desbalanceada y varios
puntos atípicos hacia la derecha.
Una normal teórica tiene asimetría (skewness) = 0 y curtosis (kurtosis) = 3 (exceso de curtosis = 0). No es una prueba de hipótesis, pero es un indicador numérico útil y rápido.
tabla_skew_kurt <- datos_ejemplo %>%
dplyr::group_by(caso) %>%
dplyr::summarise(
asimetria = moments::skewness(valor),
curtosis = moments::kurtosis(valor),
curtosis_exceso = moments::kurtosis(valor) - 3,
.groups = "drop"
)
tabla_skew_kurt
## # A tibble: 2 × 4
## caso asimetria curtosis curtosis_exceso
## <chr> <dbl> <dbl> <dbl>
## 1 x_no_normal (exponencial) 2.69 14.6 11.6
## 2 x_normal (normal teórica) -0.181 3.29 0.287
Regla práctica: |asimetría| > 1 sugiere asimetría
marcada; |curtosis - 3| > 1 sugiere colas más pesadas/livianas que la
normal. Se espera que x_normal tenga ambos valores cercanos
a 0, y que x_no_normal tenga asimetría claramente
positiva.
En todas las pruebas que siguen, la hipótesis nula es H0: los
datos provienen de una distribución normal. Un p-valor
menor a alpha (típicamente 0.05) lleva a
rechazar H0 (evidencia de no-normalidad); un p-valor
mayor significa que no hay evidencia suficiente
para rechazar H0 (compatible con normalidad, pero no la
“prueba”).
alpha <- 0.05
Es la prueba más usada y una de las más potentes para muestras pequeñas y medianas (n < 5000).
tabla_shapiro <- dplyr::bind_rows(
broom::tidy(shapiro.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(shapiro.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_W = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_shapiro
## # A tibble: 2 × 4
## caso estadistico_W p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 0.992 5.15e- 1 No se rechaza H0
## 2 x_no_normal 0.757 1.74e-14 Se rechaza H0
El test de Kolmogorov-Smirnov clásico no es adecuado cuando media y
varianza se estiman de los propios datos; para eso se usa la
corrección de Lilliefors
(nortest::lillie.test). Esta prueba tiende a ser más
sensible a desviaciones cerca del centro de la
distribución que en las colas.
tabla_lillie <- dplyr::bind_rows(
broom::tidy(nortest::lillie.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(nortest::lillie.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_D = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_lillie
## # A tibble: 2 × 4
## caso estadistico_D p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 0.0601 2.04e- 1 No se rechaza H0
## 2 x_no_normal 0.185 9.04e-14 Se rechaza H0
Da más peso a las colas de la distribución que Kolmogorov-Smirnov, por lo que es especialmente sensible a colas pesadas y asimetrías marcadas.
tabla_ad <- dplyr::bind_rows(
broom::tidy(nortest::ad.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(nortest::ad.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_A = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_ad
## # A tibble: 2 × 4
## caso estadistico_A p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 0.401 3.56e- 1 No se rechaza H0
## 2 x_no_normal 8.19 4.88e-20 Se rechaza H0
Al igual que Kolmogorov-Smirnov y Anderson-Darling, es una prueba
basada en la función de distribución empírica (EDF):
compara la distribución acumulada observada con la teórica. A diferencia
de KS (que mira la máxima distancia puntual) y de AD (que pondera
fuertemente las colas), Cramér-von Mises usa una distancia
cuadrática integrada a lo largo de toda la distribución, por lo
que suele comportarse como un punto intermedio entre ambas
(nortest::cvm.test).
tabla_cvm <- dplyr::bind_rows(
broom::tidy(nortest::cvm.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(nortest::cvm.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_W2 = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_cvm
## # A tibble: 2 × 4
## caso estadistico_W2 p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 0.0772 2.24e- 1 No se rechaza H0
## 2 x_no_normal 1.43 7.37e-10 Se rechaza H0
Variante simplificada de Shapiro-Wilk, útil como confirmación
adicional (nortest::sf.test).
tabla_sf <- dplyr::bind_rows(
broom::tidy(nortest::sf.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(nortest::sf.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_W = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_sf
## # A tibble: 2 × 4
## caso estadistico_W p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 0.991 4.01e- 1 No se rechaza H0
## 2 x_no_normal 0.750 1.05e-12 Se rechaza H0
Analitica::JBGTest)Ambas versiones se basan en la misma idea: combinar la asimetría y la curtosis muestral en un solo estadístico.
Jarque-Bera clásico (Jarque & Bera, 1980;
moments::jarque.test):
\[JB = \frac{n}{6} S^2 + \frac{n}{24} (K-3)^2\]
donde \(S\) es la asimetría muestral
y \(K\) la curtosis muestral. Bajo H0,
\(JB\) se distribuye
asintóticamente chi-cuadrado con 2 grados de libertad.
El problema: esa aproximación chi-cuadrado(2) es solo asintótica, y en
muestras pequeñas o moderadas el tamaño real del test
(la tasa de falsos rechazos) puede alejarse bastante del nivel nominal
alpha que uno cree estar usando.
Jarque-Bera con correcciones de Glinskiy (Glinskiy
et al., 2024; Analitica::JBGTest): propone
modificaciones al estadístico (y a su distribución de
referencia) para los casos en que la media y/o la varianza poblacional
se conocen a priori, y también para el caso más común en que
ambas son desconocidas (mu = NULL,
sigma2 = NULL, que es lo que usamos aquí). La idea central
es mejorar la aproximación en muestras pequeñas/moderadas, de modo que
el p-valor reportado sea más confiable que el del JB clásico cuando
n no es grande.
## Versión clásica
jb_clasico_normal <- moments::jarque.test(x_normal)
jb_clasico_no_normal <- moments::jarque.test(x_no_normal)
## Versión con correcciones de Glinskiy
jbg_normal <- Analitica::JBGTest(x_normal)
jbg_no_normal <- Analitica::JBGTest(x_no_normal)
tabla_jb_comparacion <- tibble::tribble(
~version, ~caso, ~estadistico_JB, ~p_value,
"JB clásico (moments)", "x_normal", jb_clasico_normal$statistic, jb_clasico_normal$p.value,
"JB clásico (moments)", "x_no_normal", jb_clasico_no_normal$statistic, jb_clasico_no_normal$p.value,
"JB con corrección de Glinskiy (Analitica)", "x_normal", jbg_normal$statistic, jbg_normal$p_value,
"JB con corrección de Glinskiy (Analitica)", "x_no_normal", jbg_no_normal$statistic, jbg_no_normal$p_value
) %>%
dplyr::mutate(decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_jb_comparacion
## # A tibble: 4 × 5
## version caso estadistico_JB p_value decision
## <chr> <chr> <dbl> <dbl> <chr>
## 1 JB clásico (moments) x_no… 1.33 0.514 No se r…
## 2 JB clásico (moments) x_no… 1019. 0 Se rech…
## 3 JB con corrección de Glinskiy (Analitic… x_no… 1.33 0.514 No se r…
## 4 JB con corrección de Glinskiy (Analitic… x_no… 1019. 0 Se rech…
Qué comparar en la tabla: para cada caso
(x_normal, x_no_normal), el estadístico y el
p-valor pueden diferir entre ambas versiones, sobre todo si
n es pequeño/moderado. En muestras grandes, ambas versiones
deberían tender a dar la misma conclusión; la diferencia se nota más
cuando n es chico, que es justamente el escenario que
motivó la corrección de Glinskiy. Prueba cambiar n en la
sección 2 (por ejemplo a 20 o 30) y vuelve a correr esta sección para
ver la diferencia de forma más marcada.
Para el resto del documento (tabla resumen, caso difícil y función
del apéndice) seguimos usando la versión de Glinskiy
(tabla_jbg), por ser la más reciente y más confiable en
muestras no muy grandes.
tabla_jbg <- tibble::tibble(
caso = c("x_normal", "x_no_normal"),
estadistico_JB = c(jbg_normal$statistic, jbg_no_normal$statistic),
df = c(jbg_normal$df, jbg_no_normal$df),
p_value = c(jbg_normal$p_value, jbg_no_normal$p_value),
decision = c(jbg_normal$decision, jbg_no_normal$decision)
)
tabla_jbg
## # A tibble: 2 × 5
## caso estadistico_JB df p_value decision
## <chr> <dbl> <dbl> <dbl> <chr>
## 1 x_normal 1.33 2 0.514 Normal
## 2 x_no_normal 1019. 2 0 Not Normal
Compara frecuencias observadas vs. esperadas por intervalos (bins).
Es, en general, menos potente que Shapiro-Wilk, pero sigue la lógica
clásica de bondad de ajuste (nortest::pearson.test).
tabla_pearson <- dplyr::bind_rows(
broom::tidy(nortest::pearson.test(x_normal)) %>% dplyr::mutate(caso = "x_normal"),
broom::tidy(nortest::pearson.test(x_no_normal)) %>% dplyr::mutate(caso = "x_no_normal")
) %>%
dplyr::transmute(caso, estadistico_chi2 = statistic, p_value = p.value,
decision = dplyr::if_else(p_value > alpha, "No se rechaza H0", "Se rechaza H0"))
tabla_pearson
## # A tibble: 2 × 4
## caso estadistico_chi2 p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 x_normal 22 3.75e- 2 Se rechaza H0
## 2 x_no_normal 98 1.37e-15 Se rechaza H0
resumen_facil <- dplyr::bind_rows(
tabla_shapiro %>% dplyr::mutate(metodo = "Shapiro-Wilk"),
tabla_lillie %>% dplyr::mutate(metodo = "Kolmogorov-Smirnov (Lilliefors)"),
tabla_ad %>% dplyr::mutate(metodo = "Anderson-Darling"),
tabla_cvm %>% dplyr::rename(estadistico = estadistico_W2) %>% dplyr::mutate(metodo = "Cramér-von Mises"),
tabla_sf %>% dplyr::mutate(metodo = "Shapiro-Francia"),
tabla_jbg %>% dplyr::mutate(metodo = "Jarque-Bera (Glinskiy)"),
tabla_pearson %>% dplyr::mutate(metodo = "Pearson chi-cuadrado")
) %>%
dplyr::select(metodo, caso, p_value, decision) %>%
tidyr::pivot_wider(names_from = caso, values_from = c(p_value, decision))
resumen_facil
## # A tibble: 7 × 5
## metodo p_value_x_normal p_value_x_no_normal decision_x_normal
## <chr> <dbl> <dbl> <chr>
## 1 Shapiro-Wilk 0.515 1.74e-14 No se rechaza H0
## 2 Kolmogorov-Smirnov (Li… 0.204 9.04e-14 No se rechaza H0
## 3 Anderson-Darling 0.356 4.88e-20 No se rechaza H0
## 4 Cramér-von Mises 0.224 7.37e-10 No se rechaza H0
## 5 Shapiro-Francia 0.401 1.05e-12 No se rechaza H0
## 6 Jarque-Bera (Glinskiy) 0.514 0 Normal
## 7 Pearson chi-cuadrado 0.0375 1.37e-15 Se rechaza H0
## # ℹ 1 more variable: decision_x_no_normal <chr>
Lo esperable: en los casos “fáciles” las 7 pruebas
deberían coincidir: todas sin evidencia para rechazar
normalidad en x_normal, y todas rechazando normalidad en
x_no_normal. Esto cambia en el siguiente ejemplo.
Hasta aquí, ambos casos fueron “fáciles” porque todos los métodos apuntan en la misma dirección. En la práctica, sin embargo, hay distribuciones que parecen razonablemente normales a simple vista (simétricas, en forma de campana) pero que tienen colas más pesadas que una normal. Un ejemplo clásico es la distribución t de Student con pocos grados de libertad: es simétrica como la normal, pero con colas pesadas.
set.seed(333)
x_dificil <- rt(200, df = 3) * 10 + 50 ## simétrica, pero con colas pesadas
ggplot2::ggplot(data.frame(valor = x_dificil), ggplot2::aes(x = valor)) +
ggplot2::geom_histogram(ggplot2::aes(y = ggplot2::after_stat(density)),
bins = 25, fill = "#55A868", alpha = 0.6, color = "white") +
ggplot2::stat_function(
fun = function(x, m, s) dnorm(x, m, s),
args = list(m = mean(x_dificil), s = sd(x_dificil)),
color = "red", linewidth = 0.8, n = 200
) +
ggplot2::labs(title = "Caso difícil: histograma vs. densidad normal teórica", x = NULL, y = "Densidad")
ggpubr::ggqqplot(x_dificil, title = "Q-Q plot: caso difícil")
ggplot2::ggplot(data.frame(valor = x_dificil), ggplot2::aes(x = "", y = valor)) +
ggplot2::geom_boxplot(fill = "#55A868", alpha = 0.7) +
ggplot2::labs(title = "Boxplot: caso difícil", x = NULL, y = "Valor")
tibble::tibble(
asimetria = moments::skewness(x_dificil),
curtosis = moments::kurtosis(x_dificil),
curtosis_exceso = moments::kurtosis(x_dificil) - 3
)
## # A tibble: 1 × 3
## asimetria curtosis curtosis_exceso
## <dbl> <dbl> <dbl>
## 1 -0.292 6.31 3.31
Lectura esperada: el histograma debería verse
razonablemente simétrico y “acampanado” (a diferencia de
x_no_normal), y la asimetría debería quedar cercana a 0.
Sin embargo, en el Q-Q plot es esperable ver puntos que se
escapan de la banda de confianza en ambos extremos (colas más
largas que la normal), y la curtosis debería ser notoriamente
mayor a 3. Es decir: los métodos informales ya dan una pista de
que algo no calza del todo, aunque menos evidente que en
x_no_normal.
jbg_dificil <- Analitica::JBGTest(x_dificil)
jb_clasico_dificil <- moments::jarque.test(x_dificil)
tabla_dificil <- tibble::tribble(
~metodo, ~estadistico, ~p_value,
"Shapiro-Wilk", shapiro.test(x_dificil)$statistic, shapiro.test(x_dificil)$p.value,
"Kolmogorov-Smirnov (Lilliefors)", nortest::lillie.test(x_dificil)$statistic, nortest::lillie.test(x_dificil)$p.value,
"Anderson-Darling", nortest::ad.test(x_dificil)$statistic, nortest::ad.test(x_dificil)$p.value,
"Cramer-von Mises", nortest::cvm.test(x_dificil)$statistic, nortest::cvm.test(x_dificil)$p.value,
"Shapiro-Francia", nortest::sf.test(x_dificil)$statistic, nortest::sf.test(x_dificil)$p.value,
"Jarque-Bera clásico (moments)", jb_clasico_dificil$statistic, jb_clasico_dificil$p.value,
"Jarque-Bera (Glinskiy)", jbg_dificil$statistic, jbg_dificil$p_value,
"Pearson chi-cuadrado", nortest::pearson.test(x_dificil)$statistic, nortest::pearson.test(x_dificil)$p.value
) %>%
dplyr::mutate(decision = dplyr::if_else(p_value > alpha, "No se rechaza H0 (parece normal)", "Se rechaza H0 (no normal)"))
tabla_dificil
## # A tibble: 8 × 4
## metodo estadistico p_value decision
## <chr> <dbl> <dbl> <chr>
## 1 Shapiro-Wilk 0.955 0.00000553 Se rechaza H0 (no norm…
## 2 Kolmogorov-Smirnov (Lilliefors) 0.0932 0.000229 Se rechaza H0 (no norm…
## 3 Anderson-Darling 2.26 0.00000937 Se rechaza H0 (no norm…
## 4 Cramer-von Mises 0.443 0.00000955 Se rechaza H0 (no norm…
## 5 Shapiro-Francia 0.948 0.00000484 Se rechaza H0 (no norm…
## 6 Jarque-Bera clásico (moments) 94.4 0 Se rechaza H0 (no norm…
## 7 Jarque-Bera (Glinskiy) 94.4 0 Se rechaza H0 (no norm…
## 8 Pearson chi-cuadrado 33.8 0.00224 Se rechaza H0 (no norm…
Este es el punto central del documento: con datos de colas pesadas
como x_dificil, no todas las pruebas
coinciden.
n no es grande,
ya que fue diseñada justamente para corregir el comportamiento del JB
clásico en esas condiciones.Importante: los resultados exactos (qué prueba rechaza y cuál no) dependen de la semilla, el tamaño de muestra y los grados de libertad de la t utilizada. Si al ejecutar este Rmd obtienes una combinación distinta, es exactamente el punto: pequeños cambios en los datos pueden mover el veredicto de pruebas límite. Por eso nunca se debe decidir normalidad en base a un solo test, y por eso los métodos gráficos (histograma, Q-Q) son un complemento indispensable para ver en qué parte de la distribución está el problema (centro vs. colas).
Analitica::JBGTest) sobre la clásica
(moments::jarque.test), ya que su p-valor es más confiable
en ese rango de tamaños muestrales.