Author

Mauricio Prieto Palacios

Published

Última edición el jueves 16 de junio del 2022.

1 Resumen atípicos

El conjunto de datos original tiene 55,692 observaciones. Para cada variable, el total de datos atípicos y su respectiva proporción del total de observaciones es:

Code
fum_eda %>% 
  resumen_atipicos() %>% 
  gt() %>% 
  fmt_percent(columns = prop,
              decimals = 2) %>% 
  fmt_integer(columns = total,
              sep_mark = ",")
Table 1: Observaciones atípicas por variables
variable total prop
transpeptidasa 5,828 10.46%
aspartato 4,378 7.86%
azucar 4,108 7.38%
trigliceridos 3,266 5.86%
creatinina 3,147 5.65%
proteina_orina 3,093 5.55%
edad 2,514 4.51%
vista 2,199 3.95%
colesterol_bueno 1,604 2.88%
escucha 1,456 2.61%
colesterol_malo 1,260 2.26%
sistolica 1,122 2.01%
hemoglobina 1,046 1.88%
peso 398 0.71%

Por ejemplo, la variable colesterol_bueno presenta muchas observaciones extremas

Code
fum_eda %>% 
  ggplot(aes(colesterol_bueno)) +
  geom_boxplot(fill = pint["azulc"]) +
  scale_x_continuous(breaks = breaks_width(100))

Un caso incluso peor ocurren para transpeptidasa:

Code
fum_eda %>% 
  ggplot(aes(transpeptidasa)) +
  geom_boxplot(fill = pint["azulc"]) 

Ninguna columna numérica tiene distribución normal:

Code
no_norm(fum_eda)
[1] TRUE
Code
no_norm(fum_eda, fun = ad.test)
[1] TRUE
Code
no_norm(fum_eda, fun = cvm.test)
[1] TRUE

Entonces la prueba de Grubbs no es aplicable para detectar observaciones atípicas; a pesar de esto el comportamiento de los datos extremos es tan fuerte que la prueba rechaza la posibilidad que sean observaciones comunes

Code
outliers::grubbs.test(fum_eda$colesterol_bueno)

    Grubbs test for one outlier

data:  fum_eda$colesterol_bueno
G = 38.04268, U = 0.97401, p-value < 2.2e-16
alternative hypothesis: highest value 618 is an outlier

2 KNN

Una primera aproximación a la detección de los valores atípicos se realiza a través del puntaje dado por el algoritmo de k vecinos más cercanos.

Code
knn_distancia <- fum_eda %>%
  select_if(is.numeric) %>%
  scale() %>% 
  FNN::get.knn(k = 3) %>% 
  pluck("nn.dist") %>% 
  rowMeans()
Code
max(knn_distancia)
## [1] 51.52709
which.max(knn_distancia)
## [1] 29544
Code
fum_knn <- 
  fum_eda %>% 
  mutate(knn_distancia = knn_distancia)

fum_knn %>% 
  ggplot(aes(knn_distancia)) +
  geom_boxplot()

Code
fum_knn %>% 
  ggplot(aes(colesterol_malo, creatinina, size = knn_distancia)) +
  geom_point(alpha = 0.1)

3 Aislamiento

Para hallar las observaciones anómalas se utiliza el algoritmo de bosque de aislamiento.

Code
# remotes::install_github("Zelazny7/isofor")
aislamiento <- isofor::iForest(fum_eda, 
                               nt = 3,
                               phi = 1e3) %>% 
   predict(fum_eda)

length(aislamiento)
## [1] 55692
summary(aislamiento)
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##  0.3531  0.3787  0.4015  0.4155  0.4376  0.8075

El algoritmo asigna a cada observación un puntaje entre 0 y 1 que indica qué tan fácil resulta separarla del resto de observaciones. Entre mayor sea el puntaje, más sencillo fue aislar a la observación y por tanto es verosímil creer que es atípica.

La estrategia es eliminar las observaciones con los puntajes más altos. Para esto se muestra la distribución de los puntajes:

Code
aislamiento %>%
  as_tibble() %>%
  ggplot(aes(value)) +
  geom_histogram(fill = pint["azulc"],
                 colour = "white",
                 bins = 30) +
  labs(x = "Puntaje aislamiento",
       y = "Conteo") +
  scale_x_continuous(labels = label_percent()) +
  theme(panel.grid.major.y = element_blank(),
        panel.grid.minor.y = element_blank())

