2. Detección de outliers

Author

Rubén Cool

Introducción

En el análisis de regresión lineal simple, el objetivo es modelar la relación entre una variable respuesta cuantitativa y y una variable predictora x mediante una línea recta:

y = \beta_0 + \beta_1 x + \epsilon, \quad \epsilon \sim N(0, \sigma^2)

Sin embargo, la presencia de observaciones atípicas (outliers) o puntos con alta influencia puede distorsionar gravemente las estimaciones por Mínimos Cuadrados Ordinarios, alterando la pendiente, el intercepto, el coeficiente de determinación (R^2) e invalidando las pruebas de hipótesis sobre los supuestos del modelo.

ggplot(datos_contaminados, aes(x = x, y = y)) +
  # Puntos normales en azul, el atípico destacado en rojo
  geom_point(aes(color = x == 12), size = 3) +
  scale_color_manual(values = c("navy", "red")) +
  # Recta original (sin el atípico)
  geom_abline(intercept = coef(modelo_original)[1], 
              slope = coef(modelo_original)[2], 
              color = "blue", linewidth = 1.2, linetype = "solid") +
  # Recta contaminada (con el atípico)
  geom_abline(intercept = coef(modelo_contaminado)[1], 
              slope = coef(modelo_contaminado)[2], 
              color = "red", linewidth = 1.2, linetype = "dashed") +
  labs(title = "Efecto devastador de un solo valor atípico en OLS",
       subtitle = "Línea azul: muestra el modelo real.Línea roja: muestra cómo se deforma al incluir el punto rojo",
       x = "Variable Predictora (x)",
       y = "Variable Respuesta (y)") +
  theme_minimal() +
  theme(legend.position = "none")

summary(modelo_contaminado)

Call:
lm(formula = y ~ x, data = datos_contaminados)

Residuals:
    Min      1Q  Median      3Q     Max 
-42.387  -1.776   0.902   4.602   7.866 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)   
(Intercept)   8.5242     3.5254   2.418  0.02212 * 
x             1.5719     0.5512   2.852  0.00794 **
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 8.843 on 29 degrees of freedom
Multiple R-squared:  0.219, Adjusted R-squared:  0.1921 
F-statistic: 8.131 on 1 and 29 DF,  p-value: 0.007936
summary(modelo_original)

Call:
lm(formula = y ~ x, data = datos)

Residuals:
    Min      1Q  Median      3Q     Max 
-3.1495 -0.5404 -0.1273  1.0565  2.6500 

Coefficients:
            Estimate Std. Error t value Pr(>|t|)    
(Intercept)  2.93678    0.61179    4.80 4.79e-05 ***
x            2.84464    0.09995   28.46  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 1.471 on 28 degrees of freedom
Multiple R-squared:  0.9666,    Adjusted R-squared:  0.9654 
F-statistic:   810 on 1 and 28 DF,  p-value: < 2.2e-16
Modelo contaminado Modelo Original
\hat \beta_0 8.52 2.93
\hat \beta_1 1.57 2.84
R^2 0.219 0.966

Valores atípicos

  • También llamados observaciones atípicas.

  • Valores que distan mucho del resto del conjunto observado (Montgomery, Peck y Vining, 2012).

  • Existen valores atípicos tanto en la variable respuesta (vertical outliers, o simplemente outliers) como en las variables predictoras (leverages) (Rousseeuw y Leroy, 1987).

Tipos de valores atípicos

  1. Un outlier es atípico en la variable respuesta; su residual dista extremadamente de los demás (3 a 4 desviaciones estándar) (Montgomery, Peck y Vining, 2012).

  2. Un good leverage es atípico en alguna de las variables predictoras, distando del clúster y conservando el comportamiento correspondiente en la variable respuesta (Rousseeuw y Leroy, 1987).

  3. Un bad leverage es atípico en alguna de las variables predictoras, distando del clúster, pero sin conservar el comportamiento en la respuesta (Rousseeuw y Leroy, 1987).

Causa de valores atípicos (Kowalskie, 2025)

  1. Errores en la recopilación o entrada de datos (error al momento de ingresar un dato, error de un instrumento).

  2. Acontecimientos extraordinarios o causas no conocidas (una pandemia, una liquidación, un cambio de políticas).

  3. Valores que son genuinamente extremos, pero válidos (calificaciones, riquezas).

