Funciones en R Base: Con paciencia y coraje

Una introducción práctica al diseño y uso de funciones sin depender de paquetes

Emanuel Ciardullo

R en Buenos Aires

Introducción

  • Las funciones son bloques de código reutilizables.
  • Permiten escribir programas más ordenados, legibles y mantenibles.
  • Hoy veremos cómo se definen, usan y evalúan funciones en R base.

¿Por qué usamos funciones?

Ventajas de usar funciones


  • Evitan repetir código
  • Dividen el problema en partes
  • Facilitan la depuración y pruebas
  • Hacen que el código sea reutilizable

¿Qué es una función?


Una función tiene un nombre, argumentos y un cuerpo.

nombre <- function(argumentos) {
  operaciones
}
  • nombre: como ejecutamos la función

  • argumentos: inputs que necesita la funcion

  • operaciones: es el cuerpo de la funcion. Lo que se va a ejecutar.

Ejemplo: imputar valores faltantes

¿Qué problema queremos resolver?


  • Tenemos una variable numérica con algunos valores NA.
  • Queremos reemplazar los NA por el promedio de los valores no faltantes.
  • Lo haremos primero con código suelto y luego con una función.

Código sin función



x <- c(1.5, 2.1, NA, 4.0)

x[is.na(x)] <- mean(x, na.rm = TRUE)

Función



imputar_media <- function(vec) {
 
   vec[is.na(vec)] <- mean(vec, na.rm = TRUE)
  
   return(vec)
}

x <- imputar_media(c(1.5, 2.1, NA, 4.0))


Más claro y reutilizable.

Gestión de errores

¿Por qué es importante validar?


  • Prevenir errores inesperados.

  • Mejorar la robustez del código.

  • Comunicar al usuario qué está mal.

Ejemplo 1:



imputar_media <- function(vec) {
  
  if (!is.numeric(vec)) {
    stop("La variable debe ser numérica.")
  }
  
  vec[is.na(vec)] <- mean(vec, na.rm = TRUE)
  return(vec)
}

Ejemplo 2: Advertencias


imputar_media <- function(vec) {
  
  if (!is.numeric(vec)) {
    stop("La variable debe ser numérica.")
  }
  
  if (all(is.na(vec))) {
    warning("Todos los valores son NA. Se devuelve el vector original.")
    return(vec)
  }
  
  vec[is.na(vec)] <- mean(vec, na.rm = TRUE)
  return(vec)
}

missing


missing() se puede utilizar para comprobar si se ha especificado un valor como argumento de una función.

imputar_media <- function(vec) {
  
  if(missing(vec)) stop("Argumento faltante")
  
  vec[is.na(vec)] <- mean(vec, na.rm = TRUE)
  return(vec)
}

#imputar_media()

Valores por defecto

¿Qué son los valores por defecto?


  • Argumentos que toman un valor predeterminado si no se especifican.

  • Permiten que la función sea más flexible y fácil de usar.

Ejemplo: dos formas de imputación

imputar_valor <- function(vec, metodo = "media") {
  
  if (metodo == "media") {
    
    vec[is.na(vec)] <- mean(vec, na.rm = TRUE)
    
  } else if (metodo == "mediana") {
    
    vec[is.na(vec)] <-  median(vec, na.rm = TRUE)
    
  } else {
    stop("Método inválido.")
  }
  
  return(vec)
}
imputar_valor(c(1, NA, 3, 8))
#> [1] 1 4 3 8
imputar_valor(c(1, NA, 3, 8), metodo = "mediana")
#> [1] 1 3 3 8

Uso de return()

• Devuelve un valor explícito

• Mejora claridad del código

• No siempre es obligatorio (pero si recomendable!!)


suma2 <- function(a, b) {
  resultado <- a + b
  return(resultado)
}

suma2 <- function(a, b) {
  a + b
}

Interacción con el entorno


a = 4 

suma2 <- function(b = 2) {
  a = a - 2 
  resultado <- a + b
  return(resultado)
}

#suma2()

¿Qué valor devuelve la función?

¿Qué valor queda guardado en el objeto a?

missing

missing() se puede utilizar para comprobar si se ha especificado un valor como argumento de una función.


suma2 <- function(a, b){
  
  if(missing(b)) b = a
  
  resultado <- a + b 
  return(resultado)
}

suma2(a = 2)
#> [1] 4

dot-dot-dot


El argumento … es un argumento especial útil para pasar un número desconocido de argumentos a otra función.


suma2 <- function(a, b, ...){
  
  resultado <- sum(c(a, b), ... ) 
  
  return(resultado)
}

Testeo de funciones


• Confirma que la función hace lo esperado

• Detecta errores antes de usarlas en producción


stopifnot(
imputar_valor(c(1, NA, 3, 8))[2] == mean(c(1,3,8)) &
imputar_valor(c(1, NA, 3, 8), metodo = "mediana")[2] == median(c(1,3,8))
)

Debuguear funciones



debugonce()

browser()

Evaluar eficiencia

¿Por qué importa la eficiencia?

Algunas funciones se ejecutan miles de veces o sobre de bases de datos muy grandes. La velocidad de ejecución puede afectar el rendimiento del análisis.

imputar_loop <- function(vec) {
  for (i in seq_along(vec)) {
    if (is.na(vec[i])) {
      vec[i] <- mean(vec, na.rm = TRUE)
    }
  }
  return(vec)
}
x <- rep(c(1:100, NA), 10)

system.time(imputar_loop(x))         
#>    user  system elapsed 
#>    0.00    0.00    0.01
system.time(imputar_valor(x)) 
#>    user  system elapsed 
#>       0       0       0

Buenas practicas a la hora de escribir funciones

  • Documentar bien la función.

  • Evitar las variables globales. Todos los inputs deberian ser parametros de la función.

  • Usar return().

  • Ser consistente con el tipo del output.

  • Las funciones deben hacer una sola cosa y ser lo mas simples posible. Es mejor hacer multiples funciones que una gran función dificil de entender.

  • No cargar paquetes en las funciones, usar package::functions.

Preguntas

Integrando la velocidad de C++ en R

Gracias