Semana 3: Importación y limpieza de datos

Preparando datos para su análisis en investigación en salud

Author

Agoralab

Published

Invalid Date

Importación y limpieza de datos

Bienvenida y repaso

En las dos primeras semanas, nos hemos familiarizado con el entorno de R, Quarto y las estructuras básicas de datos. Ahora estamos listos para comenzar a trabajar con datos reales, lo que implica importarlos desde diferentes fuentes y prepararlos para su análisis.

La importación y limpieza de datos son pasos fundamentales en cualquier proyecto de análisis, especialmente en ciencias de la salud donde la calidad de los datos puede afectar significativamente las conclusiones científicas.

Objetivos de la sesión

Al finalizar esta sesión, serás capaz de:

  1. Importar datos desde diversos formatos (CSV, Excel, otros)
  2. Inspeccionar un conjunto de datos recién importado para identificar posibles problemas
  3. Limpiar y transformar datos para hacerlos más adecuados para el análisis
  4. Manejar valores faltantes, categorías inconsistentes y otros problemas comunes
  5. Comprender el concepto de datos “tidy” (ordenados) y su importancia
  6. Utilizar funciones básicas de dplyr para filtrar y seleccionar datos

Importación de datos

Uno de los primeros pasos en cualquier análisis es cargar datos desde archivos externos. R ofrece múltiples opciones para importar datos desde distintos formatos.

El flujo de trabajo para datos en investigación sanitaria

En investigación en salud, es común seguir este flujo de trabajo para manejar datos:

  1. Recolección - Obtención de datos (estudios clínicos, registros electrónicos, encuestas, etc.)
  2. Importación - Lectura de los datos en R desde su formato original
  3. Limpieza - Corrección de inconsistencias, valores atípicos, datos faltantes
  4. Transformación - Reestructuración de los datos para el análisis
  5. Análisis - Aplicación de métodos estadísticos para responder preguntas de investigación
  6. Comunicación - Presentación de resultados en informes, artículos o visualizaciones

En esta sesión, nos centraremos en los pasos 2, 3 y 4, que suelen representar hasta el 80% del tiempo en un proyecto de análisis.

Importación desde archivos CSV

Los archivos CSV (Comma-Separated Values) son uno de los formatos más comunes para almacenar datos tabulares. Podemos cargarlos usando funciones de R base o del paquete readr (parte del tidyverse):

Code
# Instalamos los paquetes necesarios si no están disponibles
if (!requireNamespace("readr", quietly = TRUE)) {
  install.packages("readr")
}
if (!requireNamespace("writexl", quietly = TRUE)) {
  install.packages("writexl")
}
if (!requireNamespace("readxl", quietly = TRUE)) {
  install.packages("readxl")
}
if (!requireNamespace("skimr", quietly = TRUE)) {
  install.packages("skimr")
}
if (!requireNamespace("dplyr", quietly = TRUE)) {
  install.packages("dplyr")
}
if (!requireNamespace("tidyr", quietly = TRUE)) {
  install.packages("tidyr")
}
if (!requireNamespace("coronavirus", quietly = TRUE)) {
  install.packages("coronavirus")
}
Code
# Usando R base
# datos_base <- read.csv("ruta/al/archivo.csv")

# Usando readr (más rápido y con mejores defaults)
library(readr)

# Ejemplo con datos incluidos en R
# Primero, guardamos el dataset mtcars como un CSV para este ejemplo
write_csv(mtcars, "mtcars.csv")

# Luego lo leemos para mostrar cómo funciona la importación
datos_csv <- read_csv("mtcars.csv")

# Veamos los primeros registros
head(datos_csv)

Ventajas de usar read_csv() de readr vs. read.csv() de base R:

  • Más rápido para archivos grandes
  • No convierte strings a factores automáticamente
  • Usa tibbles (un tipo mejorado de data frame)
  • Mejores mensajes de error y progreso
  • Detecta automáticamente el tipo de columnas
Parámetros útiles para importar CSV en investigación médica

Cuando trabajas con datos de estudios clínicos o sanitarios, estos parámetros pueden ser muy útiles:

datos <- read_csv("datos_clinicos.csv",
  # Para datos con códigos especiales para valores faltantes
  na = c("", "NA", "NULL", "missing", "-999"),
  
  # Para saltarse líneas de cabecera no relevantes
  skip = 2,
  
  # Para especificar el tipo de cada columna (evita inferencias incorrectas)
  col_types = cols(
    id_paciente = col_character(),
    edad = col_integer(),
    grupo_tratamiento = col_factor(levels = c("Control", "Intervención")),
    fecha_ingreso = col_date(format = "%d/%m/%Y")
  ),
  
  # Para datos con separador decimal con coma (habitual en países hispanohablantes)
  locale = locale(decimal_mark = ",")
)