Relevancia de los valores atípicos

Datos Factibles:

  1. Pueden controlar muchas propiedades claves del modelo.

  2. Pueden hacer resaltar inadecuaciones en el modelo.

  3. Si el valor atípico es un punto de respuesta especialmente deseable, sería en extremo valioso conocer los valores de los regresores, cuando se observó esa respuesta.

  4. Si la muestra es pequeña, los datos pueden ser legítimos, pero extremos, por lo que valdría la pena conservarlos.

  5. Los análisis de identificación y de seguimiento de los valores atípicos con frecuencia dan como resultado mejoras en el proceso, o nuevos conocimientos acerca de factores cuyo efecto sobre la respuesta se desconocía antes.

Datos “Malos”:

  1. Se deben a eventos desacostumbrados.

  2. Algunos ejemplos son la medición o análisis incorrectos, el registro erróneo de los datos o la falla de un instrumento de medición adecuado.

  3. Si este es el caso, se deberían corregir o eliminar del conjunto de datos.

Detección de valores atípicos

Se detectan valores atípicos visualmente, ya que parecen como puntos individuales que se alejan del patrón general. Para ello se utilizan histogramas, boxplots, scatterplots, centroides y clústers, y elipses de confianza (Bettinger y Spring, 2020).

A continuación, mostraremos cómo detectar, diagnosticar y tratar valores atípicos utilizando R, empleando la base de datos airquality para observar cómo la limpieza de datos ayuda a restaurar el comportamiento adecuado del modelo y la normalidad de los errores.

Preparación y Análisis Exploratorio

Cargamos los datos y ajustamos nuestro modelo inicial de regresión lineal simple tomando como variable respuesta el Ozono (Ozone) y como predictora la Temperatura (Temp).

library(ggplot2)
library(MASS)
library(nortest)

# Cargar y limpiar valores faltantes (NA) en airquality
data("airquality")
air_clean <- na.omit(airquality)

# Ajuste del modelo OLS original
modelo_orig <- lm(Ozone ~ Temp, data = air_clean)
summary(modelo_orig)

Call:
lm(formula = Ozone ~ Temp, data = air_clean)

Residuals:
    Min      1Q  Median      3Q     Max 
-40.922 -17.459  -0.874  10.444 118.078 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -147.6461    18.7553  -7.872 2.76e-12 ***
Temp           2.4391     0.2393  10.192  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 23.92 on 109 degrees of freedom
Multiple R-squared:  0.488, Adjusted R-squared:  0.4833 
F-statistic: 103.9 on 1 and 109 DF,  p-value: < 2.2e-16

Gráficamente podemos observar la relación y ubicar de manera visual el punto atípico superior (una concentración de ozono inusualmente alta para esa temperatura):

ggplot(air_clean, aes(x = Temp, y = Ozone)) +
  geom_point(color = "navy", size = 2) +
  geom_smooth(method = "lm", color = "red", se = FALSE) +
  labs(title = "Regresión Lineal: Ozone vs Temp",
       subtitle = "Modelo OLS con observaciones originales",
       x = "Temperatura (°F)",
       y = "Ozono (ppb)") +
  theme_minimal()
`geom_smooth()` using formula = 'y ~ x'

Detección Numérica de Valores Atípicos e Influencia

Para ir más allá de la simple inspección visual, la literatura estadística define tres herramientas fundamentales de diagnóstico:

  1. Leverage (h_{ii}): Mide cuán extrema es una observación en el espacio de la variable predictora x. Se considera un valor extremo en x si supera el umbral:

    \bar{h} = \frac{p}{n}, \quad h_{ii} > 3\left(\frac{p}{n}\right)

  2. Distancia de Cook (D_i): Mide el impacto global que tiene la eliminación de la observación i-ésima sobre todo el conjunto de estimaciones del modelo. El umbral de alerta común es D_i \ge \frac{4}{n - p}.

  3. Residuales Estudentizados (RStudent t_i): Evalúan si el error de predicción es extremadamente grande (3 a 4 desviaciones estándar), detectando outliers en la variable respuesta y. Se suele usar la región de rechazo |t_i| > 3.

Vamos a calcular e identificar estos puntos en nuestro código de R:

n <- nrow(air_clean)
p <- 2 # Intercepto y pendiente

# 1. Leverage
hat_vals <- hatvalues(modelo_orig)
umbral_hat <- 3 * (p / n)