Code
# ggsave(filename = "output/imagenes/aislamiento.pdf")

Se aprecia que la gran mayoría de los datos tienen un puntaje bajo, lo que indica que no son observaciones atípicas. Un diagrama de caja y brazos permite apreciar los valores extremos de manera sencilla:

Code
aislamiento %>% 
  as_tibble() %>% 
  ggplot(aes(value)) +
  geom_boxplot(fill = pint["azulc"],
               outlier.colour = pint["azulc"]) +
  labs(x = "Puntaje aislamiento") +
  scale_x_continuous(labels = label_percent(),
                     breaks = breaks_width(0.05)) +
  theme(panel.grid.major.y = element_blank(),
        panel.grid.minor.y = element_blank(),
        panel.grid.minor.x = element_blank(),
        axis.ticks.y = element_blank(),
        axis.text.y = element_blank())

Entonces se eliminará todas aquellas variables que cuyo puntaje supere un umbral determinado, el cual debe ser grande pero no tanto como para eliminar más del 50% de los datos. Tras prueba y error se propone que el umbral sea el tercer cuantil de la distribución de puntajes más 1.1 vecese el rango intercuantil de la misma distribución. Esto garantiza que solo se elimine el 4.98% de los datos.

Code
umbral <- quantile(aislamiento, 0.75, names = FALSE) + 1.1 * IQR(aislamiento)
umbral
[1] 0.5023356
Code
total_anomalos <- length(aislamiento[aislamiento >= umbral])
total_anomalos
[1] 3484
Code
total_anomalos / nrow(fum_eda)
[1] 0.06255836
Code
anomalo <- which(aislamiento >= umbral)

Incluso dentro de los datos anómalos hay observaciones extremas en algunas variables como colesterol_bueno:

Code
fum_eda %>% 
  slice(anomalo) %>% 
  ggplot(aes(colesterol_bueno)) +
  geom_boxplot(fill = pint["azulc"])

4 Sin atípicos

Ahora se elimina los datos anómalos.

Code
fum_sinatipico <- fum_eda %>% 
  slice(setdiff(1:nrow(fum_eda), 
                anomalo))

Se conservó poco más del 95% de los datos

Code
nrow(fum_sinatipico) / nrow(fum_eda)
[1] 0.9374416

De los datos filtrados, ahora se muestra el total de datos atípicos y su proporción para cada variable

Code
fum_sinatipico %>% 
  resumen_atipicos() %>% 
  gt() %>% 
  fmt_percent(columns = prop,
              decimals = 2) %>% 
  fmt_integer(columns = total,
              sep_mark = ",")
variable total prop
transpeptidasa 4,929 9.44%
azucar 3,886 7.44%
aspartato 3,315 6.35%
trigliceridos 3,149 6.03%
creatinina 2,832 5.42%
proteina_orina 2,414 4.62%
edad 2,024 3.88%
vista 1,880 3.60%
colesterol_bueno 1,493 2.86%
escucha 1,205 2.31%
colesterol_malo 1,132 2.17%
hemoglobina 1,030 1.97%
sistolica 802 1.54%
peso 59 0.11%

Hay una reducción ligera en las proporciones, pero aún hay comportamientos extremos en alguas variables como azucar.

Code
fum_sinatipico %>% 
  ggplot(aes(azucar)) +
  geom_histogram()

Code
est_atip <- fum_eda %>% 
  group_by(fumador, sexo) %>% 
  summarise(across(.cols = c(transpeptidasa, azucar),
                    .fns = est_res,
                   .names = "{.col}-{.fn}")) %>% 
  pivot_longer(cols = -c(fumador, sexo),
               names_to = c("var", ".value"),
               values_to = "a",
               names_sep = "-") %>% 
  pivot_longer(cols = - c(fumador, sexo, var),
               names_to = "estadistica",
               values_to = "estimacion") %>% 
  mutate(estadistica = as_factor(estadistica),
         estadistica = fct_inorder(estadistica),
         var = as_factor(var)) %>% 
  ungroup()
Code
est_atip %>% 
  # filter(estadistica == "min") %>% 
  ggplot(aes(estimacion, var, fill = fumador)) +
  geom_col(position = position_dodge()) +
  facet_wrap(~ estadistica, scales = "free") +
  scale_fill_pint("calido") +
  theme(legend.position = "bottom")