Especificar explícitamente estos parámetros puede prevenir muchos errores comunes en la importación de datos.

Importación desde archivos Excel

Los archivos Excel son muy comunes en contextos de investigación en salud. El paquete readxl facilita su importación:

Code
# Cargamos el paquete readxl
library(readxl)

# Para este ejemplo, creamos un archivo Excel temporal
# (En la práctica, trabajarías con tus propios archivos)
# Creemos un pequeño dataset
datos_ejemplo <- data.frame(
  id = 1:5,
  paciente = c("A", "B", "C", "D", "E"),
  edad = c(45, 52, 67, 38, 71),
  presion_sistolica = c(120, 135, 142, 118, 155),
  presion_diastolica = c(80, 85, 95, 75, 90)
)

# Guardar como Excel
library(writexl)
write_xlsx(datos_ejemplo, "datos_clinicos.xlsx")

# Leer archivo Excel
datos_excel <- read_excel("datos_clinicos.xlsx")
head(datos_excel)
Code
# También puedes especificar qué hoja leer en un libro con múltiples hojas
# datos_excel2 <- read_excel("archivo.xlsx", sheet = "Datos 2023")

# O leer un rango específico
# datos_excel3 <- read_excel("archivo.xlsx", range = "A1:D20")

Otros formatos de importación

R puede trabajar con muchos otros formatos:

Code
# Archivos de SPSS (comunes en investigación clínica)
# library(haven)
# datos_spss <- read_sav("archivo.sav")

# Archivos de Stata
# datos_stata <- read_dta("archivo.dta")

# Archivos de SAS
# datos_sas <- read_sas("archivo.sas7bdat")

# Archivos JSON (comunes en datos de APIs)
# library(jsonlite)
# datos_json <- fromJSON("archivo.json")

# Bases de datos SQL
# library(DBI)
# library(RSQLite)
# conn <- dbConnect(SQLite(), "mi_base.sqlite")
# datos_sql <- dbGetQuery(conn, "SELECT * FROM mi_tabla")
# dbDisconnect(conn)

Mejores prácticas para importación de datos

Al importar datos, considera estas recomendaciones:

  1. Guarda los datos crudos sin modificar: Mantén siempre una copia de los datos originales.
  2. Usa rutas relativas: Mejor que absolutas, para facilitar la reproducibilidad.
  3. Especifica el encoding si es necesario: Para manejar caracteres especiales (ej. acentos).
  4. Documenta la fuente de los datos: Incluye información sobre procedencia y fecha.
Code
# Especificar encoding (ej. para datos con acentos en español)
# datos_acentos <- read_csv("datos_español.csv", locale = locale(encoding = "ISO-8859-1"))

# Usar el paquete here para rutas relativas (muy recomendable)
# library(here)
# datos <- read_csv(here("datos", "clinicos", "pacientes_2023.csv"))

Inspección inicial del dataset

Una vez importados los datos, el siguiente paso es explorarlos para entender su estructura y detectar posibles problemas.

Nunca confíes en datos crudos

En investigación clínica y epidemiológica, los datos crudos casi siempre tienen problemas que podrían afectar tus análisis:

  • Valores faltantes o codificados de manera inconsistente
  • Errores de entrada de datos (ej. presión arterial registrada como 1600 en lugar de 160)
  • Inconsistencias en unidades (mg/dL vs mmol/L)
  • Duplicados (el mismo paciente registrado varias veces)
  • Categorías mal escritas (“Masculino”, “masculino”, “M”, “Varón”, “V” para el mismo concepto)
  • Datos implausibles (edad de 200 años, IMC de 0.5)

Dedicar tiempo a una exploración exhaustiva inicial puede ahorrarte interpretaciones erróneas y conclusiones incorrectas más adelante.

Vamos a trabajar con un conjunto de datos de ejemplo sobre pacientes:

Code
# Creemos un conjunto de datos más completo para nuestra exploración
set.seed(123)  # Para reproducibilidad

