2.3 Datos faltantes

La problemática originada por los datos faltantes (missing data) en cualquier conjunto de datos subyace cuando se desea realizar un análisis estadístico, para más información en R, se puede consultar CRAN Task View: Missing Data

Vamos a ver un ejemplo, empleando el conjunto de datos airquality que contiene datos falntantes en sus dos primeras variables:

data("airquality")
datos <- airquality[,1:3]
summary(datos)
##      Ozone           Solar.R           Wind       
##  Min.   :  1.00   Min.   :  7.0   Min.   : 1.700  
##  1st Qu.: 18.00   1st Qu.:115.8   1st Qu.: 7.400  
##  Median : 31.50   Median :205.0   Median : 9.700  
##  Mean   : 42.13   Mean   :185.9   Mean   : 9.958  
##  3rd Qu.: 63.25   3rd Qu.:258.8   3rd Qu.:11.500  
##  Max.   :168.00   Max.   :334.0   Max.   :20.700  
##  NA's   :37       NA's   :7
nrow(datos)
## [1] 153
# Datos faltantes por variable
sapply(datos, function(x) sum(is.na(x)))
##   Ozone Solar.R    Wind 
##      37       7       0

A continuación se muestra la distribución de los datos perdidos en el data.frame (a lo largo del tiempo, por mes):

plot(ts(airquality[,1:2]))

¿Existe un patrón no aleatorio en los datos faltantes del ozono? Esta pregunta puede ser abordada parcialmente utilizando el test de Little (Little 1988), disponible en la función mcar_test() del paquete naniar. Este test permite evaluar si los datos faltantes son generados por un mecanismo completamente aleatorio (MCAR). Si la hipótesis de MCAR es rechazada, esto sugiere que los datos faltantes podrían estar siguiendo un mecanismo MAR (missing at random) o MNAR (non missing at random).

Sin embargo, en muchos estudios, se omite el paso anterior y se procede directamente con alguno de los siguientes métodos:

  • Análisis de casos completos (complete cases)
  • Análisis de casos disponibles (borrado por parejas pairwise cases)
  • Imputación de datos faltantes (por la media, mediana, último valor observado, vecino más cercano, valores predichos usando los datos observados….)

Siguiendo con el ejemplo, ante la presencia de datos faltantes, en R inicialmente no podemos conocer cómo se relacionan las tres primeras variables:”

cor(datos[,1:3])
##         Ozone Solar.R Wind
## Ozone       1      NA   NA
## Solar.R    NA       1   NA
## Wind       NA      NA    1

y requiere indicar cómo tratar los datos perdidos. Por ejemplo, una opción sería realizar un análisis sólo de los casos completos, eliminando todas las observaciones (filas) con algún dato faltante de nuestro conjunto de datos:

datosC <- na.omit(datos)
nrow(datosC) # n fija (sólo se utilizan 111 de las 153 de Wind)
## [1] 111
cor(datosC[,1:3])
##              Ozone    Solar.R       Wind
## Ozone    1.0000000  0.3483417 -0.6124966
## Solar.R  0.3483417  1.0000000 -0.1271835
## Wind    -0.6124966 -0.1271835  1.0000000
# otra forma de hacerlo sería:
# nrow(datos[complete.cases(datos),]) 
# cor(datos[,1:3], use ="complete.obs") 

También, se podría usar toda la información disponible. El tamaño muestral \(n\) sería variable en función de los NA’s de cada par de variables:

cor(datos[,1:3], use = "pairwise.complete.obs")
##              Ozone     Solar.R        Wind
## Ozone    1.0000000  0.34834169 -0.60154653
## Solar.R  0.3483417  1.00000000 -0.05679167
## Wind    -0.6015465 -0.05679167  1.00000000

Por ejmmplo, ahora la correlación usa los \(146\) pares de observaciones disponibles para (Solar.R,Wind), en lugar de \(111\) del primer caso.

Por último, también se podría realizar una imputación (Van Buuren 2018). A modo de ejemplo, en el siguiente código, se utiliza la media:

datosI <- datos
datosI$Ozone[is.na(datos$Ozone)] <- mean(datos$Ozone, na.rm = T)
datosI$Solar.R[is.na(datos$Solar.R)] <- mean(datosI$Solar.R, na.rm = T)
cor(datosI[,1:3])
##              Ozone     Solar.R        Wind
## Ozone    1.0000000  0.30296951 -0.53093584
## Solar.R  0.3029695  1.00000000 -0.05524488
## Wind    -0.5309358 -0.05524488  1.00000000

Notar que para el caso del ozono, se han sustituido los 37 NA’s (24% de las observaciones) por un único valor (de ahí que ahora la varianza sea menor a la observada inicialmente, algo que en principio, no sería deseable).

