tacosdedatos

Héctor Sebastián Arcos Robledo
Héctor Sebastián Arcos Robledo

Posted on

¿Cómo lidiar con valores faltantes en R?

The best thing to do with missing data is to not have any

Gertrude Mary Cox

La estadística Gertrude Mary Cox tiene razón, pero en el mundo real es imposible, trabajar con datos implica trabajar con valores faltantes. Por valores faltantes se entiende valores que debieron haberse registrado, pero no lo fueron. R almacena los valores faltantes como NA, lo que significa Not Available. Es importante comprender cómo lidiar con ellos, ya que pueden tener efectos inesperados en nuestros análisis. El remplazamiento de los valores faltantes, lo que se denomina imputación, tiene que hacerse siempre con mucho cuidado: introducir sólo la media puede dar lugar a estimaciones y decisiones erróneas. Por eso es importante aprender cómo encontrarlos, cómo procesarlos y ordenarlos, explorar por qué faltan datos e imputar, cuando sea necesario, aquellos faltantes. Para ello utilizaremos las funciones de las librerías naniar y visdat y el conjunto de datos airquality. naniar tiene un flujo de trabajo similar para explorar valores faltantes y hacer imputaciones, por lo que su aprendizaje es fácil.

# if (!require("naniar")) install.packages("naniar")
# if (!require("visdat")) install.packages("visdat")
# if (!require("simputation")) install.packages("simputation")
# if (!require("tidyverse")) install.packages("tidyverse")

library(naniar)
library(visdat)
library(tidyverse)
library(simputation)

Enter fullscreen mode Exit fullscreen mode

¿Cómo puedo comprobar si me faltan valores?

Para detectar valores faltantes podemos usar el comando any_na, el cual devuelve TRUE si hay alguno o FALSE si no hay ninguno. El comando are_na pregunta "¿cuáles de estos valores son NA?" Para evitar contar manualmente cuántos NA hay, utilizamos n_miss, su comando inverso es n_complete. Por su parte, prop_miss nos ofrece la proporción de valores faltantes, mientras que prop_complete la proporción de valores completos. Si hacemos cualquier cálculo con valores faltantes y datos, la regla general es la siguiente: los cálculos con NA devuelven NA, por lo que hay que tener en cuenta también algunas trampas al trabajar con datos faltantes: por ejemplo, NaN es Not a Number (no es un número), y proviene de operaciones como la raíz cuadrada de -1. R en realidad interpreta NaN como un valor perdido. NULL es un valor vacío pero no faltante, esto es sutilmente diferente de un NA. Inf es un valor infinito, y resulta de operaciones como 10 dividido entre 0 y no se trata de un valor faltante.

x <- c(1, NA, 3, NA, NA, 5)

any_na(x)

are_na(x)

n_miss(x)

n_complete(x)

prop_miss(x)

prop_complete(x)
Enter fullscreen mode Exit fullscreen mode

¿Cómo hacer summarise si me faltan valores?

Necesitamos resumir los datos que faltan para identificar las variables, los casos o los patrones faltantes, ya que pueden sesgar nuestro análisis de datos. Para hacer resúmenes de datos faltantes más detallados, utilizamos una familia de funciones que comienzan con miss_, cada una de las cuales proporciona diferentes maneras de hacer resúmenes de valores faltantes, devolviéndo un dataframe. Por ejemplo, miss_var_summary y miss_case_summary devuelven el número y el porcentaje de faltantes en cada variable. Estos resúmenes funcionan bien después de agrupar con group_by de dplyr, por lo que se puede explorar la falta de datos por cada grupo.

miss_var_summary(airquality) # cada fila representa una variable con el número y el porcentaje de NA's

miss_case_summary(airquality) # cada fila representa una fila con el número y el porcentaje de NA's

miss_var_table(airquality) # devuelve un dataframe con el número de NA's en una variable: número y porcentaje de variables afectadas

miss_case_table(airquality) # devuelve un dataframe con el número de NA's en una variable: número y porcentaje de filas afectadas

miss_var_span(airquality, var = Ozone, span_every = 10) # calcula el número de NA's en una variable para un intervalo repetido. Útil en series de tiempo para buscar patrones por cada x núm. de filas (span_every = núm. de NA's por cada x filas)