datos_pacientes <- data.frame(
  id = 1:100,
  edad = sample(18:90, 100, replace = TRUE),
  sexo = sample(c("M", "m", "Masculino", "F", "f", "Femenino", NA), 100, replace = TRUE),
  peso = round(rnorm(100, mean = 70, sd = 15), 1),
  altura = round(rnorm(100, mean = 1.65, sd = 0.1), 2),
  presion_sistolica = sample(c(100:200, NA), 100, replace = TRUE),
  presion_diastolica = sample(c(60:120, NA), 100, replace = TRUE),
  fumador = sample(c("Sí", "Si", "S", "No", "N", "NS/NC", NA), 100, replace = TRUE),
  diagnostico = sample(c("Hipertensión", "Diabetes", "EPOC", "Asma", "Sano", NA), 
                      100, replace = TRUE),
  fecha_ingreso = sample(seq(as.Date("2023-01-01"), as.Date("2023-12-31"), by = "day"), 
                        100, replace = TRUE)
)

# Introducir algunos errores para ejercicio de limpieza
datos_pacientes$peso[sample(1:100, 5)] <- NA  # Algunos NA
datos_pacientes$peso[sample(1:100, 3)] <- -99  # Código para "no medido"
datos_pacientes$altura[sample(1:100, 3)] <- 0  # Error: altura imposible
datos_pacientes$presion_sistolica[sample(1:100, 3)] <- 0  # Error

Ahora exploremos este conjunto de datos:

Code
# Primeras filas
head(datos_pacientes)
Code
# Dimensiones: filas y columnas
dim(datos_pacientes)
[1] 100  10
Code
# Estructura del dataframe
str(datos_pacientes)
'data.frame':   100 obs. of  10 variables:
 $ id                : int  1 2 3 4 5 6 7 8 9 10 ...
 $ edad              : int  48 68 31 84 59 67 60 31 42 86 ...
 $ sexo              : chr  "f" "Masculino" "F" NA ...
 $ peso              : num  34 69.7 68.7 46.1 82.8 59.3 86 62 78 42.6 ...
 $ altura            : num  1.65 1.61 1.51 1.62 1.56 1.72 1.74 1.62 1.47 1.7 ...
 $ presion_sistolica : num  194 121 179 185 0 155 132 163 152 130 ...
 $ presion_diastolica: int  73 87 65 68 92 111 105 78 96 93 ...
 $ fumador           : chr  NA NA "NS/NC" "Sí" ...
 $ diagnostico       : chr  "Sano" "Asma" "Asma" "Diabetes" ...
 $ fecha_ingreso     : Date, format: "2023-01-26" "2023-09-26" ...
Code
# Resumen estadístico
summary(datos_pacientes)
       id              edad           sexo                peso       
 Min.   :  1.00   Min.   :21.00   Length:100         Min.   :-99.00  
 1st Qu.: 25.75   1st Qu.:38.75   Class :character   1st Qu.: 56.98  
 Median : 50.50   Median :51.00   Mode  :character   Median : 68.20  
 Mean   : 50.50   Mean   :52.63                      Mean   : 64.17  
 3rd Qu.: 75.25   3rd Qu.:67.25                      3rd Qu.: 80.45  
 Max.   :100.00   Max.   :89.00                      Max.   :110.60  
                                                     NA's   :4       
     altura      presion_sistolica presion_diastolica   fumador         
 Min.   :0.000   Min.   :  0.0     Min.   : 60.00     Length:100        
 1st Qu.:1.570   1st Qu.:123.2     1st Qu.: 70.00     Class :character  
 Median :1.650   Median :152.0     Median : 87.50     Mode  :character  
 Mean   :1.597   Mean   :147.0     Mean   : 88.29                       
 3rd Qu.:1.722   3rd Qu.:177.0     3rd Qu.:103.25                       
 Max.   :1.900   Max.   :200.0     Max.   :120.00                       
                 NA's   :2                                              
 diagnostico        fecha_ingreso       
 Length:100         Min.   :2023-01-02  
 Class :character   1st Qu.:2023-03-27  
 Mode  :character   Median :2023-06-12  
                    Mean   :2023-06-20  
                    3rd Qu.:2023-09-23  
                    Max.   :2023-12-30  
                                        
Code
# Para una exploración más detallada, podemos usar el paquete skimr
library(skimr)
skim(datos_pacientes)
Data summary
Name datos_pacientes
Number of rows 100
Number of columns 10
_______________________
Column type frequency:
character 3
Date 1
numeric 6
________________________
Group variables None

Variable type: character

skim_variable n_missing complete_rate min max empty n_unique whitespace
sexo 11 0.89 1 9 0 6 0
fumador 19 0.81 1 5 0 6 0
diagnostico 15 0.85 4 12 0 5 0

Variable type: Date

skim_variable n_missing complete_rate min max median n_unique
fecha_ingreso 0 1 2023-01-02 2023-12-30 2023-06-12 86

Variable type: numeric

