Funciones condicionales en R

Autores/as

Christian Ballejo

Tamara Ricardo

Introducción

Continuamos incorporando funciones útiles para el manejo de datos, enfocándonos en la creación de nuevas variables y en la transformación de variables cuantitativas a variables categóricas, ya sean nominales u ordinales.

Algunas de estas operaciones serán necesarias para alcanzar los objetivos del trabajo práctico grupal de esta unidad, y seguramente también se utilizarán en las unidades siguientes.

Como vimos dentro del universo tidyverse, para crear nuevas variables a partir de cálculos utilizamos la función-verbo mutate():

datos <- datos  |>   
  mutate(variable_nueva = funcion(var1))

Frecuentemente necesitaremos agrupar, resumir o discretizar variables continuas, dividiendo su rango en categorías que pueden ser dicotómicas o politómicas, según los objetivos del análisis.

Condicional simple: if_else()

Para obtener salidas dicotómicas, podemos usar la función condicional if_else() del paquete dplyr. Esta función es una versión simplificada y segura del clásico condicional if que existe en la mayoría de los lenguajes de programación, así como en hojas de cálculo como Microsoft Excel® y Google Sheets®, entre otras.

Supongamos que tenemos un dataframe llamado datos, con una variable cuantitativa denominada var1:

# Cargar tidyverse
library(tidyverse)

# Datos ejemplo
set.seed(123)

datos <- tibble(
  var1 = sample(1:90, size = 50, replace = TRUE)
  )

Queremos crear una variable cualitativa dentro de datos, llamada variable_nueva, cuyos valores estén dados por el cumplimiento de alguna condición de var1. Por ejemplo, si los valores de var1 son mayores a 10, entonces variable_nueva, tomará el valor “mayor a 10”, en caso contrario, tomará el valor “menor o igual a 10”:

# Crear variable_nueva
datos <- datos |>   
  mutate(variable_nueva = if_else(condition = var1 > 10,  
                                  true = "mayor a 10",  
                                  false = "menor o igual a 10"))

# Explorar variable_nueva
tabyl(datos, variable_nueva)
     variable_nueva  n percent
         mayor a 10 44    0.88
 menor o igual a 10  6    0.12

La función if_else() tiene tres argumentos obligatorios:

  • condition: la condición a evaluar.

  • true: valor que tomará la nueva variable si se cumple la condición.

  • false: valor que tomará la nueva variable si no se cumple la condición.

Este proceso se conoce como dicotomización de una variable, ya que el resultado posible consta de solo dos categorías.

Los valores de salida pueden ser de distintos tipos (carácter, numérico o lógico). Sin embargo, cuando discretizamos una variable cuantitativa, habitualmente generamos una variable cualitativa de tipo ordinal, con categorías expresadas como texto (tipo character).

Ahora bien, al ser ordinal estas categorías de la variable_nueva deben “ordenarse” en la forma de los valores de la variable, pero el lenguaje R no sabe con que estamos trabajando y respeta siempre el ordenamiento alfanumérico. Por lo tanto, en este ejemplo las categorías se van a estar ordenando al reves del orden numérico natural (de menor a mayor).

La categoría "mayor a 10” se ordena alfabéticamente antes de “menor o igual a 10”, porque luego del empate de las letras m, le siguen la a en el primer caso y la e en el segundo.

Para ordenar estas categorías debemos transformar la variable de character a factor. Esto se puede hacer en un solo paso dentro del mutate():

datos <- datos |>   
  mutate(variable_nueva = if_else(condition = var1 > 10,  
                                  true = "mayor a 10",  
                                  false = "menor o igual a 10"),
         
         variable_nueva = factor(variable_nueva,
                                 levels = c("menor o igual a 10", 
                                            "mayor a 10")))

# Explorar variable_nueva
tabyl(datos, variable_nueva)
     variable_nueva  n percent
 menor o igual a 10  6    0.12
         mayor a 10 44    0.88

Otra forma más artesanal, igualmente válido, es “forzar” el ordenamiento con las categorías así:

datos <- datos |>   
  mutate(variable_nueva = if_else(condition = var1 > 10,  
                                  true = "2.mayor a 10",  
                                  false = "1.menor o igual a 10"))

