Funciones

¿Cómo creo una función?

Las funciones en R son las herramientas que nos permiten realizar una gran diversidad de análisis. Para ello, R provee de una numerosa y potente biblioteca de ellas, las cuales vienen instalada por defecto. Además, como hemos desarrollado al principio del curso, existe una abanico mega-diverso de paquetes de R desarrollados por los propios usuarios, especialmente desarrollados para la realización de tareas específicas.

No obstante, es usual que en un flujo de trabajo de intermedio a avanzado necesitemos desarrollar nuestras propias funciones. La razón para ello radica en la ejecución de una tarea muy específica como parte de nuestros análisis, pero que al mismo tiempo debe realizarse múltiples veces para que amerite su desarrollo en forma de función.

Las funciones en R se asignan a objetos, cuyo nombre será el nombre de la función, con la expresión function(). Dentro de los paréntesis debemos indicar los argumentos de la función. Estos serán los objetos con los que la función trabajará de alguna manera, y devolverá alguna salida determinada. La sintaxis general es la siguiente:

mi_funcion <- function(argumento1, argumento2, argumento3){
  # Inserte aqui el codigo que trabajara con los objetos definidos en los argumentos,
  # junto con la salida que se propone para la nueva funcion
}

Por ejemplo:

blabla <- function(nombre = "- inserte aquí su nombre -"){
  print(paste("Mi nombre es", nombre, "y me encanta el curso de Fundamentos básicos del lenguaje R."))
}

Al correr el bloque de código anterior, simplemente estamos creando la función, y no corriendo el código programado. Esta se carga como un objeto propio en el entorno, quedando disponible para su uso futuro.

En este ejemplo, el argumento nombre se encuentra asignado a una línea de texto. De esta manera, la función entiende que el valor indicado es el valor por defecto, para los casos en los que el usuario no define un valor particular para dicho argumento. La definición de valores por defecto no es obligatoria, pero puede ser útil en muchos casos. Si corremos dicha función suelta, sin definir nada en específico, podemos observar el resultado:

blabla()
[1] "Mi nombre es - inserte aquí su nombre - y me encanta el curso de Fundamentos básicos del lenguaje R."

En cambio, podemos asignarle un valor al argumento nombre:

blabla(nombre = "Lionel Scaloni")
[1] "Mi nombre es Lionel Scaloni y me encanta el curso de Fundamentos básicos del lenguaje R."

Unas palabritas sobre entornos

Una cuestión a considerar en el contexto de creación de funciones es el uso que las mismas hacen de los entornos. Podemos definir a un entorno como el espacio en donde se guardan nuestros objetos. Cuando creamos un objeto, sea una tabla, un vector, una función o cualquier otro, estos se guardan en el entorno global, y de hecho aparecen visibles en el panel correspondiente de la interfaz de RStudio. Cuando creamos una función, se crea un sub-entorno contenido dentro de la función. ¿Por qué es esto relevante? Porque los entornos son los lugares en donde se guardan los objetos de los que haremos uso para nuestras funciones, y una función buscará primero en el entorno de la función, luego en el entorno que se encuentra un nivel por encima de este.

Veámoslo con un ejemplo. La siguiente función toma un número y le suma 5:

fff <- function(x){
  y <- x + 5
  y
}

Por ejemplo:

fff(x = 3)
[1] 8

Esta función guarda el resultado en un objeto llamado y. Sin embargo, podemos ver que este objeto no se guarda en el entorno global, lo cual se evidencia cuando intentamos leer dicho objeto:

y
Error in eval(expr, envir, enclos): object 'y' not found

La razón es que y se creó en el entorno de la función fff(), se guarda allí dentro, se utiliza para la ejecución de la función, y finalmente se descarta.

Veamos ahora la siguiente variante de la función:

fff <- function(){
  y <- x + 5
  y
}

La diferencia es que esta función no tiene argumentos. Veamos lo que sucede al correrla:

fff()
Error in fff(): object 'x' not found

El error se debe a que x no está definido. Pero miremos lo que pasa si definimos a x por fuera de la función:

x <- 9
fff()
[1] 14

¿Qué pasó? La función buscó al objeto x dentro del entorno de la función, no lo encontró, pero luego buscó en el entorno un nivel por encima, que es el entorno global. Encontró un objeto con dicho nombre, y lo utilizó para su ejecución.

Para testear: ¿Qué sucedería si x está definido por fuera de la función pero también dentro de la misma?

Uso de return()