skim_variable n_missing complete_rate mean sd p0 p25 p50 p75 p100 hist
id 0 1.00 50.50 29.01 1 25.75 50.50 75.25 100.0 ▇▇▇▇▇
edad 0 1.00 52.63 19.63 21 38.75 51.00 67.25 89.0 ▇▇▇▆▆
peso 4 0.96 64.17 33.58 -99 56.98 68.20 80.45 110.6 ▁▁▁▇▇
altura 0 1.00 1.60 0.30 0 1.57 1.65 1.72 1.9 ▁▁▁▂▇
presion_sistolica 2 0.98 146.97 39.30 0 123.25 152.00 177.00 200.0 ▁▁▃▇▇
presion_diastolica 0 1.00 88.29 18.59 60 70.00 87.50 103.25 120.0 ▇▆▅▃▆

Identificación de problemas comunes

A partir de la exploración, podemos identificar varios problemas típicos:

  1. Valores faltantes (NA): Presentes en varias columnas
  2. Códigos especiales: Valores como -99 que representan “no medido”
  3. Valores imposibles: Como alturas de 0 metros
  4. Inconsistencia en categorías: Diferentes formas de registrar el mismo valor (ej. “Sí”/“Si”/“S”)
  5. Tipos de datos incorrectos: Variables que deberían ser factores pero están como caracteres

Limpieza de datos

Ahora abordaremos cada uno de estos problemas:

1. Manejo de valores faltantes (NA)

Code
# Contar NA por columna
colSums(is.na(datos_pacientes))
                id               edad               sexo               peso 
                 0                  0                 11                  4 
            altura  presion_sistolica presion_diastolica            fumador 
                 0                  2                  0                 19 
       diagnostico      fecha_ingreso 
                15                  0 
Code
# Identificar filas con NA en la columna de peso
which(is.na(datos_pacientes$peso))
[1] 16 30 51 89
Code
# Opciones para manejar NA:

# 1. Eliminar filas con NA (use with caution!)
datos_sin_na <- na.omit(datos_pacientes)
dim(datos_sin_na)  # Verificar cuántas filas quedan
[1] 56 10
Code
# 2. Reemplazar NA con un valor (imputación simple)
# Aquí reemplazamos NA en presión sistólica con la media
media_sistolica <- mean(datos_pacientes$presion_sistolica, na.rm = TRUE)
datos_pacientes$presion_sistolica_imp <- ifelse(
  is.na(datos_pacientes$presion_sistolica),
  media_sistolica,
  datos_pacientes$presion_sistolica
)

# Verificar que no hay NA en la nueva columna
sum(is.na(datos_pacientes$presion_sistolica_imp))
[1] 0
La importancia de entender el mecanismo de los datos faltantes

En bioestadística, se distinguen tres tipos de mecanismos de valores faltantes:

  1. MCAR (Missing Completely At Random): La probabilidad de que un valor falte no depende de ninguna variable. Este es el escenario ideal pero poco común.

  2. MAR (Missing At Random): La probabilidad de datos faltantes depende de variables observadas. Por ejemplo, los pacientes de mayor edad pueden tener más valores faltantes en pruebas físicas.

  3. MNAR (Missing Not At Random): La probabilidad de datos faltantes depende del valor que falta. Por ejemplo, las personas con valores extremos de presión arterial pueden abandonar el estudio.

El tipo de mecanismo de datos faltantes determina qué estrategias de manejo son apropiadas. La eliminación de casos completos solo es segura bajo MCAR, mientras que para MAR se puede usar imputación múltiple, y para MNAR se necesitan modelos específicos.

2. Corrección de valores imposibles o códigos especiales

Code
# Reemplazar códigos especiales (-99) en peso con NA
datos_pacientes$peso_limpio <- ifelse(datos_pacientes$peso == -99, NA, datos_pacientes$peso)

# Identificar y corregir alturas imposibles
table(datos_pacientes$altura == 0)  # Ver cuántos valores son 0

FALSE  TRUE 
   97     3 
Code
datos_pacientes$altura_limpia <- ifelse(datos_pacientes$altura <= 0, NA, datos_pacientes$altura)

# Lo mismo para presión arterial
datos_pacientes$presion_sistolica_limpia <- ifelse(
  datos_pacientes$presion_sistolica <= 0 | datos_pacientes$presion_sistolica > 250, 
  NA, 
  datos_pacientes$presion_sistolica
)