Code
fum_eda %>% 
  ggplot(aes(colesterol_malo, 
             fill = fumador)) +
  geom_histogram()

Code
fum_eda %>% 
  summarise(across(where(is.numeric),
                   .fns = est_res,
                   .names = "{.col}-{.fn}")) %>% 
  pivot_longer(everything(),
                 names_to = c("variable", ".value"),
                 values_to = "calc",
                 names_sep = "-"
    )
# A tibble: 14 × 9
   variable           min cuartil1 mediana   media cuartil3    max     sd   iqr
   <chr>            <dbl>    <dbl>   <dbl>   <dbl>    <dbl>  <dbl>  <dbl> <dbl>
 1 edad              20       40      40    44.2       55     85   12.1   15   
 2 peso              30       55      65    65.9       75    135   12.8   20   
 3 vista              0.1      0.8     1     1.01       1.2    9.9  0.486  0.4 
 4 escucha            1        1       1     1.03       1      2    0.160  0   
 5 sistolica         71      112     120   121.       130    240   13.7   18   
 6 azucar            46       89      96    99.3      104    505   20.8   15   
 7 trigliceridos      8       74     108   127.       160    999   71.6   86   
 8 colesterol_bueno   4       47      55    57.3       66    618   14.7   19   
 9 colesterol_malo    1       92     113   115.       136   1860   40.9   44   
10 hemoglobina        4.9     13.6    14.8  14.6       15.8   21.1  1.56   2.20
11 proteina_orina     1        1       1     1.09       1      6    0.405  0   
12 creatinina         0.1      0.8     0.9   0.886      1     11.6  0.222  0.2 
13 aspartato          6       19      23    26.2       28   1311   19.4    9   
14 transpeptidasa     1       17      25    40.0       43    999   50.3   26   

5 Imputar atípicos

Code
q3trans <- quantile(fum_sinatipico$transpeptidasa, probs = 0.75)
summary(fum_sinatipico$transpeptidasa)
   Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
   2.00   17.00   25.00   37.09   41.00  999.00 
Code
# fum_imputar <- fum_sinatipico %>% 
#   mutate(transpeptidasa = ifelse(transpeptidasa > quantile(transpeptidasa, probs = 0.75), 
#                                  NA, 
#                                  transpeptidasa)) %>% 
#   as.data.frame()
# 
# nimp <- fum_imputar %>% 
#   filter(is.na(transpeptidasa)) %>% 
#   nrow() 
Code
fum_imputar <- fum_sinatipico %>% 
  mutate(across(.cols = c(transpeptidasa, 
                          azucar,
                          aspartato, 
                          trigliceridos,
                          creatinina,
                          colesterol_malo),
                .fns = ~ ifelse(.x > quantile(.x, probs = 0.75),
                                NA,
                                .x)
                )
         ) %>% 
  as.data.frame()

fum_imputar %>% 
  naniar::miss_var_summary()
# A tibble: 18 × 3
   variable         n_miss pct_miss
   <chr>             <int>    <dbl>
 1 trigliceridos     13015     24.9
 2 transpeptidasa    12931     24.8
 3 azucar            12724     24.4
 4 colesterol_malo   12719     24.4
 5 aspartato         12058     23.1
 6 creatinina         9585     18.4
 7 sexo                  0      0  
 8 edad                  0      0  
 9 peso                  0      0  
10 vista                 0      0  
11 escucha               0      0  
12 sistolica             0      0  
13 colesterol_bueno      0      0  
14 hemoglobina           0      0  
15 proteina_orina        0      0  
16 caries                0      0  
17 sarro                 0      0  
18 fumador               0      0  
Code
imp <- missForest::missForest(fum_imputar,
                              ntree = 10)