miss_var_run(airquality, var = Ozone) # devuelve la longitud de la ejecución (run_length) de datos completos y faltantes de una variable. Útil para encontrar patrones repetidos de NA's 

# cada función puede utilizarse con group_by
airquality %>% 
  group_by(Month) %>% 
  miss_var_summary()

Enter fullscreen mode Exit fullscreen mode

¿Cómo puedo visualizar cuántos valores faltantes tengo?

Para obtener una visualización general de la cantidad de elementos faltantes, podemos usar la función vis_miss del paquete visdat, con el propósito de obtener un heatmap, donde los valores color negro representan los valores faltantes y, en cambio, el gris, los valores presentes. También por default la visualización ofrece algunas estadísticas de resumen. Adicionalmente, vis_miss permite agrupar valores faltantes al establecer cluster = TRUE, esto permite ordenar las filas por valores faltantes para identificar co-ocurrencias comunes.

vis_miss(airquality)

vis_miss(airquality, cluster = TRUE)

gg_miss_var(airquality) # visualización análoga al comando miss_var_summary 

gg_miss_var(airquality, facet = Month) # visualización faceteada por la variable "Month"

gg_miss_case(airquality) # visualización análoga al comando miss_case_summary

gg_miss_upset(airquality) # muestra el número de combinaciones de valores faltantes que coexisten

gg_miss_fct(x = airquality, fct = Month) # explorar cómo cambian los valores faltantes en cada variable en un factor

gg_miss_span(airquality, Month, span_every = 10) # visualización análoga al comando miss_var_span (admite faceteado)

Enter fullscreen mode Exit fullscreen mode

Búsqueda y sustitución de valores faltantes

Imaginemos que una vez que terminamos de limpiar nuestros datos, encontramos que faltan valores que no estaban etiquetados como NA, sino como "sin información", "no disponible", "ND", "N/A", o simplemente aparecen espacios en blanco " ". ¿Cómo los reemplazamos por NA regulares? Pues bien, antes de comenzar es bueno saber cuán grande es el problema. El comando miss_scan_count, toma el dataframe más el parámetro de búsqueda search con una lista de aquellos valores a buscar, de modo que podamos saber cuántas veces aparecen los argumentos que asignamos en el parámetro de búsqueda en cada variable. El parámetro de búsqueda search puede tomar todos los argumentos que queramos al mismo tiempo.

Una vez hecho lo anterior, podemos reemplazarlos mediante la función replace_with_na, la cual toma el dataframe y una lista que contiene el nombre de una variable más los valores que se desea reemplazar con NA. Pero para poder reemplazar los valores faltantes en todas las variables usamos replace_with_na_all(condition = ~.x %in% c("N/A", "missing", "na")), donde la condición nos sirve para identificar todos aquellos valores que deseamos modificar. Para un subconjunto de variables seleccionadas replace_with_na_at y para un subconjunto de variables que cumplen alguna condición (como ser numéricas o de carácter) replace_with_na_if.

# identificar por separado si hay valores faltantes identificados como "N/A", "missing", "na" o " " (espacio en blanco)
miss_scan_count(data = airquality, search = list("N/A"))

miss_scan_count(data = airquality, search = list("missing"))

miss_scan_count(data = airquality, search = list("na"))

miss_scan_count(data = airquality, search = list(" "))

#  identificar en conjunto si hay valores faltantes identificados como  "N/A", "missing", "na", " "
miss_scan_count(data = airquality, search = list("N/A", "missing", "na", " "))

# reemplazar todos los valores faltantes identificados como "N/A", "na", "missing" con "NA" para las variables year y score
airquality_clean <- replace_with_na(airquality, replace = list(year = c("N/A", "na", "missing"),
                                                               score = c("N/A", "na", "missing")))

# probar si `airquality_clean` aún tiene valores faltantes identificados como "N/A", "na", "missing" 
miss_scan_count(airquality_clean, search = list("N/A", "na", "missing"))

# usar `replace_with_na_at()` para reemplazar con NA
replace_with_na_at(airquality,
                   .vars = c("year", "month", "day"), 
                   ~.x %in% c("N/A", "missing", "na", " "))

# usar `replace_with_na_if()` para reemplazar con NA sólo valores de carácter, usando `is.character`
replace_with_na_if(airquality,
                   .predicate = is.character, 
                   ~.x %in% c("N/A", "missing", "na", " "))