# Explorar variable_nueva
tabyl(datos, variable_nueva)
       variable_nueva  n percent
 1.menor o igual a 10  6    0.12
         2.mayor a 10 44    0.88

Aquí agregamos números iniciales a las etiquetas de las categorías para darle el orden que deseamos, sin necesidad de convertir a factor.

Función cut_interval()

El ecosistema tidyverse ofrece la función cut_interval() para la creación de intervalos regulares.

Es una adaptación de la función cut() de R base para tidy data y sus argumentos son similares. Volviendo al ejemplo anterior:

datos <- datos |> 
  mutate(grupo_var = cut_interval(x = var1,
                                  length = 10,
                                  right = TRUE, 
                                  ordered_result = TRUE)
         )

# Explora grupo_var
tabyl(datos, grupo_var)
 grupo_var n percent
    [0,10] 6    0.12
   (10,20] 4    0.08
   (20,30] 7    0.14
   (30,40] 6    0.12
   (40,50] 6    0.12
   (50,60] 5    0.10
   (60,70] 4    0.08
   (70,80] 7    0.14
   (80,90] 5    0.10

Los argumentos función son:

  • x: conjunto de datos numéricos de entrada (obligatorio).

  • length: longitud de cada intervalo regular (obligatorio).

  • n: cantidad de intervalos a crear (obligatorio, omitir si se definió length).

  • right: indica si los intervalos son cerrados a la derecha o viceversa (valor por defecto TRUE, intervalos cerrados a la derecha).

  • labels: etiquetas de los intervalos automáticas o numéricas (por defecto intervalos matemáticos).

  • ordered_result: determina si el resultado es un factor ordenado. Por defecto vale FALSE (la salida es tipo carácter).

No es necesario definir los argumentos opcionales siempre y cuando los valores por defecto sirvan para nuestra tarea.

Función case_when()

Cuando las condiciones no son simples, es decir, el resultado no es dicotómico y además los intervalos son irregulares, utilizamos la función case_when() que es una vectorización de la función if_else().

Supongamos que queremos agrupar la variable cuantitativa de números enteros (var1) en tres grupos irregulares:

datos <- datos |> 
  mutate(grupo_var = case_when( 
    var1 %in% c(0:24)  ~  "Grupo 1", 
    var1 %in% c(25:64) ~    "Grupo 2", 
    var1 >= 65 ~ "Grupo 3")
    )

# Explora grupo_var
tabyl(datos, grupo_var)
 grupo_var  n percent
   Grupo 1 12    0.24
   Grupo 2 23    0.46
   Grupo 3 15    0.30

Existe una condición por cada grupo creado, como si fuese un if_else() donde el valor declarado siempre es el verdadero. Se utilizan operadores de comparación como mayor ( > ), menor ( < ) y/o igual ( == ) y conectores lógicos como & (AND), | (NOT) e %in%. En cada línea va una virgulilla similar a la usada en la sintaxis formula (~) y luego la etiqueta que tomarán las observaciones que cumplan con esa condición en la nueva variable (grupo_var).

Esta evaluación es secuencial y su funcionamiento provoca que el usuario del lenguaje tenga el control de lo que esta sucediendo, por lo que cualquier mala definición de las condiciones puede provocar resultados incorrectos.

Si incorporamos el argumento .default podemos indicar que valor toma si no se cumple ninguna de las condiciones anteriores.

Por ejemplo, podríamos tener algún valor perdido (NA) en var1 y queremos que la variable grupo_var etiquete esos valores perdidos como "Sin dato".

datos <- datos |> 
  # Generar valores NA
  mutate(var1 = na_if(var1, 50)) |> 
  
  mutate(grupo_var = case_when( 
    var1 %in% c(0:24)  ~  "Grupo 1", 
    var1 %in% c(25:64) ~    "Grupo 2", 
    var1 >= 65 ~ "Grupo 3",
    .default = "Sin dato")
    )

# Explora grupo_var
tabyl(datos, grupo_var)
 grupo_var  n percent
   Grupo 1 12    0.24
   Grupo 2 22    0.44
   Grupo 3 15    0.30
  Sin dato  1    0.02

Otra forma de definir los valores que no cumplen con ninguna condición es usando TRUE ~ valor:

