Sesión 5

Automatización: funciones, condicionales y simulación

Juan David Leongómez

Universidad El Bosque

11 de junio de 2026

Agenda

  • Funciones en R
  • Condicionales: ifelse() y case_when()
  • Combinar ambas: una función que clasifica
  • ¿Por qué simular datos?
  • Simulación básica con R
  • Simulación de variables correlacionadas con faux
  • Un análisis completo: GAD-7 simulado

Parte 1

Funciones en R

El problema del código repetido

Imagina que quieres crear el mismo histograma con una línea de media para varias variables:

ggplot(datos, aes(x = gad7)) +
  geom_histogram(fill = "#225faa", bins = 22) +
  geom_vline(aes(xintercept = mean(gad7)), colour = "red", linewidth = 1) +
  labs(title = "GAD-7", x = "Puntaje", y = "Frecuencia") + theme_minimal()

ggplot(datos, aes(x = bienestar)) +
  geom_histogram(fill = "#225faa", bins = 30) +
  geom_vline(aes(xintercept = mean(bienestar)), colour = "red", linewidth = 1) +
  labs(title = "Bienestar", x = "Puntaje", y = "Frecuencia") + theme_minimal()

# ... y lo mismo para horas_sueno, edad, rendimiento...

Si decides cambiar el color, el número de bins o el tema, tienes que editar cada bloque por separado, y cada copia es una oportunidad para cometer un error.

Cuando escribes el mismo código más de dos veces, es momento de escribir una función.

Anatomía de una función

nombre <- function(argumento1, argumento2) {
  # cuerpo: lo que hace la función
  resultado
}

Cuatro partes:

  1. Nombre: como cualquier objeto en R, debe ser descriptivo
  2. Argumentos: los valores que recibe (pueden ser cero o varios)
  3. Cuerpo: el código que se ejecuta
  4. Valor de retorno: R devuelve automáticamente el último objeto evaluado

Una función simple

redondear_media <- function(x) {
    round(mean(x, na.rm = TRUE), 2)
}

redondear_media(c(3.5, 4.1, 2.8, 3.9))
redondear_media(c(15, 7, 12, 9, 14))
[1] 3.57
[1] 11.4

También se pueden añadir argumentos con valor por defecto:

redondear_media <- function(x, decimales = 2) {
    round(mean(x, na.rm = TRUE), decimales)
}

redondear_media(c(3.5, 4.1, 2.8, 3.9))
redondear_media(c(3.5, 4.1, 2.8, 3.9), decimales = 0)
[1] 3.57
[1] 4

Actividad · 5 min

Tu primera función

Escribe una función llamada resumen_variable que reciba un vector numérico y devuelva una cadena de texto con la media y la desviación estándar redondeadas a 1 decimal, en el formato "10.0 ± 3.2". Pruébala con c(12, 7, 15, 9, 11, 14, 6).

paste0() concatena cadenas de texto sin separador: paste0("Media: ", 3.5) produce "Media: 3.5". Puedes combinar texto y números en varios puntos:

paste0(
  "De los ", 
  156, 
  " participantes, un tercio (", 
  round(156 / 3), 
  ") no terminaron."
)

Recuerda que la función de la media es mean() y la de la desviación estándar es sd(). Es mejor que ambas incluyan na.rm = TRUE para manejar valores NA (de otro modo, si hay valores NA en el vector, el resultado también será NA). No olvides usar round() para redondear los resultados a un decimal.

Parte 2

Condicionales

if / else: lógica de una sola decisión

puntaje <- 17

if (puntaje >= 15) {
    "Ansiedad severa"
} else {
    "Ansiedad no severa"
}
[1] "Ansiedad severa"

if / else evalúa un solo valor. Si le pasas un vector de varios puntajes, solo evalúa el primero y lanza una advertencia.

ifelse(): la versión vectorizada

ifelse() recibe tres argumentos en orden fijo:

ifelse(condición,       # ① evaluada para cada elemento del vector → TRUE o FALSE
       si_verdadero,    # ② valor devuelto cuando la condición es TRUE
       si_falso)        # ③ valor devuelto cuando la condición es FALSE

Por ejemplo:

puntajes <- c(3, 9, 14, 17, 5, 20)

