7. dplyr II (mutate/recodes)

1 Gramática dplyr II: mutate, if_else, case_when, recode, across, coalesce

Objetivo: crear y transformar variables de forma segura; recodificar categorías; manejar NA; aplicar transformaciones a muchas columnas a la vez con across().

Tip

Trabajaremos sobre BaseSal de la sección anterior (rank, sex, yrs.service, salary).
Si tu columna de antigüedad tiene otro nombre, el código intenta detectarla.

Code
#| label: s7-setup
#| echo: true
library(tidyverse)
Warning: package 'tidyverse' was built under R version 4.2.2
Warning: package 'ggplot2' was built under R version 4.2.3
Warning: package 'tibble' was built under R version 4.2.3
Warning: package 'tidyr' was built under R version 4.2.2
Warning: package 'purrr' was built under R version 4.2.2
Warning: package 'stringr' was built under R version 4.2.3
Warning: package 'forcats' was built under R version 4.2.2
Warning: package 'lubridate' was built under R version 4.2.2
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.2     ✔ tidyr     1.3.0
✔ purrr     1.0.1     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
Code
# Si BaseSal no existe, reintenta crearlo desde Salaries:
if (!exists("BaseSal")) {
  if (!exists("Salaries")) {
    library(readxl)
    Salaries <- readxl::read_excel(ruta_salaries)  
  }
  BaseSal <- dplyr::select(Salaries, dplyr::any_of(c("rank","sex","yrs.service","salary")))
}

# Detectar posible nombre de antigüedad
nombre_ant <- intersect(c("yrs.service","yrs_service","years_service"), names(BaseSal))
nombre_ant <- if (length(nombre_ant) == 1) nombre_ant else NA_character_
nombre_ant
[1] "yrs.service"

1.1 Crear variables con mutate() y transmute()

mutate() añade columnas manteniendo las existentes. transmute() devuelve solo las columnas creadas/seleccionadas.

Code
# Tasa de cambio hipotética para COP (ejemplo didáctico)
tasa_cop <- 4556.04

Base1 <- BaseSal %>%
  mutate(
    salary_cop = salary * tasa_cop,        # conversión
    log_salary = log(salary),              # transformación
    # ingreso_ajustado: si falta salary, usa 0 (solo para ilustración)
    ingreso_ajustado = if_else(is.na(salary), 0, salary)
  )

head(Base1)
Code
# Sólo devolver las columnas nuevas y alguna clave:
Base1_min <- BaseSal %>%
  transmute(rank, sex, salary_cop = salary * tasa_cop, log_salary = log(salary))
head(Base1_min)
Warning

No imputes “0” a ciegas. Acá se usa con fin didáctico. En problemas reales, documenta y justifica cualquier imputación.

1.2 Condiciones con if_else() y case_when()

if_else(test, yes, no) para dos ramas. case_when() para múltiples condiciones (categorías).

Code
# Categorías de antigüedad (si la columna existe)
Base2 <- Base1

if (!is.na(nombre_ant)) {
  Base2 <- Base2 %>%
    mutate(
      antiguedad_cat = case_when(
        .data[[nombre_ant]] < 10              ~ "<10",
        .data[[nombre_ant]] <= 20             ~ "10-20",
        is.na(.data[[nombre_ant]])            ~ NA_character_,
        TRUE                                  ~ ">20"
      ) %>% factor(levels = c("<10","10-20",">20"))
    )
}

# Condición binaria con if_else: indicador “alto salario”
Base2 <- Base2 %>%
  mutate(salary_alto = if_else(salary >= 120000, 1L, 0L))  # umbral ejemplo

head(Base2)

1.3 Recodificar categorías con recode() y forcats

Estandariza valores de texto y arregla niveles de factores.

Code
# Limpieza rápida de 'sex' a valores canónicos
Base3 <- Base2 %>%
  mutate(
    sex = sex %>% as.character() %>% stringr::str_trim() %>% stringr::str_to_title(),
    sex = dplyr::recode(sex,
      "M" = "Male", "F" = "Female",
      .default = sex
    ) %>% factor(levels = c("Female","Male"))
  )

# Estandarizar 'rank' (si llega desordenado o con variantes)
Base3 <- Base3 %>%
  mutate(
    rank = rank %>% as.character() %>% stringr::str_squish(),
    rank = dplyr::recode(rank,
      "Assistant Professor" = "AsstProf",
      "Associate Professor" = "AssocProf",
      "Asstprof" = "AsstProf",
      "Assocprof" = "AssocProf",
      "Professor" = "Prof",
      .default = rank
    ) %>% factor(levels = c("AsstProf","AssocProf","Prof"))
  )

count(Base3, rank, sex)

1.4 Manejo de faltantes: coalesce() y replace_na()