datos <- datos |> 
  # Generar valores NA
  mutate(var1 = na_if(var1, 50)) |> 
  
  mutate(grupo_var = case_when( 
    var1 %in% c(0:24)  ~  "Grupo 1", 
    var1 %in% c(25:64) ~    "Grupo 2", 
    var1 >= 65 ~ "Grupo 3",
    TRUE ~ "Sin dato")
    )

# Explora grupo_var
tabyl(datos, grupo_var)
 grupo_var  n percent
   Grupo 1 12    0.24
   Grupo 2 22    0.44
   Grupo 3 15    0.30
  Sin dato  1    0.02

Las salidas son de tipo carácter (chr) y debemos manejar el ordenamiento de las etiquetas como vimos anteriormente, por medio de factores o comenzando con caracteres ordenados alfabéticamente.

Para simplificar el trabajo de estos intervalos de clase irregulares y no provocar errores en la confección de las condiciones, tidyverse tiene a la función between().

Intervalos: función between()

La función between() básicamente opera como un atajo para condiciones de intervalos. Define dentro de los argumentos los límites inferior y superior de un intervalo y se utiliza dentro de una función de condición tipo if_else() o case_when().

Aplicado sobre el ejemplo anterior se vería así:

datos <- datos |> 
  mutate(grupo_var = case_when( 
    between(var1, 0, 24)   ~  "Grupo 1", 
        between(var1, 25, 64)  ~    "Grupo 2", 
        between(var1, 65, Inf) ~    "Grupo 3",
        .default = "Sin dato"))

# Explora grupo_var
tabyl(datos, grupo_var)
 grupo_var  n percent
   Grupo 1 12    0.24
   Grupo 2 22    0.44
   Grupo 3 15    0.30
  Sin dato  1    0.02

Los valores declarados como límites quedan incluidos siempre dentro del intervalo (son cerrados ambos). También podemos utilizar valores reservados como Inf o -Inf cuando desconocemos con que valor máximo o mínimo nos vamos a encontrar en la variable cuantitativa original.

Intervalos: función age_categories()

El paquete epikit (Spina, Kamvar, y Schumacher 2024), contiene herramientas útiles para trabajar con datos en salud públicas. La función age_categories(), transforma una variable numérica discreta en grupos de edad. Volviendo al ejemplo anterior, supongamos que var1 contiene edades de pacientes:

# Cargar paquete
library(epikit)

datos <- datos |> 
  mutate(edad_cat = age_categories(x = var1,
                                   lower = 1,
                                   upper = 80,
                                   by = 10,
                                   separator = " a ",
                                   above.char = " y más"))

# Explora edad_cat
tabyl(datos, edad_cat)
 edad_cat n percent valid_percent
   1 a 10 6    0.12    0.12244898
  11 a 20 4    0.08    0.08163265
  21 a 30 7    0.14    0.14285714
  31 a 40 6    0.12    0.12244898
  41 a 50 5    0.10    0.10204082
  51 a 60 5    0.10    0.10204082
  61 a 70 4    0.08    0.08163265
  71 a 79 7    0.14    0.14285714
 80 y más 5    0.10    0.10204082
     <NA> 1    0.02            NA

Los argumentos principales de la función son:

  • x: vector numérico (obligatorio).

  • lower: límite inferior de edades (por defecto 0).

  • upper: límite superior de edades.

  • by: intervalo de años para los grupos.

  • breakers: alternativo a by, para definir manualmente los grupos.

  • separator: carácter que separa el rango de edades (por defecto "-").

  • ceiling: define si incluir el valor más alto en los grupos (por defecto FALSE).

  • above.char: el valor que queremos que aparezca para las edades por fuera del grupo de edad más alto (por defecto +). Solo funciona si ceiling = FALSE.

Ejemplo práctico en R

Tomemos un caso clásico como la variable edad medida en años, variable que generalmente tenemos en toda tabla de datos vinculada a personas. Trabajaremos con la base de datos “edad.txt” que contiene 106 observaciones de pacientes:

# Cargar dataset
datos <- read_csv2("datos/edad.txt")