Desarrollemos una función un poco más compleja. Por ejemplo, imaginemos una función que calcula el valor promedio de un conjunto de números (tarea que, ya sabemos, ejecuta mean()). Una forma de hacerlo es la siguiente:

valor_promedio <- function(x){
  # Sumo todos los valores del vector x con un ciclo for
  sum <- 0
  for (i in 1:length(x)){
    sum <- sum + x[i]
  }
  
  # Divido la suma por la cantidad de elementos
  promedio <- sum/length(x)
  
  return(promedio)
}

Notar que el contenido de la función es casi idéntico a como programaríamos por fuera del contexto de una función. La única diferencia es que la función permite realizar la tarea propuesta para cualquier caso, y no solo para uno en particular. Aquí, podemos definir cualquier vector numérico que quisiéramos, y calcular el promedio a partir del vector indicado.

Otra diferencia que vemos aquí respecto de la función del ejemplo anterior (blabla()) es el uso de la función return(). Esta expresión indica el valor a devolver por la función, y su llamado termina la ejecución de la función en esa línea. Es decir, toda línea de código por debajo de un return() no se ejecutará. ¿De qué serviría esto? Por ejemplo, la función podría ejecutar un bloque de código si se cumple una condición, y otro bloque si no se cumple, justificando la presencia de más de un return().

La siguiente línea calcula el valor promedio para el vector 1:30:

valor_promedio(1:30)
[1] 15.5

Notar que no es necesario aclarar explícitamente el nombre del argumento x, basta con que sea el primero.

Una versión un poquito más compleja que la anterior posee un segundo argumento que le otorga un peso a cada elemento de la muestra. Por lo tanto, estaremos calculando un promedio ponderado. Se espera un vector numérico de longitud igual a x, indicando el peso otorgado a cada elemento. Por defecto, definimos un vector numérico de unos, otorgando así el mismo peso a cada elemento:

valor_promedio <- function(x, pesos = rep(1, length(x))){
  # Sumo todos los valores del vector x
  sum <- 0
  for (i in 1:length(x)){
    sum <- sum + x[i] * pesos[i]
  }
  
  promedio <- sum/length(x)
  return(promedio)
}

Por ejemplo, para el mismo conjunto de números anterior:

pesos_aleatorios <- sample(c(0.5, 0.75, 1), length(1:30), replace = TRUE)
valor_promedio(x = 1:30, pesos = pesos_aleatorios)
[1] 11.88333

Uso de stop() y warning()

Además de return(), las funciones stop() y warning() serán de utilidad cuando creemos nuestras propias funciones. Por ejemplo, imaginemos que el usuario indica para el argumento x un vector de tipo character(). Obviamente, el promedio no puede calcularse, y obtenemos un error. Este error, sin embargo, no es muy informativo:

valor_promedio(x = LETTERS[1:10])
Error in x[i] * pesos[i]: non-numeric argument to binary operator

Podemos agregar una condición que evalúe si el vector es numérico, caso contrario utilizamos stop().

valor_promedio <- function(x, pesos = rep(1, length(x))){
  # Evalua si el vector en x son numeros
  if(!is.numeric(x)){
    stop("El vector indicado en 'x' debe ser numérico.")
  }
  
  # Sumo todos los valores del vector x
  sum <- 0
  for (i in 1:length(x)){
    sum <- sum + x[i] * pesos[i]
  }
  
  promedio <- sum/length(x)
  return(promedio)
}

Con el uso de stop() la ejecución de la función se termina, y se imprime un mensaje informativo en la consola:

valor_promedio(x = LETTERS[1:10])
Error in valor_promedio(x = LETTERS[1:10]): El vector indicado en 'x' debe ser numérico.

La función warning() imprime una advertencia, pero continúa con la ejecución. Por ejemplo, imaginemos que el usuario indica un vector de pesos de diferente longitud a la longitud del vector indicado en x. Una opción para contemplar este escenario sería la siguiente:

valor_promedio <- function(x, pesos = rep(1, length(x))){
  # Evalua si el vector en x son numeros
  if(!is.numeric(x)){
    stop("El vector indicado en 'x' debe ser numérico.")
  }
  
  # Evalua si la longitud del vector en 'pesos' es igual a la de 'x'
  if(length(x) != length(pesos)){
    warning("El argumento 'pesos' debe ser un vector numérico de la misma longitud que el vector en 'x'. Se tomó el valor por defecto.")
    pesos <- rep(1, length(x))
  }
  
  # Sumo todos los valores del vector x
  sum <- 0
  for (i in 1:length(x)){
    sum <- sum + x[i] * pesos[i]
  }
  
  promedio <- sum/length(x)
  return(promedio)
}