ifelse(puntajes >= 15, "Severa", "No severa")
[1] "No severa" "No severa" "No severa" "Severa"    "No severa" "Severa"   

Se pueden anidar para más de dos categorías, aunque el código se vuelve difícil de leer:

ifelse(puntajes >= 15, "Severa",
    ifelse(puntajes >= 10, "Moderada", "Leve o mínima")
)

Con tres categorías ya cuesta seguir la lógica;case_when() es la solución.

case_when(): múltiples condiciones

Cuando hay más de dos categorías, ifelse() anidado se vuelve ilegible. case_when() es la alternativa:

library(tidyverse) # o library(dplyr) para case_when

puntajes <- c(3, 9, 14, 17, 5, 20)

case_when(
    puntajes <= 4 ~ "Mínima",
    puntajes <= 9 ~ "Leve",
    puntajes <= 14 ~ "Moderada",
    .default = "Severa"
)
[1] "Mínima"   "Leve"     "Moderada" "Severa"   "Leve"     "Severa"  

Las condiciones se evalúan en orden: cuando una es verdadera, las siguientes se ignoran. TRUE ~ al final actúa como “todo lo demás”: siempre se evalúa como verdadero y captura cualquier valor que no coincidió con las condiciones anteriores.

Parte 1 + 2

Funciones con condicionales

Una función que clasifica puntajes GAD-7

El GAD-7 es una escala de ansiedad generalizada con puntaje 0–21. Los puntos de corte clínicos son:

Puntaje Nivel
0–4 Mínima
5–9 Leve
10–14 Moderada
15–21 Severa

Escribir clasificar_ansiedad()

clasificar_ansiedad <- function(puntaje) {
    case_when(
        puntaje <= 4 ~ "Mínima",
        puntaje <= 9 ~ "Leve",
        puntaje <= 14 ~ "Moderada",
        .default = "Severa"
    )
}

clasificar_ansiedad(c(3, 9, 14, 17, 5, 20))
[1] "Mínima"   "Leve"     "Moderada" "Severa"   "Leve"     "Severa"  

Y una segunda función para calcular la proporción con ansiedad severa:

prop_severa <- function(puntaje) {
    mean(puntaje >= 15)
}

prop_severa(c(3, 9, 14, 17, 5, 20))
[1] 0.3333333

Parte 3

¿Por qué simular datos?

Simulación de datos

Simular datos se trata de generar observaciones artificiales a partir de un modelo con propiedades conocidas o esperadas. No se trata de que lo usemos para fabricar resultados ni presentar datos falsos como reales.

Usos legítimos

  • Practicar R: no necesitas esperar a tener datos propios
  • Explorar distribuciones: entender visualmente qué implica un supuesto estadístico
  • Ver posibles resultados: imaginar escenarios antes de recolectar datos reales
  • Verificar scripts: confirmar que el código funciona antes de tener los datos definitivos
  • Análisis de poder: calcular cuántas observaciones se necesitan para detectar un efecto
  • Preregistro y reportes registrados: escribir y probar el análisis completo antes de recolectar datos

Es lo mismo que un simulador de vuelo para pilotos: permite practicar, probar escenarios y detectar problemas antes de enfrentarse a la situación real.

Parte 4

Simulación básica con R

set.seed(): reproducibilidad

R genera números aleatorios a partir de un estado interno. set.seed() fija ese estado para que la simulación sea reproducible:

# Cada vez genera diferentes números aleatorios
rnorm(5)
[1] -2.7973651  1.2339515  0.2497746 -1.8415668  0.2239075
rnorm(5)
[1]  1.1992776  0.7372381  0.1764415 -0.1727513 -0.7279121
rnorm(5)
[1] -0.5752150  0.9238518  0.8466251 -1.0566388  0.6870487
# Siempre genera los mismos números
set.seed(42)
rnorm(5)
[1]  1.3709584 -0.5646982  0.3631284  0.6328626  0.4042683
set.seed(42)
rnorm(5)
[1]  1.3709584 -0.5646982  0.3631284  0.6328626  0.4042683
set.seed(42)
rnorm(5)
[1]  1.3709584 -0.5646982  0.3631284  0.6328626  0.4042683