var(datos$Ozone,na.rm = T)
## [1] 1088.201
var(datosI$Ozone)
## [1] 823.3096

Los datos faltantes son una realidad común en muchos estudios, aunque nadie los desea. Para tratarlos correctamente, es esencial comprender cómo se obtuvieron los datos observados y por qué algunos datos no fueron registrados antes de iniciar cualquier otro análisis. No abordar adecuadamente los datos faltantes puede tener un efecto perjudicial en nuestro estudio, ya que las conclusiones obtenidas podrían ser no representativas o contener sesgos.

2.3.1 Funciones apply

2.3.1.1 La función apply

Una forma de evitar la utilización de bucles es utilizando la sentencia apply que permite evaluar una misma función en todas las filas, columnas, etc. de un array de forma simultánea.

La sintaxis de esta función es:

apply(X, MARGIN, FUN, ...)
  • X: matriz (o array).
  • MARGIN: un vector indicando las dimensiones donde se aplicará la función. 1 indica filas, 2 indica columnas, y c(1,2) indica filas y columnas.
  • FUN: función que será aplicada.
  • ...: argumentos opcionales que serán usados por FUN.

Veamos la utilización de la función apply con un ejemplo:

x <- matrix(1:9, nrow = 3)
x
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    2    5    8
## [3,]    3    6    9
apply(x, 1, sum)    # Suma por filas
## [1] 12 15 18
apply(x, 2, sum)    # Suma por columnas
## [1]  6 15 24
apply(x, 2, min)    # Mínimo de las columnas
## [1] 1 4 7
apply(x, 2, range)  # Rango (mínimo y máximo) de las columnas
##      [,1] [,2] [,3]
## [1,]    1    4    7
## [2,]    3    6    9

Alternativamente, se puede utilizar opciones más eficientes: colSums(), rowSums(), colMeans() y rowMeans(), como se muestra en el siguiente código de ejemplo:

x <- matrix(1:1e8, ncol = 10, byrow = FALSE)
t1 <- proc.time()
out<-apply(x, 2, mean)   
proc.time() - t1
##    user  system elapsed 
##    0.33    0.17    1.92
t2 <- proc.time()
out <- colMeans(x)
proc.time() - t2
##    user  system elapsed 
##    0.06    0.08    0.30

2.3.1.2 Variantes de la función apply

  1. La función lapply(X, FUN, ...) aplica la función FUN a cada elemento de una lista en R y devuelve una lista como resultado (sin necesidad de especificar el argumento MARGIN). Notar que todas las estructuras de datos en R pueden convertirse en listas, por lo que lapply() puede utilizarse en más casos que apply().
# lista con las medianas de las variables
list <- lapply(cars, median)
str(list)
## List of 4
##  $ speed    : num 15
##  $ dist     : num 36
##  $ velocidad: num 24.1
##  $ distancia: num 11
  1. La función sapply(X, FUN, ..., simplify = TRUE, USE.NAMES = TRUE) permite iterar sobre una lista o vector (alternativa más eficiente a un for):
# matriz con las medias, medianas y desv. de las variables
res <- sapply(cars, 
          function(x) c(mean = mean(x), 
                        median = median(x), 
                        sd = sd(x)))
# str(res)
res
##            speed     dist velocidad distancia
## mean   15.400000 42.98000 24.783945 13.100463
## median 15.000000 36.00000 24.140206 10.972933
## sd      5.287644 25.76938  8.509655  7.854602
cfuns <- function(x, funs = c(mean, median, sd))
            sapply(funs, function(f) f(x))
x <- 1:10
cfuns(x)
## [1] 5.50000 5.50000 3.02765
sapply(cars, cfuns)
##          speed     dist velocidad distancia
## [1,] 15.400000 42.98000 24.783945 13.100463
## [2,] 15.000000 36.00000 24.140206 10.972933
## [3,]  5.287644 25.76938  8.509655  7.854602
nfuns <- c("mean", "median", "sd")
sapply(nfuns, 
       function(f) eval(parse(text = paste0(f, "(x)"))))
##    mean  median      sd 
## 5.50000 5.50000 3.02765
  1. La función tapply() es similar a la función apply() y permite aplicar una función a los datos desagregados, utilizando como criterio los distintos niveles de una variable factor. Es decir, facilita la creación de tablars resumen por grupos. La sintaxis de esta función es como sigue:
    tapply(X, INDEX, FUN, ...,)
  • X: matriz (o array).
  • INDEX: factor indicando los grupos (niveles).
  • FUN: función que será aplicada.
  • ...: argumentos opcionales .

Consideremos, por ejemplo, el data.frame ChickWeight con datos de un experimento relacionado con la repercusión de varias dietas en el peso de pollos.