# Verificar cambios
summary(datos_pacientes[, c("altura", "altura_limpia", "presion_sistolica", "presion_sistolica_limpia")])
     altura      altura_limpia   presion_sistolica presion_sistolica_limpia
 Min.   :0.000   Min.   :1.440   Min.   :  0.0     Min.   :100.0           
 1st Qu.:1.570   1st Qu.:1.570   1st Qu.:123.2     1st Qu.:127.5           
 Median :1.650   Median :1.660   Median :152.0     Median :152.0           
 Mean   :1.597   Mean   :1.646   Mean   :147.0     Mean   :151.6           
 3rd Qu.:1.722   3rd Qu.:1.730   3rd Qu.:177.0     3rd Qu.:177.5           
 Max.   :1.900   Max.   :1.900   Max.   :200.0     Max.   :200.0           
                 NA's   :3       NA's   :2         NA's   :5               

3. Estandarización de categorías inconsistentes

Un problema muy común en datos de salud es la inconsistencia en cómo se registran las categorías:

Code
# Ver las diferentes categorías en sexo y fumador
table(datos_pacientes$sexo, useNA = "ifany")

        f         F  Femenino         m         M Masculino      <NA> 
       19        13        13        15        18        11        11 
Code
table(datos_pacientes$fumador, useNA = "ifany")

    N    No NS/NC     S    Si    Sí  <NA> 
    9    11    11    15    20    15    19 
Code
# Estandarizar la variable sexo
datos_pacientes$sexo_std <- toupper(substr(datos_pacientes$sexo, 1, 1))
datos_pacientes$sexo_std[!(datos_pacientes$sexo_std %in% c("M", "F"))] <- NA
datos_pacientes$sexo_std <- factor(datos_pacientes$sexo_std, levels = c("F", "M"))

# Estandarizar la variable fumador
datos_pacientes$fumador_std <- toupper(substr(datos_pacientes$fumador, 1, 1))
datos_pacientes$fumador_std[!(datos_pacientes$fumador_std %in% c("S", "N"))] <- NA
datos_pacientes$fumador_std <- factor(datos_pacientes$fumador_std, 
                                      levels = c("N", "S"), 
                                      labels = c("No", "Sí"))

# Verificar las nuevas variables estandarizadas
table(datos_pacientes$sexo_std, useNA = "ifany")

   F    M <NA> 
  45   44   11 
Code
table(datos_pacientes$fumador_std, useNA = "ifany")

  No   Sí <NA> 
  31   50   19 
Herramientas para estandarizar variables categóricas

Además de los métodos básicos mostrados, existen paquetes que facilitan la estandarización:

  1. forcats: Especializado en manipulación de factores

    library(forcats)
    # Unir niveles similares
    datos$diagnostico <- fct_collapse(datos$diagnostico,
      "Cardiovascular" = c("Hipertensión", "Insuficiencia cardíaca", "Cardiopatía"),
      "Respiratorio" = c("EPOC", "Asma", "Neumonía")
    )
  2. stringdist: Para encontrar coincidencias aproximadas

    library(stringdist)
    # Encuentra el valor estándar más cercano
    estandarizar <- function(valor, estandares) {
      if (is.na(valor)) return(NA)
      i <- which.min(stringdist(valor, estandares))
      return(estandares[i])
    }
  3. janitor: Para limpiar nombres de variables

    library(janitor)
    datos_limpios <- clean_names(datos)  # estandariza nombres de columnas

4. Conversión a tipos de datos adecuados

Es importante asegurarse de que las variables tengan el tipo de dato correcto:

Code
# Convertir diagnóstico a factor (variable categórica)
datos_pacientes$diagnostico <- factor(datos_pacientes$diagnostico)

# Asegurarse de que fecha_ingreso es tipo fecha
class(datos_pacientes$fecha_ingreso)
[1] "Date"
Code
# En caso de que no fuera tipo fecha:
# datos_pacientes$fecha_ingreso <- as.Date(datos_pacientes$fecha_ingreso)

# Verificar la estructura actualizada
str(datos_pacientes[, c("diagnostico", "fecha_ingreso")])
'data.frame':   100 obs. of  2 variables:
 $ diagnostico  : Factor w/ 5 levels "Asma","Diabetes",..: 5 1 1 2 5 2 2 1 NA 5 ...
 $ fecha_ingreso: Date, format: "2023-01-26" "2023-09-26" ...

5. Creación de variables derivadas

A menudo necesitamos crear nuevas variables basadas en las existentes:

Code
# Crear variable IMC a partir de peso y altura
datos_pacientes$imc <- datos_pacientes$peso_limpio / (datos_pacientes$altura_limpia^2)

# Categorizar el IMC según criterios estándar
datos_pacientes$categoria_imc <- cut(
  datos_pacientes$imc,
  breaks = c(0, 18.5, 25, 30, Inf),
  labels = c("Bajo peso", "Normal", "Sobrepeso", "Obesidad"),
  right = FALSE
)