# usar `replace_with_na_all()` para reemplazar todos los tipos de valores faltantes con NA 
replace_with_na_all(airquality, ~.x %in% c("N/A", "missing", "na", " "))

Enter fullscreen mode Exit fullscreen mode

Hasta aquí hemos revisado cómo reemplazar valores faltantes que se encuentran explícitamente en un dataframe, pero qué ocurre con aquellos que están implícitos, es decir, valores que se suponen que faltan, pero ni siquiera aparecen explícitamente en nuestro conjunto de datos. Imaginemos que tenemos un dataframe con las puntuaciones de tetris de tres amigos, Raúl, Samuel y Bruno, sus puntuaciones fueron registradas en la mañana, tarde y noche. ¿Notas que no aparece la puntuación de Samuel por la noche? Pues bien, para hacer explícitos valores implícitos usamos la función tidyr::complete(). Otra función bastante útil de esta familiar es tidyr::fill(), la cual completa los valores faltantes de una columna de arriba a abajo (por default), aunque si se agrega el argumento *_.direction = c("down", "up", "downup", "updown")) se puede modifcar la dirección del relleno.

tetris <- 
  tibble::tribble(~nombre,    ~tiempo,    ~valor,
                  "Raúl",     "mañana",    358,
                  "Raúl",     "tarde",     534,
                  "Raúl",     "noche",     100,
                  "Samuel",   "mañana",    139,
                  "Samuel",   "tarde",     177,
                  "Bruno",    "mañana",    963,
                  "Bruno",    "tarde",     962,
                  "Bruno",    "noche",     929) %>% 
  mutate(valor = as.numeric(valor))

tetris %>% 
  tidyr::complete(nombre, tiempo)

tetris <- 
  tibble::tribble(~nombre,    ~tiempo,    ~valor,
                  "Raúl",     "mañana",    358,
                   NA,        "tarde",     534,
                   NA,        "noche",     100,
                  "Samuel",   "mañana",    139,
                   NA,        "tarde",     177,
                   NA,        "noche",     152,
                  "Bruno",    "mañana",    963,
                   NA,        "tarde",     962,
                   NA,        "noche",     929) %>% 
  mutate(valor = as.numeric(valor))


tetris %>% 
  tidyr::fill(nombre)

Enter fullscreen mode Exit fullscreen mode

¿Cómo se pueden utilizar los datos nabulares?

Antes de decidir qué hacer con los valores perdidos, si eliminarlos o imputarlos, es importante conocer algunas de las estructuras de datos especiales para facilitar el trabajo con valores faltantes: las matrices sombra y los datos nabulares. Una matriz de sombra es una representación binaria de un conjunto de datos, donde en lugar de ver los valores, obtenemos un conjunto de datos con unos (falta valor NA) y ceros (no falta valor !NA). Los datos se pueden convertir en una matriz de sombra mediante la función as_shadow, pero para sacar el máximo provecho, es mejor utilizar bind_shadow, la cual adjunta una matriz sombra a la matriz original y las variables se quedan con el mismo nombre que en la matriz original, sólo se agrega el sufijo __NA_. Poner los datos de esta forma, es lo que se conoce como datos nabulares (acrónimo de __NA_ y __Tabular_).

as_shadow(airquality)

# crear datos nabulares uniendo la matriz sombra a los datos con `bind_shadow()`.
bind_shadow(airquality)

# unir la matriz sombra sólo para aquellas variables con valores faltantes utilizando bind_shadow(only_miss = TRUE)
bind_shadow(airquality, only_miss = TRUE)

# uso de datos nabulares para realizar resúmenes
airquality %>% 
  bind_shadow() %>% 
  group_by(Ozone_NA) %>% 
  summarise(mean = (Wind))

# visualizar valores faltantes utilizando geom_density (visualizar una variable como una densidad o una distribución)
airquality %>% 
  bind_shadow() %>% 
ggplot(aes(x = Temp,
           color = Ozone_NA)) +
  geom_density()  # visualizar variable Temp cuando hay datos para Ozone y para cuando no los hay

# visualizar valores faltantes utilizando geom_density + facets (visualizar una variable como una densidad o una distribución)
airquality %>% 
  bind_shadow() %>% 