Sin set.seed(), cada ejecución produce valores distintos. Con el mismo número de semilla, el resultado es idéntico. El número en sí no importa, solo que sea consistente.

Las funciones básicas de simulación

Función Qué simula
rnorm(n, mean, sd) Distribución normal
runif(n, min, max) Distribución uniforme
sample(x, n, replace) Muestra de un vector
rbinom(n, size, prob) Distribución binomial

Por ejemplo:

set.seed(17)
rnorm(6, mean = 10, sd = 3) # seis valores normales
[1]  6.954974  9.761090  9.301039  7.548196 12.316273  9.503164
runif(6, min = 0, max = 100) # seis valores uniformes
[1] 83.46921 83.00830 95.69678 94.84971 60.07300 26.18074
sample(c("A", "B", "C"), 6, replace = TRUE) # seis letras al azar
[1] "B" "A" "B" "C" "A" "A"

Cómo se ven las distribuciones

rnorm() · runif() · rbinom() — distribuciones paramétricas

set.seed(17)
data.frame(
    Normal   = rnorm(1000, mean = 10, sd = 3),
    Uniforme = runif(1000, min = 0, max = 20),
    Binomial = rbinom(1000, size = 20, prob = 0.3)
) |>
    pivot_longer(everything(),
        names_to = "dist", values_to = "valor"
    ) |>
    mutate(dist = factor(dist,
        levels = c("Normal", "Uniforme", "Binomial")
    )) |>
    ggplot(aes(x = valor, fill = dist)) +
    geom_histogram(binwidth = 1, show.legend = FALSE) +
    facet_wrap(~dist, scales = "free", nrow = 1) +
    scale_fill_manual(values = c(
        "Normal" = "#225faa", "Uniforme" = "#5b9ad5",
        "Binomial" = "#7db87d"
    )) +
    labs(x = NULL, y = "Frecuencia") +
    theme_minimal()

Cómo se ven las distribuciones

sample() — muestrear desde un conjunto existente

data.frame(carrera = sample(
    c(
        "Psicología", "Medicina",
        "Enfermería", "Nutrición"
    ),
    1000,
    replace = TRUE
)) |>
    ggplot(aes(x = carrera, fill = carrera)) +
    geom_bar(show.legend = FALSE) +
    labs(x = NULL, y = "Frecuencia") +
    theme_minimal()

Construir un data.frame simulado

set.seed(123)

datos_simple <- data.frame(
    id = paste0("ID_", str_pad(1:200, width = 3, pad = "0")),
    edad = round(rnorm(200, mean = 22, sd = 3)),
    puntaje = round(runif(200, min = 0, max = 21)),
    carrera = sample(
        c("Psicología", "Medicina", "Enfermería"),
        200,
        replace = TRUE
    )
)

glimpse(datos_simple)
Rows: 200
Columns: 4
$ id      <chr> "ID_001", "ID_002", "ID_003", "ID_004", "ID_005", "ID_006", "I…
$ edad    <dbl> 20, 21, 27, 22, 22, 27, 23, 18, 20, 21, 26, 23, 23, 22, 20, 27…
$ puntaje <dbl> 21, 3, 19, 12, 8, 9, 15, 2, 7, 14, 7, 17, 5, 10, 6, 4, 20, 7, …
$ carrera <chr> "Medicina", "Psicología", "Enfermería", "Enfermería", "Enferme…

Parte 5

Variables correlacionadas con faux

La limitación de rnorm() independiente

Las variables simuladas con rnorm() por separado no tienen ninguna relación entre sí. Pero en datos reales las variables suelen estar correlacionadas: más ansiedad tiende a asociarse con menos bienestar, menos horas de sueño, etc.

Simulación independiente

set.seed(1)
ind <- data.frame(
    gad7      = rnorm(500, mean = 8, sd = 5),
    bienestar = rnorm(500, mean = 65, sd = 15)
)

ggplot(ind, aes(x = gad7, y = bienestar)) +
    geom_point(alpha = 0.3, colour = "#225faa") +
    geom_smooth(method = "lm", colour = "red", se = FALSE) +
    labs(title = paste0("r = ",
                        round(cor(ind$gad7, ind$bienestar), 2)),
        x = "GAD-7", y = "Bienestar") +
    theme_minimal()