Notar que se evalúa la condición planteada para el argumento pesos. Si no se cumple que la longitud del vector indicado es igual a la longitud del vector en x, la función imprime una advertencia. Además, para que la función siga ejecutándose correctamente, definimos pesos <- rep(1, length(x)), es decir, el valor por defecto. Este último paso es fundamental, de lo contrario la salida de la función sería impredecible.

valor_promedio(x = 1:30, pesos = c(0.5, 1, 0.75))
Warning in valor_promedio(x = 1:30, pesos = c(0.5, 1, 0.75)): El argumento
'pesos' debe ser un vector numérico de la misma longitud que el vector en 'x'.
Se tomó el valor por defecto.
[1] 15.5

Vemos que el promedio es calculado correctamente (con pesos = rep(1, length(x))), pero además se imprime una advertencia.

El uso de las funciones stop() y warning() en el contexto de funciones no es fundamental, pero adquieren relevancia para funciones que usarán otras personas. Uno nunca sabe con absoluta seguridad el tipo de entrada que un usuario externo usaría en una de nuestras funciones, y uno debe prevenir distintos escenarios. Sin embargo, si a las funciones las usaremos sólo nosotros, contemplar estos escenarios no será vital, por lo que no sería absolutamente necesario el uso de stop() o warning().

Funciones del tipo apply

Las funciones de la familia apply se utilizan para aplicar una función determinada a una lista o a un vector. Es útil para realizar un mismo cálculo a un conjunto de elementos, evitando utilizar, por ejemplo, un ciclo for.

Por ejemplo, imaginemos una lista de 3 elementos, cada elemento un vector numérico:

lista_numeros <- list(sample(1:1000, size = 50),
                      sample(1:1000, size = 50),
                      sample(1:1000, size = 50))
lista_numeros
[[1]]
 [1] 531 637 483   8 454 106 368 664 976 236 298  10 797 952 356  34 796 882 822
[20] 500 838 927 890 263 206 965 816 141 258 681  63 982 906 848 851 511 657 325
[39] 343   6 351 835 753 806 337 311 490  65 373 902

[[2]]
 [1] 997 674 968 678 609 855 464 142 173 389  57 218 527 989  64 784 469 436 875
[20] 538 472 550 216 599 830 786 866 774 841 944 712 128 839 526 258 850 340 811
[39]  76 579 711 205 632 951 713 517 847 627 600 135

[[3]]
 [1] 280 971 793 491 923 360 102 396 516  46 281 161 803 407 350 534 427 951 570
[20] 612 806 357 263 689 575 363 183 410 663 572 525 662 685 317  83 944 442 621
[39] 147  94 856 790 333 958 208  36 585 813 730 433

La función sapply() aplica una función dada a un objeto de tipo list(), y devuelve un vector. En este ejemplo, utilizaremos la función recientemente creada (valor_promedio()), para calcular el promedio de cada conjunto de números de la lista (indicados en el primer argumento):

promedios <- sapply(lista_numeros, FUN = valor_promedio)
promedios
[1] 532.20 576.82 502.34

Notar que para el argumento FUN indicamos el nombre de la función sin los paréntesis, la cual debe existir en el entorno (cargada por el usuario o por defecto en R).

Si quisiéramos indicar otros argumentos para la función valor_promedio(), estos pueden definirse luego del argumento FUN:

pesos_aleatorios <- sample(c(0.5, 0.75, 1), size = 50, replace = TRUE)
promedios <- sapply(lista_numeros, FUN = valor_promedio, pesos = pesos_aleatorios)
promedios
[1] 378.900 426.005 360.005

Para pensar: tal cual como está programada, la función valor_promedio() no es útil para ser utilizada con la función sapply(), especialmente en cuanto al argumento pesos. ¿Por qué?

Existen otras variantes a la función sapply(), como por ejemplo lapply(), que funciona de manera idéntica pero devuelve una lista en vez de un vector.

Ejercicio final

  1. Cree una función que calcule el área de un triángulo a partir de su base y altura. Contemple el escenario en el que el usuario indique valores no numéricos para los argumentos.
  2. Cree una función que calcule el error estándar de un conjunto de números. El error estándar se calcula realizando el cociente entre el desvío estándar de la muestra y la raíz cuadrada de la cantidad de números evaluados.
  3. A partir de la función que calcula el error estándar, utilice la función sapply() para calcular el error estándar del conjunto de números seq(1, 50, 20), c(501, 920, 759, 233) y -14:28.

Volver arriba