# Explorar datos
glimpse(datos)
Rows: 106
Columns: 3
$ id_mujer         <dbl> 115971, 1689710, 114710, 3058900, 3223547, 3234027, 3…
$ fecha_nacimiento <chr> "30/05/1967", "23/04/1991", "06/11/1962", "09/03/1964…
$ fecha_test       <chr> "17/01/2022", "01/02/2022", "16/03/2022", "23/02/2022…

A continuación, vamos a transformar las variables cuyo nombre contenga “fecha_” a formato fecha para poder calcular edades:

datos <- datos |> 
  # Transformar a formato fecha
  mutate(across(.cols = starts_with("fecha_"),
                .fns = ~ dmy(.x))) |> 
  # Calcular edad al momento del testeo
  mutate(edad = round(
    as.duration(fecha_test - fecha_nacimiento) / dyears(1)))

Una primera posibilidad es dicotomizar la edad usando el valor de la mediana:

datos |>  
  summarise(mediana = median(edad))
# A tibble: 1 × 1
  mediana
    <dbl>
1      56

Aplicando el valor de la mediana dentro de un if_else() podríamos hacer:

datos <- datos |>  
  mutate(grupo_edad1 = if_else(condition = edad > 56, 
                                  true = "mayor a la mediana", 
                                  false = "menor o igual a la mediana"))

# Explorar grupo_edad1
datos |> 
  count(grupo_edad1)
# A tibble: 2 × 2
  grupo_edad1                    n
  <chr>                      <int>
1 mayor a la mediana            52
2 menor o igual a la mediana    54

Observamos en el conteo que grupo_edad1 se construyó adecuadamente pero el orden de los niveles no es correcto si queremos que siga el ordenamiento natural de edad (de menor a mayor).

Una de las formas que vimos es convertir a factor:

datos <- datos |> 
  mutate(grupo_edad1 = if_else(condition = edad > 56, 
                                  true = "mayor a la mediana", 
                                  false = "menor o igual a la mediana"),
         grupo_edad1 = factor(grupo_edad1, 
                                 levels = c("menor o igual a la mediana",
                                            "mayor a la mediana")))

# Explorar grupo_edad1
datos |> 
  count(grupo_edad1)
# A tibble: 2 × 2
  grupo_edad1                    n
  <fct>                      <int>
1 menor o igual a la mediana    54
2 mayor a la mediana            52

Vemos que en el conteo el formato de la variable ya no es chr sino fct y el orden de las etiquetas siguen la forma “menor a mayor”.

Otra forma es:

datos <- datos |> 
  mutate(grupo_edad1 = if_else(condition = edad > 56, 
                                  true = "2.mayor a la mediana", 
                                  false = "1.menor o igual a la mediana"))

# Explorar grupo_edad1
datos |> 
  count(grupo_edad1)
# A tibble: 2 × 2
  grupo_edad1                      n
  <chr>                        <int>
1 1.menor o igual a la mediana    54
2 2.mayor a la mediana            52

Si en cambio necesitamos que los grupos sean mas de dos y que estos intervalos de clase sean regulares, podemos usar cut_interval():

datos <- datos |>  
  mutate(grupo_edad2 = cut_interval(x = edad, 
                                    length = 10))

# Explorar grupo_edad2
datos |> 
  count(grupo_edad2)
# A tibble: 8 × 2
  grupo_edad2     n
  <fct>       <int>
1 [0,10]          3
2 (10,20]         3
3 (20,30]         2
4 (30,40]         3
5 (40,50]        13
6 (50,60]        52
7 (60,70]        27
8 (70,80]         3

La salida muestra ocho grupos etarios con etiquetas ordenadas con notación matemática, donde un corchete indica que el límite del intervalo es cerrado, es decir contiene el valor y un paréntesis es abierto y no lo hace.Así es que el primer grupo va de 0 a 10 años y el segundo de 11 a 20.

Estos sucede así porque en forma predeterminada el argumento right = TRUE. Veamos que pasa si lo cambiamos a FALSE:

datos <- datos |>  
  mutate(grupo_edad2 = cut_interval(x = edad, 
                                    length = 10,
                                    right = F))

# Explorar grupo_edad2
datos |> 
  count(grupo_edad2)
# A tibble: 8 × 2
  grupo_edad2     n
  <fct>       <int>