Code
class(imp)
[1] "missForest"
Code
str(imp)
List of 2
 $ ximp    :'data.frame':   54161 obs. of  23 variables:
  ..$ sexo            : Factor w/ 2 levels "Femenino","Masculino": 1 1 2 1 2 2 1 2 2 2 ...
  ..$ edad            : int [1:54161] 40 40 55 40 30 40 50 45 30 30 ...
  ..$ estatura        : int [1:54161] 155 160 170 155 180 160 150 175 175 170 ...
  ..$ peso            : int [1:54161] 60 60 60 60 75 60 60 75 65 75 ...
  ..$ cintura         : num [1:54161] 81.3 81 80 86 85 85.5 85 89 89 87 ...
  ..$ vista           : num [1:54161] 1 0.6 0.8 1 1.2 1 0.8 1 1.5 1.2 ...
  ..$ escucha         : int [1:54161] 1 1 1 1 1 1 1 1 1 1 ...
  ..$ sistolica       : int [1:54161] 114 119 138 120 128 116 115 113 130 124 ...
  ..$ relajacion      : int [1:54161] 73 70 86 74 76 82 74 64 77 78 ...
  ..$ azucar          : int [1:54161] 94 130 89 80 95 94 86 94 100 101 ...
  ..$ colesterol      : int [1:54161] 215 192 242 184 217 226 210 198 184 184 ...
  ..$ trigliceridos   : int [1:54161] 82 115 182 74 199 68 66 147 141 197 ...
  ..$ colesterol_bueno: int [1:54161] 73 42 55 62 48 55 48 43 82 39 ...
  ..$ colesterol_malo : int [1:54161] 126 127 151 107 129 157 149 126 73 106 ...
  ..$ hemoglobina     : num [1:54161] 12.9 12.7 15.8 12.5 16.2 17 13.7 16 14.7 17.9 ...
  ..$ proteina_orina  : int [1:54161] 1 1 1 1 1 1 1 1 1 1 ...
  ..$ creatinina      : num [1:54161] 0.7 0.6 1 0.6 1.2 0.7 0.8 0.8 0.7 1.1 ...
  ..$ aspartato       : num [1:54161] 18 22 21 16 18 21 23.6 26 24.5 24.3 ...
  ..$ alanina         : num [1:54161] 19 19 16 14 27 27 22.3 24 22.4 26.3 ...
  ..$ transpeptidasa  : num [1:54161] 27 18 22 22 33 39 14 31.2 37 32.1 ...
  ..$ caries          : Factor w/ 2 levels "No","Si": 1 1 1 1 1 2 1 1 1 1 ...
  ..$ sarro           : Factor w/ 2 levels "Si","No": 1 1 2 2 1 1 2 2 2 1 ...
  ..$ fumador         : Factor w/ 2 levels "No","Si": 1 1 2 1 1 2 1 1 1 1 ...
 $ OOBerror: Named num [1:2] 0.0315 0
  ..- attr(*, "names")= chr [1:2] "NRMSE" "PFC"
 - attr(*, "class")= chr "missForest"
Code
naniar::miss_var_summary(imp$ximp)
# A tibble: 23 × 3
   variable   n_miss pct_miss
   <chr>       <int>    <dbl>
 1 sexo            0        0
 2 edad            0        0
 3 estatura        0        0
 4 peso            0        0
 5 cintura         0        0
 6 vista           0        0
 7 escucha         0        0
 8 sistolica       0        0
 9 relajacion      0        0
10 azucar          0        0
# … with 13 more rows
Code
fum_imp <- imp$ximp %>% 
  as_tibble() %>% 
  mutate(across(.cols = c(azucar, 
                          colesterol_malo, 
                          aspartato, 
                          transpeptidasa,
                          trigliceridos),
                .fns = as.integer)
         )
Code
resumen_atipicos(fum_imp)
# A tibble: 19 × 3
   variable         total     prop
   <chr>            <int>    <dbl>
 1 azucar            4239 0.0783  
 2 trigliceridos     3133 0.0578  
 3 creatinina        2903 0.0536  
 4 proteina_orina    2764 0.0510  
 5 edad              2403 0.0444  
 6 estatura          2089 0.0386  
 7 vista             1947 0.0359  
 8 colesterol_bueno  1556 0.0287  
 9 relajacion        1459 0.0269  
10 escucha           1370 0.0253  
11 colesterol_malo   1106 0.0204  
12 hemoglobina       1099 0.0203  
13 colesterol        1042 0.0192  
14 sistolica          956 0.0177  
15 cintura            954 0.0176  
16 aspartato          457 0.00844 
17 peso               357 0.00659 
18 alanina              7 0.000129
19 transpeptidasa       0 0       
Code
gg_double(fum_imp, creatinina, num_bins = 10, ancho = 0.1)

Code
# saveRDS(object = fum_imp, file = "output/fum_imp.RDS")
tablero %>% 
  pin_write(fum_imp,
            "fum_imp",
            title = "Datos tras imputar",
            description = "Se imputó con bosques aleatorios")