# 2. Distancia de Cook
cook_vals <- cooks.distance(modelo_orig)
umbral_cook <- 4 / (n - p)

# 3. RStudent
r_student <- rstudent(modelo_orig)

# Identificar filas problemáticas
atipicos_rstudent <- which(abs(r_student) > 3)
atipicos_cook <- which(cook_vals > umbral_cook)

cat("Observaciones con RStudent > 3:", atipicos_rstudent, "\n")
Observaciones con RStudent > 3: 23 34 77 
cat("Observaciones con Distancia de Cook alta:", atipicos_cook, "\n")
Observaciones con Distancia de Cook alta: 23 34 63 77 80 

Visualicemos las Distancias de Cook para identificar claramente el impacto de la observación clave (la número 77):

plot(modelo_orig, which = 4, col = "blue", pch = 16)
abline(h = umbral_cook, col = "red", lty = 2)

Tratamiento de Valores Atípicos

Ante la presencia de valores atípicos confirmados como problemáticos o erróneos, existen dos vías principales: la eliminación (dropping) o la imputación / winsorización.

Opción A: Eliminación de observaciones extremas

Si determinamos que el punto responde a un error de medición o un evento extraordinario ajeno al fenómeno de estudio, podemos eliminarlo y reajustar el modelo:

# Identificamos y filtramos la observación 77 (la de mayor RStudent/Cook)
air_filtered <- air_clean[-atipicos_rstudent, ]

# Reajustar modelo sin el outlier
modelo_filtrado <- lm(Ozone ~ Temp, data = air_filtered)
summary(modelo_filtrado)

Call:
lm(formula = Ozone ~ Temp, data = air_filtered)

Residuals:
    Min      1Q  Median      3Q     Max 
-38.137 -14.772   1.216  12.180  56.132 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -142.5091    14.6182  -9.749   <2e-16 ***
Temp           2.3413     0.1867  12.538   <2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 18.62 on 106 degrees of freedom
Multiple R-squared:  0.5973,    Adjusted R-squared:  0.5935 
F-statistic: 157.2 on 1 and 106 DF,  p-value: < 2.2e-16
lillie.test(modelo_orig$residuals)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  modelo_orig$residuals
D = 0.094492, p-value = 0.01631
lillie.test(modelo_filtrado$residuals)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  modelo_filtrado$residuals
D = 0.074334, p-value = 0.1509

Opción B: Winsorización (Alternativa para conservar datos)

Si los valores son extremos pero legítimos (es decir, pertenecen a la población pero alargan las colas de la distribución), la Winsorización sustituye los valores que rebasan los percentiles extremos por dichos umbrales, reduciendo su peso sin perder el tamaño de la muestra.

A diferencia de la eliminación (dropping), que borra por completo la observación extrema, la Winsorización consiste en sustituir los valores más extremos (tanto los más altos como los más bajos) por el valor del percentil correspondiente fijado como límite (por ejemplo, el percentil 95 para los valores altos y el percentil 5 para los bajos).

¿Cuándo se debe utilizar?

  • Valores reales pero extremos: Se recomienda cuando verificas que el dato es legítimo y pertenece a la población de estudio (por ejemplo, salarios muy altos o temperaturas sumamente inusuales), pero su magnitud es tal que deforma los estimadores de Mínimos Ordinarios Cuadrados (OLS).

  • Preservar el tamaño de muestra: Es ideal cuando tienes muestras pequeñas donde cada observación cuenta y eliminar filas reduciría drásticamente los grados de libertad del modelo.

  • Colas largas naturales: Cuando la distribución de la variable presenta de manera natural colas pesadas.

Nota importante: Si el valor atípico es claramente un error de captura o de medición, la Winsorización no es la solución adecuada, ya que mantendrías un dato falso aunque con el valor del percentil; en esos casos se prefiere corregir o eliminar)

# 1. Definir los límites de los percentiles (por ejemplo, 5% inferior y 5% superior)
q_inf <- quantile(air_clean$Ozone, 0.05, na.rm = TRUE)
q_sup <- quantile(air_clean$Ozone, 0.95, na.rm = TRUE)

# 2. Crear una nueva columna en el dataframe con la variable Ozone winsorizada
air_clean$Ozone_winsorized <- pmin(pmax(air_clean$Ozone, q_inf), q_sup)

