mutate(): crear y transformar columnas

r
tidyverse
El verbo para añadir o modificar columnas en un pipeline. case_when() como ifelse adulto, across() para varias columnas a la vez y trampas de orden de evaluación.

Por qué mutate() reemplaza a df$nueva <- ...

mutate() es el verbo de dplyr para añadir o modificar columnas. En R base, esto se hace con asignación:

ventas$ingresos_eur <- ventas$ingresos_usd * 0.92

Funciona, pero:

  1. No compone con el pipe (te sale del flujo).
  2. Para varias columnas necesitas escribir el nombre del data frame cada vez.
  3. No queda en el código de qué pipeline forma parte la transformación.

mutate() resuelve los tres:

ventas |>
  mutate(ingresos_eur = ingresos_usd * 0.92)

Forma básica: crear una columna

ventas |>
  mutate(
    ingresos_eur = ingresos_usd * 0.92,
    año          = year(fecha),
    es_premium   = precio > 1000
  )

Las columnas se crean secuencialmente: puedes usar una recién creada en la siguiente línea del mismo mutate().

ventas |>
  mutate(
    ingresos_eur = ingresos_usd * 0.92,
    margen_eur   = ingresos_eur - coste_eur     # usa la columna recién creada
  )

Esto es distinto a, por ejemplo, SQL puro, donde el orden no garantiza dependencia. En mutate() sí lo hace.

Múltiples columnas en un mismo mutate()

Lo idiomático es agrupar transformaciones relacionadas en un único mutate():

ventas |>
  mutate(
    año     = year(fecha),
    mes     = month(fecha),
    dia_sem = wday(fecha, label = TRUE)
  )

Frente a tres mutates encadenados. Es más legible y la lectura agrupa lo que pertenece al mismo paso conceptual.

mutate() vs transmute()

# mutate(): conserva todas las columnas y añade
ventas |> mutate(ingresos_eur = ingresos_usd * 0.92)

# transmute(): solo deja las columnas mencionadas
ventas |> transmute(
  region,
  ingresos_eur = ingresos_usd * 0.92
)

transmute() es mutate() + select() en una sola operación. Útil cuando vas a quedarte solo con un subconjunto reducido.

Desde dplyr 1.0, transmute() está soft-deprecated en favor de mutate(.keep = "none"), pero la sintaxis explícita transmute() sigue siendo más legible y se entiende sin pensar.

case_when(): el ifelse adulto

Para recodificaciones con múltiples categorías, evita anidar ifelse():

# Lo que NO se debe escribir:
ventas |> mutate(
  segmento = ifelse(ingresos > 10000, "alto",
                    ifelse(ingresos > 1000, "medio",
                           ifelse(ingresos > 100, "bajo", "marginal")))
)

# Lo correcto: case_when()
ventas |> mutate(
  segmento = case_when(
    ingresos > 10000 ~ "alto",
    ingresos > 1000  ~ "medio",
    ingresos > 100   ~ "bajo",
    TRUE             ~ "marginal"   # default
  )
)

case_when() evalúa de arriba a abajo y devuelve el primer match. La línea TRUE ~ ... es el default.

Una versión moderna (dplyr 1.1+) usa .default:

ventas |> mutate(
  segmento = case_when(
    ingresos > 10000 ~ "alto",
    ingresos > 1000  ~ "medio",
    ingresos > 100   ~ "bajo",
    .default = "marginal"
  )
)

Más legible. Adóptala si tu versión de dplyr lo permite.

.before y .after: posición de las nuevas columnas

Por defecto, las nuevas columnas van al final. Si quieres ponerlas en una posición concreta:

ventas |>
  mutate(
    ratio_margen = margen / ingresos,
    .after = ingresos
  )

.before = 1 las pone al principio. Útil cuando trabajas con tablas anchas y quieres que las columnas calculadas vayan al lado de las originales relacionadas.

across(): aplicar a varias columnas a la vez

Si quieres aplicar la misma transformación a múltiples columnas:

ventas |>
  mutate(across(starts_with("ingresos_"), \(x) x * 0.92))

ventas |>
  mutate(across(where(is.numeric), ~ round(.x, 2)))

across() reemplaza al antiguo mutate_at(), mutate_if(), etc. Es el patrón unificado. Recibe dos cosas:

  • Un selector de columnas (starts_with(...), where(...), nombres concretos).
  • Una función a aplicar (con la sintaxis lambda moderna \(x) ... o la forma vieja ~ .x ...).

across() es probablemente el helper de dplyr que más eleva tu código cuando lo dominas: convierte 10 líneas repetitivas en una.

Trampas habituales

  • mutate() no es destructivo. Si quieres MODIFICAR una columna existente, asigna con el mismo nombre: mutate(precio = precio * 1.05). La columna se sobrescribe.
  • case_when() requiere que todos los lados derechos sean del mismo tipo. Mezclar "alto" con NA puro falla porque NA por defecto es lógico. Usa NA_character_ cuando trabajas con strings.
  • No uses mutate() para crear columnas que dependen de OTRAS filas. Para eso necesitas mutate() después de group_by() (siguiente tutorial) o funciones de ventana como lag(), lead(), cumsum().
  • El orden importa cuando una columna nueva depende de otra existente que también modificas. Si en el mismo mutate() haces B = A * 2 y luego A = A + 1, B usará el A original, no el actualizado.

En la siguiente entrega

Con filter(), select() y mutate(), ya puedes manipular un data frame fila a fila y columna a columna. Pero lo más útil del análisis es agregar: contar por categoría, calcular medias por grupo, encontrar el máximo por región. Eso es group_by() + summarise(), el patrón split-apply-combine. Es lo siguiente.