coalesce(a, b, c) toma el primer no-NA de izquierda a derecha. tidyr::replace_na() asigna un valor por defecto a los NA de columnas específicas.

Code
# Si existieran dos columnas de antigüedad (ej.: yrs.service y yrs_service),
# puedes fusionarlas en una sola sin perder datos no-NA:
if (all(c("yrs.service","yrs_service") %in% names(BaseSal))) {
  Base3 <- Base3 %>%
    mutate(yrs.service = coalesce(`yrs.service`, yrs_service))
}

# Reemplazar NA en 'salary' por la mediana (solo para práctica, no recomendado sin análisis)
med_sal <- median(Base3$salary, na.rm = TRUE)
Base3_demo <- Base3 %>%
  tidyr::replace_na(list(salary = med_sal))

sum(is.na(Base3$salary)); sum(is.na(Base3_demo$salary))
[1] 0
[1] 0
Important

Buenas prácticas: antes de reemplazar NA, pregúntate por qué faltan y si la imputación distorsiona el análisis (anota tu decisión).

1.5 Transformaciones masivas con across()

Aplica una función a muchas columnas a la vez.

Code
# Estandarizar (z-score) todas las columnas numéricas, devolviendo nuevas columnas *_z
Base4 <- Base3 %>%
  mutate(
    across(where(is.numeric), ~ as.numeric(scale(.x)), .names = "{.col}_z")
  )

# Redondear a 2 decimales algunas columnas numéricas existentes (sin crear nuevas)
Base5 <- Base3 %>%
  mutate(
    across(c(salary, salary_cop, log_salary), ~ round(.x, 2))
  )

# Crear indicadores TRUE/FALSE si una numérica está por encima de su Q3
q3_salary <- quantile(Base3$salary, 0.75, na.rm = TRUE)
Base6 <- Base3 %>%
  mutate(salary_gt_q3 = salary > q3_salary)

head(select(Base4, salary, salary_z))
Code
head(select(Base6, salary, salary_gt_q3))

1.6 Operaciones por fila: rowwise() (cuando depende de varias columnas)

Útil si necesitas combinar varias columnas en una sola operación por fila.

Code
# Ejemplo didáctico: "pago_neto" = salary - (10% si salary > 120k)
Base_row <- Base3 %>%
  rowwise() %>%
  mutate(
    pago_neto = if_else(salary > 120000, salary * 0.9, salary)
  ) %>%
  ungroup()

head(select(Base_row, salary, pago_neto))

1.7 Ejercicios

  1. Crea salary_miles = salary / 1000 y salary_log10 = log10(salary).

  2. Genera rango_sal con case_when según salary: <90k, 90–120k, >120k (usa factor con ese orden).

  3. Recodifica sex a F/M (respetando NA) y guarda en sex_bin.

  4. Con across(where(is.numeric)), crea columnas nuevas con el cuadrado de las numéricas (sufijo _2).

  5. (Opcional) Imputa salary con la mediana por rank (pista: group_by(rank) |> mutate(salary = if_else(is.na(salary), median(salary, na.rm=TRUE), salary))).

Code
# 1) Nuevas transformaciones
Base_ex1 <- Base3 %>%
  mutate(
    salary_miles = salary / 1000,
    salary_log10 = log10(salary)
  )
head(select(Base_ex1, salary, salary_miles, salary_log10))
Code
# 2) Rango salarial con case_when
Base_ex2 <- Base3 %>%
  mutate(
    rango_sal = case_when(
      salary <  90000                ~ "<90k",
      salary <= 120000               ~ "90–120k",
      salary >  120000               ~ ">120k",
      TRUE                           ~ NA_character_
    ) %>% factor(levels = c("<90k","90–120k",">120k"))
  )
count(Base_ex2, rango_sal)
Code
# 3) sex_bin (F/M)
Base_ex3 <- Base3 %>%
  mutate(
    sex_bin = dplyr::recode(as.character(sex),
      "Female" = "F", "Male" = "M",
      .default = NA_character_
    )
  )
count(Base_ex3, sex, sex_bin)
Code
# 4) across para cuadrados *_2
Base_ex4 <- Base3 %>%
  mutate(across(where(is.numeric), ~ .x^2, .names = "{.col}_2"))
Base_ex4 %>% select(ends_with("_2")) %>% head()
Code
# 5) (Opcional) Imputar salary con mediana por rank
Base_ex5 <- Base3 %>%
  group_by(rank) %>%
  mutate(
    salary = if_else(is.na(salary), median(salary, na.rm = TRUE), salary)
  ) %>%
  ungroup()
sum(is.na(Base3$salary)); sum(is.na(Base_ex5$salary))
[1] 0
[1] 0