# 3. Ajustar el nuevo modelo con la variable winsorizada
modelo_winsor <- lm(Ozone_winsorized ~ Temp, data = air_clean)

# Comparar los coeficientes del modelo original vs el winsorizado
summary(modelo_orig)

Call:
lm(formula = Ozone ~ Temp, data = air_clean)

Residuals:
    Min      1Q  Median      3Q     Max 
-40.922 -17.459  -0.874  10.444 118.078 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -147.6461    18.7553  -7.872 2.76e-12 ***
Temp           2.4391     0.2393  10.192  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 23.92 on 109 degrees of freedom
Multiple R-squared:  0.488, Adjusted R-squared:  0.4833 
F-statistic: 103.9 on 1 and 109 DF,  p-value: < 2.2e-16
summary(modelo_winsor)

Call:
lm(formula = Ozone_winsorized ~ Temp, data = air_clean)

Residuals:
    Min      1Q  Median      3Q     Max 
-39.752 -16.327   0.085  11.738  64.936 

Coefficients:
             Estimate Std. Error t value Pr(>|t|)    
(Intercept) -141.1259    16.0769  -8.778 2.58e-14 ***
Temp           2.3442     0.2051  11.427  < 2e-16 ***
---
Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1

Residual standard error: 20.5 on 109 degrees of freedom
Multiple R-squared:  0.545, Adjusted R-squared:  0.5409 
F-statistic: 130.6 on 1 and 109 DF,  p-value: < 2.2e-16

Visualización gráfica del efecto

# Gráfico comparativo
plot(air_clean$Temp, air_clean$Ozone, pch = 16, col = "gray70",
     main = "Efecto de la Winsorización en Regresión Lineal",
     xlab = "Temperatura (°F)", ylab = "Ozono (ppb)")

# Puntos originales que fueron "recortados" (winsorizados)
points(air_clean$Temp[air_clean$Ozone > q_sup], 
       air_clean$Ozone[air_clean$Ozone > q_sup], 
       col = "purple", pch = 17, cex = 1.3)

# Rectas de regresión
abline(modelo_orig, col = "red", lwd = 2)         # OLS Original (afectado por el pico)
abline(modelo_winsor, col = "forestgreen", lwd = 2, lty = 2) # OLS Winsorizado

legend("topleft", 
       legend = c("OLS Original", "OLS Winsorizado", "Valores acotados"),
       col = c("red", "forestgreen", "purple"), 
       lty = c(1, 2, NA), pch = c(NA, NA, 17), lwd = 2)

# Obtener los residuos estandarizados o estudentizados del modelo
residuos_std <- rstandard(modelo_orig)

# Crear un data frame para ggplot
df_qq <- data.frame(residuos = residuos_std)

# Generar el Q-Q plot con ggplot2
ggplot(df_qq, aes(sample = residuos)) +
  stat_qq(color = "navy", size = 2) +
  stat_qq_line(color = "red", linetype = "dashed", linewidth = 1) +
  labs(
    title = "Gráfico Q-Q de Residuos Estándar",
    subtitle = "Verificación del supuesto de normalidad para modelo_orig",
    x = "Cuantiles Teóricos",
    y = "Cuantiles Muestrales (Residuos)"
  ) +
  theme_minimal()

# Obtener los residuos estandarizados o estudentizados del modelo
residuos_stdwin <- rstandard(modelo_winsor)

# Crear un data frame para ggplot
df_qqwin <- data.frame(residuos = residuos_stdwin)

# Generar el Q-Q plot con ggplot2
ggplot(df_qqwin, aes(sample = residuos)) +
  stat_qq(color = "navy", size = 2) +
  stat_qq_line(color = "red", linetype = "dashed", linewidth = 1) +
  labs(
    title = "Gráfico Q-Q de Residuos Estándar",
    subtitle = "Verificación del supuesto de normalidad para modelo_winsor",
    x = "Cuantiles Teóricos",
    y = "Cuantiles Muestrales (Residuos)"
  ) +
  theme_minimal()

lillie.test(modelo_orig$residuals)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  modelo_orig$residuals
D = 0.094492, p-value = 0.01631
lillie.test(modelo_winsor$residuals)

    Lilliefors (Kolmogorov-Smirnov) normality test

data:  modelo_winsor$residuals
D = 0.070994, p-value = 0.1845