Simulación con faux

set.seed(1)
cor_dat <- rnorm_multi(n = 500,
                       mu = c(8, 65), sd = c(5, 15),
                       r = -0.45, empirical = TRUE,
                       varnames = c("gad7", "bienestar")
                      )

ggplot(cor_dat, aes(x = gad7, y = bienestar)) +
    geom_point(alpha = 0.3, colour = "#225faa") +
    geom_smooth(method = "lm", colour = "red", se = FALSE) +
    labs(title = paste0("r = ",
                        round(cor(cor_dat$gad7, cor_dat$bienestar), 2)),
        x = "GAD-7", y = "Bienestar") +
    theme_minimal()

faux::rnorm_multi()

El paquete faux permite especificar directamente las correlaciones entre variables:

library(faux)

datos_cor <- rnorm_multi(
    n         = 500,
    mu        = c(8, 65, 6.5),          # medias
    sd        = c(5, 15, 1.5),          # desviaciones estándar
    r         = c(-0.45, -0.30, 0.20),  # correlaciones: gad7-bien, gad7-sueño, bien-sueño
    varnames  = c("gad7", "bienestar", "horas_sueno"),
    empirical = FALSE                    # FALSE: parámetros poblacionales (por defecto)
)

empirical = FALSE (por defecto) trata mu, sd y r como parámetros poblacionales: cada muestra tendrá valores ligeramente distintos por azar, igual que en datos reales. Con empirical = TRUE la muestra coincide exactamente con los valores especificados, lo que puede ser útil para ejemplos demostrativos pero no refleja la variabilidad real de un muestreo.

Simular la base de datos GAD-7

set.seed(42)

gad7_sim <- rnorm_multi(
    n        = 5000,
    mu       = c(8, 65, 6.5),
    sd       = c(5, 15, 1.5),
    r        = c(-0.45, -0.30, 0.20),
    varnames = c("gad7", "bienestar", "horas_sueno")) |>
    mutate(
        gad7 = round(pmax(0, pmin(21, gad7))),
        bienestar = round(pmax(0, pmin(100, bienestar))),
        horas_sueno = round(pmax(3, pmin(12, horas_sueno)), 1),
        carrera = sample(
            c("Psicología", "Medicina", "Enfermería", "Nutrición"),
            n(),
            replace = TRUE),
        semestre = sample(1:10, n(), replace = TRUE),
        sexo = sample(
            c("Mujer", "Hombre", "No binario"),
            n(),
            replace = TRUE, prob = c(0.60, 0.35, 0.05)
        )
    )

Verificar la estructura y las correlaciones

glimpse(gad7_sim)
Rows: 5,000
Columns: 7
$ gad7           <dbl> 12, 11, 10, 9, 8, 7, 16, 9, 11, 7, 1, 15, 9, 2, 8, 7, 4…
$ bienestar      <dbl> 44, 74, 60, 55, 59, 67, 43, 67, 34, 66, 44, 31, 87, 68,…
$ horas_sueno    <dbl> 7.7, 6.0, 5.9, 5.8, 7.5, 4.3, 6.8, 4.6, 7.3, 6.4, 9.4, …
$ carrera        <chr> "Psicología", "Medicina", "Enfermería", "Psicología", "…
$ semestre       <int> 10, 3, 9, 9, 5, 6, 8, 9, 5, 10, 7, 8, 2, 7, 4, 4, 2, 5,…
$ sexo           <chr> "Mujer", "Mujer", "Hombre", "Mujer", "Hombre", "Hombre"…
$ nivel_ansiedad <chr> "Moderada", "Moderada", "Moderada", "Leve", "Leve", "Le…
gad7_sim |>
    select(gad7, bienestar, horas_sueno) |>
    cor() |>
    round(2)
             gad7 bienestar horas_sueno
gad7         1.00     -0.44       -0.29
bienestar   -0.44      1.00        0.21
horas_sueno -0.29      0.21        1.00

Parte 5 + 1 + 2

Aplicar las funciones a los datos simulados

Clasificar y calcular sobre 5 000 observaciones