ggplot(aes(x = Temp,
           color = Ozone_NA)) +
  geom_density() +  # visualizar variable Temp cuando hay datos para Ozone y para cuando no los hay
  facet_wrap(~Ozone_NA) # evita que las densidades se superpongan

# visualizar valores faltantes utilizando geom_boxplot (visualizar una variable como una distribución)
airquality %>% 
  bind_shadow() %>% 
ggplot(aes(x = Temp,
           color = Ozone_NA)) +
  geom_boxplot()  # visualizar variable Temp cuando hay datos para Ozone y para cuando no los hay

# visualizar valores faltantes de dos variables utilizando scatterplot
airquality %>% 
  bind_shadow() %>% 
ggplot(aes(x = Temp,
           y = Wind,
           color = Ozone_NA)) +
  geom_point() # visualizar variables Temp y Wind cuando hay datos para Ozone y para cuando no los hay

airquality %>% 
  bind_shadow() %>% 
ggplot(aes(x = Ozone,
           y = Solar.R)) +
  geom_miss_point() # visualizar los valores faltantes colocándolos en los márgenes a través de imputación, es decir, completa los valores que faltan. Hace la imputación 10% por debajo del valor mínimo  

Enter fullscreen mode Exit fullscreen mode

¿Qué hacer con los valores perdidos? ¿eliminarlos o imputarlos?

Una vez que entendemos nuestros datos y las relaciones entre variables y los valores faltantes, estamos en condiciones de hacer imputaciones con la función impute_below que imputa valores por debajo del rango de los datos. Por ejemplo, supongamos que tenemos un vector de números del 5 al 10, pero con un valor faltante. La función imputa el valor en 4.4, ya que hace la imputación por debajo del valor más bajo de los datos disponibles.

impute_below(c(5, 6, 7, NA, 9, 10))
Enter fullscreen mode Exit fullscreen mode

Pero también existen algunas variantes:

  • impute_below_if solo imputa si las variables cumplen una condición (como si x columna es numérica impute_below_if(data, is. numeric)).
  • impute_below_at imputa variables especificadas dentro del argumento vars. Así impute_below_at(data, vars(var1, var2))).
  • impute_below_all imputa para todas las variables. Así, impute_below_all(data).

Una vez que imputamos, necesitamos tener certeza de cuáles fueron los valores imputados, por lo que una buena práctica consiste en identificar los valores faltantes mediante bind_shadow, para crear una matriz sombra a la matriz original y tengamos así certeza de cuáles fueron los valores imputados.

airquality_imputed <-
  airquality %>% 
  bind_shadow() %>% 
  impute_below_all()

# graficar la distribución del número de valores completos y faltantes de una sola variable
ggplot(airquality_imputed,
       aes(x = Ozone, 
           fill = Ozone_NA)) +
  geom_histogram()

# graficar la distribución del número de valores completos y faltantes de una sola variable en diferentes facetas
ggplot(airquality_imputed,
       aes(x = Ozone, 
           fill = Ozone_NA)) +
  geom_histogram() +
  facet_wrap(~ Month)

# graficar la distribución del número de valores completos y faltantes de dos variables
airquality_imputed <-
  airquality %>% 
  bind_shadow() %>% 
  add_label_shadow() %>% # agrega una etiqueta que identifica si es o no un valor faltante en una columna
  impute_below_all()

ggplot(airquality_imputed,
       aes(x = Ozone, 
           y = Solar.R,
           color = any_missing)) +
  geom_point()

Enter fullscreen mode Exit fullscreen mode

¿Cómo hacer buenas imputaciones?

Para comprender las buenas imputaciones, primero debemos comprender las malas imputaciones. Una imputación mala es aquella que toma el promedio de los valores completos y lo utiliza para imputarlo en los valores faltantes. Este método de imputación es malo ya que tiende a aumentar artificialmente la media y reducir la varianza. Para examinar malas imputaciones, podemos utilizar la función impute_mean y sus variaciones, las cuales son parecidas a impute_below, por lo que pueden ejecutarse para un vector, variables basadas en una condición, especificadas o para todas:

  • impute_mean(data$variable)
  • impute_mean_if(data, is.numeric)
  • impute_mean_at(data, vars(variable1, variable2))
  • impute_mean_all(data)