data(ChickWeight)
# str(ChickWeight)
head(ChickWeight)
##   weight Time Chick Diet
## 1     42    0     1    1
## 2     51    2     1    1
## 3     59    4     1    1
## 4     64    6     1    1
## 5     76    8     1    1
## 6     93   10     1    1
peso <- ChickWeight$weight
dieta <- ChickWeight$Diet
levels(dieta) <- c("Dieta 1", "Dieta 2", "Dieta 3", "Dieta 4")
tapply(peso, dieta, mean)  # Peso medio por dieta
##  Dieta 1  Dieta 2  Dieta 3  Dieta 4 
## 102.6455 122.6167 142.9500 135.2627
tapply(peso, dieta, summary)
## $`Dieta 1`
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   35.00   57.75   88.00  102.65  136.50  305.00 
## 
## $`Dieta 2`
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    39.0    65.5   104.5   122.6   163.0   331.0 
## 
## $`Dieta 3`
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##    39.0    67.5   125.5   142.9   198.8   373.0 
## 
## $`Dieta 4`
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max. 
##   39.00   71.25  129.50  135.26  184.75  322.00

Alternativamente, se podría emplear la función aggregate() que tiene las ventajas de admitir fórmulas y disponer de un método para series de tiempo.

help(aggregate)
aggregate(peso,by=list(dieta=dieta),FUN = "mean" )
##     dieta        x
## 1 Dieta 1 102.6455
## 2 Dieta 2 122.6167
## 3 Dieta 3 142.9500
## 4 Dieta 4 135.2627
aggregate(peso~dieta,FUN = "summary" ) # con formula
##     dieta peso.Min. peso.1st Qu. peso.Median peso.Mean
## 1 Dieta 1   35.0000      57.7500     88.0000  102.6455
## 2 Dieta 2   39.0000      65.5000    104.5000  122.6167
## 3 Dieta 3   39.0000      67.5000    125.5000  142.9500
## 4 Dieta 4   39.0000      71.2500    129.5000  135.2627
##   peso.3rd Qu. peso.Max.
## 1     136.5000  305.0000
## 2     163.0000  331.0000
## 3     198.7500  373.0000
## 4     184.7500  322.0000

2.3.2 Tablas (para informes)

  1. Tablas con kable():

A continuación, se muestra un ejemplo, de tabla resumen, con las medias, medianas y desviación típica de las variables:

res <- sapply(cars, 
          function(x) c(mean = mean(x), 
                        median = median(x), 
                        sd = sd(x)))
knitr::kable(t(res), digits = 1, 
             col.names = c("Media", "Mediana", "Desv. típica"))
Media Mediana Desv. típica
speed 15.4 15.0 5.3
dist 43.0 36.0 25.8
velocidad 24.8 24.1 8.5
distancia 13.1 11.0 7.9

Y en este segundo ejemplo, se muestra el resumen de un modelo de regresión lineal simple (distancia de frenado en función de la velocidad del vehículo):

modelo <- lm(dist ~ speed, data = cars)
coefs <- coef(summary(modelo))
knitr::kable(coefs, escape = FALSE, digits = 5)
Estimate Std. Error t value Pr(>|t|)
(Intercept) -17.57909 6.75844 -2.60106 0.01232
speed 3.93241 0.41551 9.46399 0.00000
  1. Tablas interactivas con datatabe() del paquete DT:
library(DT)
datatable(iris,options = list(scrollX = TRUE))

Hay muchos otros paquetes de R que se pueden utilizar para generar tablas como: kableExtra(), flextable(), reactable(), reactablefmtr(), formattable(), gt() y tinytable().

2.3.3 Operaciones con tablas de datos

Unir tablas:

  • rbind(): combina vectores, matrices, arrays o data.frames por filas.

  • cbind(): Idem por columnas.

  • merge(): Fusiona dos data.frame por columnas o nombres de fila comunes. También permite otras operaciones de unión (join) de bases de datos, algunas de ellas se verán con más detalle en el Capítulo 4.

Combinar tablas:

  • match(x, table) devuelve un vector (de la misma longitud que x) con las (primeras) posiciones de coincidencia de x en table (o NA, por defecto, si no hay coincidencia).

    Para realizar consultas combinando tablas puede ser más cómodo el operador %in% (?'%in%').

  • pmatch(x, table, ...): similar al anterior pero con coincidencias parciales de cadenas de texto.

References

Little, Roderick JA. 1988. “A Test of Missing Completely at Random for Multivariate Data with Missing Values.” Journal of the American Statistical Association 83 (404): 1198–1202.
Van Buuren, Stef. 2018. Flexible Imputation of Missing Data. CRC press.