# Crear variable de mes de ingreso (útil para análisis temporales)
datos_pacientes$mes_ingreso <- format(datos_pacientes$fecha_ingreso, "%m")
datos_pacientes$mes_ingreso <- factor(datos_pacientes$mes_ingreso, 
                                      levels = 1:12,
                                      labels = month.abb)  # Abreviaturas de meses

# Verificar las nuevas variables
summary(datos_pacientes[, c("imc", "categoria_imc", "mes_ingreso")])
      imc           categoria_imc  mes_ingreso
 Min.   : 8.236   Bajo peso:10    Oct    : 9  
 1st Qu.:20.209   Normal   :36    Nov    : 6  
 Median :24.416   Sobrepeso:18    Dec    : 6  
 Mean   :25.845   Obesidad :26    Jan    : 0  
 3rd Qu.:30.751   NA's     :10    Feb    : 0  
 Max.   :49.540                   (Other): 0  
 NA's   :10                       NA's   :79  

Datos “tidy” (ordenados)

El concepto de datos “tidy” o datos ordenados es fundamental en el análisis moderno con R. Según este principio, un conjunto de datos está ordenado cuando:

  1. Cada variable forma una columna
  2. Cada observación forma una fila
  3. Cada tipo de unidad observacional forma una tabla
¿Por qué es crucial el formato “tidy” en investigación sanitaria?

El formato “tidy” es particularmente valioso en análisis de datos sanitarios por varias razones:

  1. Facilita el análisis longitudinal: Datos de seguimiento de pacientes a lo largo del tiempo son más manejables en formato largo.

  2. Mejora la integración de datos: Facilita la combinación de datos de diferentes fuentes (historias clínicas, registros de laboratorio, etc.).

  3. Simplifica las visualizaciones: Es más directo crear gráficos de múltiples variables con ggplot2 usando datos “tidy”.

  4. Optimiza el modelado estadístico: La mayoría de funciones de modelado en R (lm(), glm(), survival::coxph()) esperan datos en formato “tidy”.

  5. Reduce errores de análisis: La estructura consistente minimiza confusiones sobre qué representa cada valor.

Veamos un ejemplo de datos que no están en formato “tidy” y cómo transformarlos:

Code
# Datos no tidy (wide format) - Mediciones de presión arterial en diferentes visitas
pacientes_wide <- data.frame(
  id = 1:5,
  nombre = c("Ana", "Beto", "Carmen", "Diego", "Elena"),
  visita1_sistolica = c(120, 135, 142, 118, 155),
  visita1_diastolica = c(80, 85, 95, 75, 90),
  visita2_sistolica = c(118, 133, 145, 120, 150),
  visita2_diastolica = c(78, 84, 97, 78, 88)
)

pacientes_wide

Estos datos no están en formato “tidy” porque tenemos múltiples variables (presión sistólica y diastólica de diferentes visitas) esparcidas en varias columnas. Transformemos a formato “tidy”:

Code
# Transformar a formato tidy (long format)
library(tidyr)

pacientes_long <- pacientes_wide %>%
  pivot_longer(
    cols = starts_with("visita"),
    names_to = c("visita", "tipo_presion"),
    names_pattern = "visita(.*)_(.*)",
    values_to = "valor"
  )

pacientes_long

Ahora cada fila representa una observación única: un paciente, en una visita específica, con un tipo específico de medición. Este formato facilita muchos análisis.

También podemos volver al formato original si es necesario:

Code
# Volver al formato wide
pacientes_wide_again <- pacientes_long %>%
  pivot_wider(
    names_from = c(visita, tipo_presion),
    names_prefix = "visita",
    names_sep = "_",
    values_from = valor
  )

head(pacientes_wide_again)

Introducción a dplyr para manipulación de datos

dplyr es un paquete del tidyverse que proporciona un conjunto de funciones para manipular fácilmente data frames. Dos de sus funciones más básicas son filter() para seleccionar filas y select() para seleccionar columnas.

Filtrar filas con filter()

Code
library(dplyr)

# Seleccionar solo pacientes mayores de 65 años
pacientes_mayores <- datos_pacientes %>%
  filter(edad > 65)

# Ver cuántos pacientes cumplen el criterio
nrow(pacientes_mayores)
[1] 28
Code
# Filtros con múltiples condiciones
pacientes_riesgo <- datos_pacientes %>%
  filter(edad > 50 & fumador_std == "Sí" & presion_sistolica_limpia > 140)