Para visualizar tales imputaciones, seguimos el mismo proceso que para impute_below, es decir, primero creamos datos nabulares (adjunta la matriz sombra a la original), luego hacemos las imputaciones y, finalmente, agregamos una etiqueta para identificar las filas donde faltan o no valores con add_label_shadow. Algo que debemos tomar en cuenta es que para hacer más eficiente nuestro código, cuando invocamos la función bind_shadow, podemos agregar el argumento only_miss = TRUE, de modo que las imputaciones solo procedan en aquellas columnas con valores faltantes.

airquality_imputed_mean <-
  airquality %>% 
  bind_shadow(only_miss = TRUE) %>% 
  impute_mean_all() %>% 
  add_label_shadow()

head(airquality_imputed_mean)  

Enter fullscreen mode Exit fullscreen mode

Para evaluar qué tan buenas son nuestras imputaciones en una sola variable, debemos buscar cambios en la media/mediana, la dispersión y la escala, a través de un diagrama de cajas. En cambio, si queremos evaluar nuestras imputaciones en más variables usamos la función shadow_long para obtener primero los datos en el formato largo correcto y luego graficar mediante un histograma.

# esta visualización nos muestra que el valor mediano es similar en cada grupo, pero es un poco menor para el grupo donde no hay valores faltantes, por lo tanto, la media no cambia, 
ggplot(airquality_imputed_mean, 
       aes(x = Ozone_NA,
           y = Ozone)) +
  geom_boxplot()

# esta visualización nos muestra que no hay variación en la dispersión de los puntos, los valores imputados están dentro de un rango sensato de los datos.
ggplot(airquality_imputed_mean, 
       aes(x = Ozone,
           y = Solar.R,
           color = any_missing)) +
  geom_point()

# para explorar muchas variables, usamos la función shadow_long para obtener datos en el formato largo correcto
airquality_imputed <-
  airquality %>% 
  bind_shadow() %>% 
  impute_mean_all() 

airquality_imputed_long <- shadow_long(airquality_imputed,
                                      Ozone,
                                      Solar.R)

head(airquality_imputed_long) # devuelve datos con las columnas variable y value más sus columnas sombra variable_NA y value_NA

ggplot(airquality_imputed_long, 
       aes(x = value,
          fill = value_NA)) +
  geom_histogram() +
  facet_wrap(~ variable)

Enter fullscreen mode Exit fullscreen mode

¿Cómo imputar datos usando una regresión lineal?

Existen muchos paquetes de imputación en R. Aquí utilizaremos el paquete simputation, el cual proporciona una interfaz sencilla y potente para imputar valores usando un modelo lineal a través de la función impute_lm, lo que nos permite predecir de mejor manera los valores faltantes. Aquí especificamos la variable que nos gustaría imputar como y, en el lado izquierdo de la fórmula, y las variables que nos gustaría utilizar para informar las imputaciones en el lado derecho. Esto devuelve un dataframe con valores imputados en y, más su matriz sombra en y_NA. Algo importante es que debemos utilizar las funciones bind_shadow y add_label_shadow para identificar los valores faltantes, sin ellas, nomas no podemos hacerlo. Otro aspecto es que, cuando se construye un modelo de imputación, es una buena práctica compararlo con un método alternativo, es decir, uno con dos variables, otro con tres y otro con cuatro. Para unirlos simplemente usamos _bind_rows y le damos nombres.

airquality_imputed_lm <-
  airquality %>% 
  bind_shadow(only_miss = TRUE) %>% # agrega las variables con _NA
  add_label_shadow() %>% # agrega una etiqueta en una columna nueva con "Missing" o "Not Missing"
  impute_lm(Ozone ~ Wind + Temp) %>% # imputar valores de Ozone usando las variables Wind + Temp 
  impute_lm(Solar.R ~ Wind + Temp) # imputar valores de Solar.R usando las variables Wind + Temp 

ggplot(airquality_imputed_lm,
       aes(x = Solar.R,
           y = Ozone,
           color = any_missing)) + # colorea los valores imputados
  geom_point()