Aplicamos clasificar_ansiedad() con mutate() y prop_severa() directamente:

gad7_sim |>
    summarise(prop_severa = prop_severa(gad7))
  prop_severa
1       0.097

También podemos calcular la proporción por subgrupo con el pipe y dplyr:

gad7_sim |>
    group_by(carrera) |>
    summarise(prop_severa = prop_severa(gad7))
# A tibble: 4 × 2
  carrera    prop_severa
  <chr>            <dbl>
1 Enfermería      0.0911
2 Medicina        0.111 
3 Nutrición       0.0935
4 Psicología      0.0934

Visualizar la distribución de niveles

gad7_sim |>
    mutate(nivel_ansiedad = factor(
        nivel_ansiedad,
        levels = c("Mínima", "Leve", "Moderada", "Severa")
    )) |>
    ggplot(aes(x = nivel_ansiedad, fill = nivel_ansiedad)) +
    geom_bar() +
    scale_fill_manual(
        values = c(
            "Mínima" = "#7db87d", "Leve" = "#f5c842",
            "Moderada" = "#f0943a", "Severa" = "#c0392b"
        )
    ) +
    labs(x = "Nivel de ansiedad", y = "Frecuencia", fill = NULL) +
    theme_minimal() +
    theme(legend.position = "none")

¿Qué tendrías que hacer para visualizar a distribución por carrera?

Actividad · 10 min

Explorar los datos simulados

  1. Calcula la proporción con ansiedad severa por sexo
  2. Crea una figura que compare la distribución de gad7 entre carreras
  3. Añade una variable calculada sueno_bajo que valga TRUE cuando horas_sueno < 6

Quiz rápido

Tres preguntas antes de cerrar.

Quiz · 1 de 3

Tienes la siguiente función. ¿Qué produce resumir(c(1.5, 2.7, 3.3))?

resumir <- function(x, dec = 1){
      round(mean(x), dec)
}
    1. Un error: falta el argumento dec
    1. 2.5 (media redondeada a 1 decimal, valor por defecto)
    1. 2.50 (media redondeada a 2 decimales)
    1. 3 (media redondeada a 0 decimales)

b) Como dec tiene valor por defecto 1, llamar resumir(c(1.5, 2.7, 3.3)) es equivalente a resumir(c(1.5, 2.7, 3.3), dec = 1). La media es 2.5, que redondeada a 1 decimal da 2.5.

Quiz · 2 de 3

¿Cuál es la diferencia principal entre ifelse() y case_when()?

    1. ifelse() solo funciona con números; case_when() con texto
    1. ifelse() maneja dos opciones: verdadero y falso; case_when() permite múltiples condiciones en cadena
    1. case_when() es más lento y solo se usa dentro de mutate()
    1. No hay diferencia práctica, son intercambiables

b) ifelse() evalúa una condición con dos salidas posibles. case_when() encadena múltiples condiciones con sus respectivos resultados, lo que lo hace mucho más legible cuando hay más de dos categorías.

Quiz · 3 de 3

¿Para qué sirve set.seed() en una simulación?

    1. Para acelerar la generación de números aleatorios
    1. Para fijar el número de observaciones que se van a generar
    1. Para que la simulación produzca los mismos valores cada vez que se ejecuta
    1. Para que los números generados sigan siempre una distribución normal

c) set.seed() fija el estado interno del generador de números aleatorios. Con la misma semilla, el código produce exactamente los mismos valores en cualquier computador, lo que hace que la simulación sea reproducible.

A practicar

Antes de la próxima sesión, escribe al menos una función propia y aplícala a datos simulados o a los datos de encuesta_bruta.xlsx de la Sesión 3.

Ideas:

  • Una función que calcule varias estadísticas descriptivas de una columna
  • Una función que tome un vector de puntajes, los clasifique con case_when() y grafique la distribución de categorías
  • Simula una base de datos que sea relevante o te interese, y luego aplica tu función a esa base de datos o haz gráficos exploratorios

Si tienes datos propios de tu trabajo o investigación, este es el momento de empezar a probar tu análisis con datos simulados antes de tocar los datos reales.

Hasta la próxima sesión

¡Gracias!

jdleongomez.github.io/curso-r