# Ver cuántos pacientes están en este grupo de riesgo
nrow(pacientes_riesgo)
[1] 8
Code
# Excluir pacientes con valores faltantes en diagnóstico
pacientes_diagnostico_completo <- datos_pacientes %>%
  filter(!is.na(diagnostico))

nrow(pacientes_diagnostico_completo)
[1] 85

Seleccionar columnas con select()

Code
# Seleccionar solo algunas columnas de interés
datos_simplificados <- datos_pacientes %>%
  select(id, edad, sexo_std, diagnostico, imc, categoria_imc)

head(datos_simplificados)
Code
# Seleccionar columnas por patrón
columnas_presion <- datos_pacientes %>%
  select(id, contains("presion"))

head(columnas_presion)
Code
# Excluir columnas
datos_sin_originales <- datos_pacientes %>%
  select(-sexo, -fumador, -peso, -altura, -presion_sistolica, -presion_diastolica)

# Ver qué columnas quedaron
names(datos_sin_originales)
 [1] "id"                       "edad"                    
 [3] "diagnostico"              "fecha_ingreso"           
 [5] "presion_sistolica_imp"    "peso_limpio"             
 [7] "altura_limpia"            "presion_sistolica_limpia"
 [9] "sexo_std"                 "fumador_std"             
[11] "imc"                      "categoria_imc"           
[13] "mes_ingreso"             

Combinar filter y select

Podemos encadenar operaciones con el operador pipe %>%:

Code
# Pacientes obesos con hipertensión, solo columnas relevantes
pacientes_obesos_hipertension <- datos_pacientes %>%
  filter(categoria_imc == "Obesidad" & diagnostico == "Hipertensión") %>%
  select(id, edad, sexo_std, imc, presion_sistolica_limpia, presion_diastolica)

head(pacientes_obesos_hipertension)

Ejercicio práctico

Vamos a trabajar con datos de COVID-19 como ejemplo de un conjunto de datos real. Para este ejercicio, usaremos datos resumidos por país:

Code
# Cargar datos de COVID-19 desde el paquete coronavirus
if (!requireNamespace("coronavirus", quietly = TRUE)) {
  install.packages("coronavirus")
}
library(coronavirus)
Code
# Usar datos del paquete
data(coronavirus)

# Ver estructura
glimpse(coronavirus)
Rows: 973,836
Columns: 15
$ date           <date> 2020-01-22, 2020-01-23, 2020-01-24, 2020-01-25, 2020-0…
$ province       <chr> "Alberta", "Alberta", "Alberta", "Alberta", "Alberta", …
$ country        <chr> "Canada", "Canada", "Canada", "Canada", "Canada", "Cana…
$ lat            <dbl> 53.9333, 53.9333, 53.9333, 53.9333, 53.9333, 53.9333, 5…
$ long           <dbl> -116.5765, -116.5765, -116.5765, -116.5765, -116.5765, …
$ type           <chr> "confirmed", "confirmed", "confirmed", "confirmed", "co…
$ cases          <dbl> 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0…
$ uid            <dbl> 12401, 12401, 12401, 12401, 12401, 12401, 12401, 12401,…
$ iso2           <chr> "CA", "CA", "CA", "CA", "CA", "CA", "CA", "CA", "CA", "…
$ iso3           <chr> "CAN", "CAN", "CAN", "CAN", "CAN", "CAN", "CAN", "CAN",…
$ code3          <dbl> 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, 124, …
$ combined_key   <chr> "Alberta, Canada", "Alberta, Canada", "Alberta, Canada"…
$ population     <dbl> 4413146, 4413146, 4413146, 4413146, 4413146, 4413146, 4…
$ continent_name <chr> "North America", "North America", "North America", "Nor…
$ continent_code <chr> "NA", "NA", "NA", "NA", "NA", "NA", "NA", "NA", "NA", "…

Ejercicio: Preparar dataset para análisis

Realiza las siguientes tareas:

  1. Filtrar solo los casos confirmados de marzo a mayo de 2020
  2. Agregar casos por país y fecha
  3. Crear una variable para indicar si el país está en América Latina
  4. Identificar los 10 países con más casos acumulados en ese periodo
Aproximación paso a paso a problemas de datos complejos

Cuando te enfrentes a tareas de manipulación de datos complejas:

  1. Comienza con subconjuntos pequeños: Prueba tu código con un pequeño subconjunto de datos para verificar que funciona como esperas.

  2. Divide la tarea en pasos más pequeños: Resuelve y verifica cada subtarea antes de pasar a la siguiente.

  3. Encadena operaciones con el pipe (%>%) cuando sea posible: Esto hace que tu código sea más legible y evita la creación de objetos intermedios.

  4. Guarda checkpoints intermedios: En análisis complejos, guarda versiones intermedias de tus datos (ej. datos_filtrados, datos_agregados) para poder revisar y depurar.

  5. Haz verificaciones de sanidad: Confirma que tus transformaciones producen los resultados esperados (totales correctos, número de filas adecuado, etc.).