airquality_imputed_lm_small <-
  airquality %>% 
  bind_shadow(only_miss = TRUE) %>% # agrega las variables con _NA
  add_label_shadow() %>% # agrega una etiqueta en una columna nueva con "Missing" o "Not Missing"
  impute_lm(Ozone ~ Wind + Temp + Month) %>% # imputar valores de Ozone usando las variables Wind + Temp + Month
  impute_lm(Solar.R ~ Wind + Temp + Month) # imputar valores de Solar.R usando las variables Wind + Temp + Month

airquality_imputed_lm_large <-
  airquality %>% 
  bind_shadow(only_miss = TRUE) %>% # agrega las variables con _NA
  add_label_shadow() %>% # agrega una etiqueta en una columna nueva con "Missing" o "Not Missing"
  impute_lm(Ozone ~ Wind + Temp + Month + Day) %>% # imputar valores de Ozone usando las variables Wind + Temp + Month + Day
  impute_lm(Solar.R ~ Wind + Temp + Month + Day) # imputar valores de Solar.R usando las variables Wind + Temp + Month + Day

bound_models <- bind_rows(small = airquality_imputed_lm_small,
                          large = airquality_imputed_lm_large,
                          .id = "imp_model") # crea un conjunto de datos con una columna adicional

ggplot(bound_models,
       aes(x = Ozone,
           y = Solar.R,
           color = any_missing)) + # colorea los valores imputados
  geom_point() +
  facet_wrap(~ imp_model) # faceteado por modelo de imputación, en este caso, small vs large

Enter fullscreen mode Exit fullscreen mode

Por último, en general, pero no restrictivamente, puede que no funcione para todos los tipos de modelos, pero hay al menos tres pasos para comparar los métodos de evaluación de inferencia de modelos en conjuntos de datos imputados de manera diferente. Esto nos prepara para adaptar nuestros nuevos modelos, de modo que podamos resumir y comparar las diferencias en los datos. Una vez que tenemos los datos en el formato correcto, ajustamos un modelo lineal para cada uno de los conjuntos de datos.

  1. Realizar el análisis de los datos completos, agregando la información de los datos sombra;
  2. Imputar los datos según el modelo lineal;
  3. Combinar los diferentes conjuntos de datos.
# análisis de los datos completos
airquality_cc <-
  airquality %>% 
  na.omit() %>% 
  bind_shadow() %>% 
  add_label_shadow()

#  imputar los datos según el modelo lineal
airquality_imputed_lm <-
  airquality %>% 
  bind_shadow() %>% 
  add_label_shadow() %>% 
  impute_lm(Ozone ~ Temp + Wind + Month + Day) %>% 
  impute_lm(Solar.R ~ Temp + Wind + Month + Day)

# unir los modelos
bound_models <- bind_rows(cc = airquality_cc,
                          imp_lm = airquality_imputed_lm,
                          .id = "imp_model")

# explorar los modelos
model_summary <- 
  bound_models %>% 
  group_by(imp_model) %>% # agrupar por el modelo de imputacion
  nest() %>% # anidar (colapsar) los datos en un formato ordenado que permite crear modelos lineales en cada fila de los datos 
  mutate(mod = map(data, # modelo lineal ajustado
                   ~lm(Temp ~ Ozone + Solar.R + Wind + Temp + Days + Month
                       data = .)),
         res = map(mod, residuals), # residuos
         pred = map(mod, predict), # predicciones
         tidy = map(mod, broom::tidy)) # coeficientes ordenados

# explorar los coeficientes y predicciones de los distintos modelos
model_summary %>% 
  select(imp_model,
         tidy) %>% 
  unnest()

model_summary %>% 
  select(imp_model,
         res) %>% 
  unnest() %>% # desanidar los datos
  ggplot(aes(x = res,
             fill = imp_model)) +
  geom_histogram(position = "dodge") # para poner los residuos de cada modelo uno al lado del otro

model_summary %>% 
  select(imp_model,
         pred) %>% 
  unnest() %>% # desanidar los datos
  ggplot(aes(x = pred,
             fill = imp_model)) +
  geom_histogram(position = "dodge") # para poner las predicciones de cada modelo uno al lado del otro




Enter fullscreen mode Exit fullscreen mode

Discussion (1)

Collapse
karinapizarnik profile image
Karina Leyva

Muchas gracias por tu aporte, me ha sido de mucha utilidad ✨