1 [0,10)          3
2 [10,20)         3
3 [20,30)         2
4 [30,40)         3
5 [40,50)        10
6 [50,60)        48
7 [60,70)        32
8 [70,80]         5

En esta salida el primer grupo va de 0 a 9 y el segundo de 10 a 19.

Hasta ahora la variable grupo_edad2 es de tipo carácter, pero si deseamos que la salida sea factor podemos incorporar el argumento ordered_result = TRUE:

datos <- datos |> 
  mutate(grupo_edad2 = cut_interval(x = edad, 
                                    length = 10,
                                    ordered_result = T))

# Explorar grupo_edad2
datos |> 
  count(grupo_edad2)
# A tibble: 8 × 2
  grupo_edad2     n
  <ord>       <int>
1 [0,10]          3
2 (10,20]         3
3 (20,30]         2
4 (30,40]         3
5 (40,50]        13
6 (50,60]        52
7 (60,70]        27
8 (70,80]         3

Construimos así una variable factor ordenada <ord>.

Por último, con el argumento labels= FALSE hacemos que las etiquetas de los ocho grupos sean numéricas:

datos <- datos |> 
  mutate(grupo_edad2 = cut_interval(x = edad, 
                                    length = 10,
                                    labels = F))

# Explorar grupo_edad2
datos |> 
  count(grupo_edad2)
# A tibble: 8 × 2
  grupo_edad2     n
        <int> <int>
1           1     3
2           2     3
3           3     2
4           4     3
5           5    13
6           6    52
7           7    27
8           8     3

Otro ejemplo, podría ser aplicando case_when() donde discretizamos la edad en cuatro grupos irregulares, forzando sus etiquetas para lograr el orden adecuado:

datos <- datos |>  
  mutate(grupo_edad3 = case_when(
    edad < 13              ~ "1.Niño",
    edad > 12 & edad < 26  ~ "2.Adolescente",
    edad > 25 & edad < 65  ~ "3.Adulto_joven",
    edad > 64              ~ "4.Adulto_mayor"
  ))

# Explorar grupo_edad3
datos |> 
  count(grupo_edad3) 
# A tibble: 4 × 2
  grupo_edad3        n
  <chr>          <int>
1 1.Niño             3
2 2.Adolescente      5
3 3.Adulto_joven    86
4 4.Adulto_mayor    12

Si no hubiésemos etiquetado con los números por delante el orden alfabético hacía que Niño fuese a parar al final del conteo.

De la misma forma pero más sencillo y controlado es:

datos <- datos |>  
  mutate(grupo_edad3 = case_when(
    between(edad, 0, 12)   ~ "1.Niño",
    between(edad, 13, 25)  ~ "2.Adolescente",
    between(edad, 26, 64)  ~ "3.Adulto_joven",
    between(edad, 65, Inf) ~ "4.Adulto_mayor"
  ))

# Explorar grupo_edad3
datos |> 
  count(grupo_edad3)
# A tibble: 4 × 2
  grupo_edad3        n
  <chr>          <int>
1 1.Niño             3
2 2.Adolescente      5
3 3.Adulto_joven    86
4 4.Adulto_mayor    12

Estas funciones condicionales que tratamos en este documento no se limitan a la tarea de construir agrupamientos de variables cuantitativas sino que sirven para cualquier situación donde a partir de una o más condiciones se produzcan una o más valores como respuesta.

Finalmente veamos como quedarían los grupos de edad con la función age_categories():

datos <- datos |>  
  mutate(grupo_edad4 = age_categories(x = edad,
                                      breakers = c(0, 12, 25, 65),
                                      above.char = " y más")
  )

# Explorar grupo_edad4
datos |> 
  count(grupo_edad4)
# A tibble: 4 × 2
  grupo_edad4     n
  <fct>       <int>
1 0-11            3
2 12-24           4
3 25-64          87
4 65 y más       12

En este caso se genera una variable de tipo factor con los grupos de edad ordenados de mayor a menor.

Volver arriba

Referencias

Spina, Alexander, Zhian N. Kamvar, y Dirk Schumacher. 2024. «epikit: Miscellaneous Helper Tools for Epidemiologists». https://CRAN.R-project.org/package=epikit.

Reutilización