Code
# 1. Filtrar casos confirmados de marzo a mayo 2020
covid_confirmados <- coronavirus %>%
  filter(
    type == "confirmed",
    date >= as.Date("2020-03-01"),
    date <= as.Date("2020-05-31")
  )

# 2. Agregar casos por país y fecha
covid_por_pais <- covid_confirmados %>%
  group_by(country, date) %>%
  summarise(casos_diarios = sum(cases, na.rm = TRUE), .groups = "drop")

# 3. Crear variable para América Latina
paises_latam <- c(
  "Brazil", "Mexico", "Argentina", "Chile", "Colombia", 
  "Peru", "Ecuador", "Bolivia", "Venezuela", "Uruguay",
  "Paraguay", "Cuba", "Dominican Republic", "Panama", 
  "Costa Rica", "Guatemala", "Honduras", "El Salvador", "Nicaragua"
)

covid_por_pais <- covid_por_pais %>%
  mutate(es_latam = country %in% paises_latam)

# 4. Identificar los 10 países con más casos acumulados
top10_paises <- covid_por_pais %>%
  group_by(country) %>%
  summarise(casos_totales = sum(casos_diarios, na.rm = TRUE)) %>%
  arrange(desc(casos_totales)) %>%
  slice_head(n = 10)

# Ver los resultados
top10_paises

Exportación de datos limpios

Después de limpiar y transformar los datos, a menudo queremos guardarlos para análisis posteriores:

Code
# Exportar a CSV
write_csv(datos_pacientes, "datos_pacientes_limpios.csv")

# Exportar a Excel
writexl::write_xlsx(datos_pacientes, "datos_pacientes_limpios.xlsx")

# Exportar a formato RDS (específico de R, mantiene tipos de datos)
saveRDS(datos_pacientes, "datos_pacientes_limpios.rds")
Tip

El formato RDS es ideal para almacenar objetos de R porque preserva la estructura exacta y los tipos de datos, incluyendo factores y atributos. Para compartir datos con personas que no usan R, los formatos CSV o Excel son más adecuados.

Documentación de la limpieza de datos

En investigación clínica y epidemiológica, es crucial documentar todos los pasos de limpieza y transformación para garantizar la reproducibilidad y transparencia:

  1. Crea un “diario de limpieza”: Documenta cada decisión tomada (por ejemplo, “Se eliminaron valores de presión arterial sistólica <60 y >250 mmHg por considerarse errores de entrada”).

  2. Guarda los datos en múltiples etapas:

    • Datos crudos originales (nunca modificados)
    • Datos después de la limpieza básica
    • Datos finales para análisis
  3. Script reproducible: Asegúrate de que todo tu proceso de limpieza está en scripts que pueden ser ejecutados de principio a fin para recrear los datos finales.

  4. Versiona tus datos: Usa control de versiones (como Git) o al menos nombra tus archivos con fechas (ej. datos_clinicos_limpios_20250319.csv).

Estos pasos no solo mejoran la calidad de la investigación, sino que también son cada vez más requeridos por revistas científicas y organismos financiadores.

Recursos adicionales

Para profundizar en la importación y limpieza de datos:

Para la próxima sesión

En la próxima sesión profundizaremos en la manipulación de datos con el tidyverse, especialmente con dplyr y tidyr:

  • Usar mutate() para crear y transformar variables
  • Resumir datos con summarise() y group_by()
  • Unir tablas con funciones de join
  • Realizar operaciones más complejas de reestructuración de datos

Conclusiones

En esta sesión hemos aprendido:

  • Cómo importar datos desde diferentes formatos (CSV, Excel)
  • Técnicas para inspeccionar y comprender la estructura de un dataset
  • Métodos para identificar y corregir problemas comunes en datos crudos
  • El concepto de datos “tidy” y cómo transformar entre formatos
  • Funciones básicas de dplyr para filtrar y seleccionar datos

La limpieza de datos, aunque a veces tediosa, es un paso crucial que puede determinar la validez de todo el análisis posterior. En investigación en salud, donde las decisiones basadas en datos pueden afectar la vida de las personas, es especialmente importante garantizar la calidad de los datos antes de proceder con análisis estadísticos.

¡Nos vemos en la próxima sesión!