Funciones en R

La mayor parte de las tareas en R se llevan a cabo a través de una función. Las funciones son bloques fundamentales de la programación en R. Una función es una sub-rutina escrita con la finalidad de hacer una tarea específica.

En programación usamos funciones para incorporar conjuntos de instrucciones que, por ser usadas frecuentemente y/o por su alta complejidad, es conveniente “encapsularlas” en un sub-programa y llamarlas cuando sea necesario.

R también permite al usuario editar funciones existentes y escribir sus propias funciones; estas tendrán las mismas propiedades de otras funciones. Escribir funciones propias permite un uso flexible, eficiente y racional de R.


Estructura de las funciones

Las funciones son un tipo especial de objetos de clase “function”:

class(sd)
## [1] "function"

 

Los operadores matemáticos y lógicos también son funciones:

"+"(1, 2)
## [1] 3

 

Una función de R tiene los siguientes elementos:


Las funciones en R son objetos creados por medio de la función function(). El código de una función en R (en general) sigue la siguiente estructura:

nombre.funcion <- function(argumentos) {
  cuerpo.de.la.funcion:
    * calculos utilizando los argumentos
    * devuelve un resultado (return())
  }

 

Podemos ver esta estructura en las funciones existentes en R. Para acceder una función en R, únicamente debemos llamarla en la consola (sin usar los paréntesis). Por ejemplo, podemos ver el código de la función sd() de la siguiente forma:

sd
## function (x, na.rm = FALSE) 
## sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
##     na.rm = na.rm))
## <bytecode: 0x37c4e68>
## <environment: namespace:stats>

 

En R estudio también podemos abrir el código de la función como un archivo de texto usando la tecla F2.

 

Podemos descomponer las funciones en los elementos básicos:

#cuerpo de la funcion
body(sd)
## sqrt(var(if (is.vector(x) || is.factor(x)) x else as.double(x), 
##     na.rm = na.rm))
#argumentos
formals(sd)
## $x
## 
## 
## $na.rm
## [1] FALSE
#ambiente
environment(sd)
## <environment: namespace:stats>

 

Algunas funciones básicas de R usan funciones primitivas: funciones escritas en C. Estas funciones no muestran el código usado para hacer los cálculos:

sum
## function (..., na.rm = FALSE)  .Primitive("sum")
body(sum)
## NULL
formals(sum)
## NULL
environment(sum)
## NULL

 

Estas funciones se encuentran solo en el paquete base de R, el cual contiene todas las funciones básicas.


Una de las ventajas de R es que podemos escribir nuestras propias funciones y simplificar cálculos o procedimientos.

A continuación vamos a crear una Función llamada “f1”, la cual sumará un par de números:

f1 <- function(x, y) {
  
  x + y
  
}

class(f1)
## [1] "function"
f1(35, 8)
## [1] 43

 

Esto es exactamente lo mismo:

f1 <- function(x, y) x + y
  
f1(35, 8)
## [1] 43

Si el cuerpo de la función lleva más de una linea de código debe ir dentro de corchetes


Nombre de la función

Los nombres de las funciones tienen pocas restricciones, y siguen las mismas “reglas” que los nombres de otros objetos en R. Estas son algunas recomendaciones para nombrar funciones:

  • No utilizar el nombre de objetos ya presentes o de nombres comunes de objetos (i.e. x o y)
  • No utilizar el nombre de funciones comúnmente usadas
  • No deben empezar con un número
  • Deben dar alguna noción de la tarea que desempeñan
  • No deben ser extremadamente largos

En caso de que existan varias funciones con el mismo nombre, estas pueden ser llamadas de forma separada usando el Namespace o nombre del paquete seguido del símbolo :: :

sd <- function(x) x^10

sd(1:5)
## [1]       1    1024   59049 1048576 9765625
stats::sd(1:5)
## [1] 1.581139

 

Especificar el Namespace antes de la función también puede ser usado para llamar funciones de paquetes que están instalados pero no han sido cargados en el ambiente actual (con library() o require()). Esto es especialmente útil cuando los nombres de las funciones de diferentes paquetes coinciden. También es común cuando se llaman funciones de otros paquetes dentro de un paquete.

Las funciones pueden ser “anónimas”:

(function(x) x^10)(1:5)
## [1]       1    1024   59049 1048576 9765625

 

Esto es más útil cuando las funciones son usadas dentro de bucles (loops) con las funciones de la familia Xapply:

l <- list(1:5, 1:4, 1:3)

lapply(l,function(x) x^10)
## [[1]]
## [1]       1    1024   59049 1048576 9765625
## 
## [[2]]
## [1]       1    1024   59049 1048576
## 
## [[3]]
## [1]     1  1024 59049

Argumentos

Los argumentos permiten al usuario ingresar los objetos (i.e. variables, matrices) sobre los cuales se llevarán a cabo el procedimiento de la función.

Los argumentos pueden tener valores por defecto, en cuyo caso no es obligatorio definirlos cuando la función es utilizada. En este ejemplo solo el segundo argumento tiene un valor por defecto. Por lo tanto el segundo argumento puede o no ser proporcionado por el usuario:

f1 <- function(x, y = 2) x + y

f1(1)
## [1] 3

 

Los argumentos por defecto se pueden modificar:

f1(3, 4)

 

Sin embargo el argumento que no tiene un valor por defecto debe ser proporcionado, sino la función produce un error:

f1()
## Error in f1(): argument "x" is missing, with no default

 

Si todos los argumentos tienen un valor por defecto las funciones se pueden llamar sin definir ningún argumento:

f1 <- function(x = -2, y = 2) x + y

f1()
## [1] 0

 
Este es el caso de funciones como dev.off() y Sys.time():

Sys.time()
## [1] "2017-04-20 09:20:28 CST"

 

Cuando se llama una función los argumentos pueden ser especificados por posición, por nombres completos o por nombres parciales:

f2 <- function(a1, b1, b2) {
  list(a1 = a1, b1 = b1, b2 = b2)
}

#por posicion
str(f2(1, 2, 3))
## List of 3
##  $ a1: num 1
##  $ b1: num 2
##  $ b2: num 3
#por posicion + nombres
str(f2(a = 1, 2, 3))
## List of 3
##  $ a1: num 1
##  $ b1: num 2
##  $ b2: num 3
str(f2(1, a= 2, 3))
## List of 3
##  $ a1: num 2
##  $ b1: num 1
##  $ b2: num 3
str(f2(1,  2, a= 3))
## List of 3
##  $ a1: num 3
##  $ b1: num 1
##  $ b2: num 2
#por posicion + nombres parciales
str(f2(1, a = 2, b1 =3))
## List of 3
##  $ a1: num 2
##  $ b1: num 3
##  $ b2: num 1

 

Sin embargo, si los nombres parciales son ambiguos se genera un error:

f2(b= 1,  2, a = 3)
## Error in f2(b = 1, 2, a = 3): argument 1 matches multiple formal arguments

 

Es más seguro (y por tanto una mejor práctica) escribir los nombres completos de los argumentos.  

Las funciones también pueden tomar argumentos lógicos. Estos son útiles (entre otras cosas) para modificar el comportamiento de la función en el momento de ser utilizada. Por ejemplo, la función mean() permite al usuario controlar si los NAs deben ser omitidos o no:

v1 <- c(1, 2, 3, NA)

# sin omitir los NAs
mean(v1, na.rm = FALSE)
## [1] NA
# omitiendo NAs
mean(v1, na.rm = TRUE)
## [1] 2

 

Los paquetes de R siguen un formato estándar de documentación en el cual cada función del paquete debe especificar claramente el tipo de objeto requerido por cada argumento. Podemos ver la documentación de una función poniendo un signo de pregunta seguido por el nombre de la función (sin paréntesis):

?mean

 

En R estudio también podemos acceder a la documentación poniendo el cursor sobre el nombre de la función y presionando F1.


Cuerpo de la función

El cuerpo de la función contiene la revisión de argumentos, manipulaciones de datos, los cálculos y la definición de resultados. El cuerpo se pueden utilizar las mismas herramientas que usamos en otros códigos de R. Sin embargo, los objetos creados dentro de una función no serán disponibles en el ambiente de trabajo (esto lo veremos más adelante).

Si tenemos una función que realiza varias tareas y por ende tiene muchos resultados para reportar, tenemos que incluir una expresión de retorno o “return statement”, para especificar que resultados serán devueltos. La expresión de retorno se genera usando la función return().

A continuación crearemos dos funciones similares, una en donde no tenemos la expresión de retorno y otra en la que si:

# Función sin "return statement"  
f1 <- function(x, y) {
  
  z1 <- 2*x + y
  z2 <- x + 2*y
  z3 <- 2*x + 2*y
  z4 <- x/y
  }

f1(5, 3)
f1.res <- f1(5, 3)
f1.res
## [1] 1.666667
# Función con "return statement"  
f2 <- function(x, y) {
  
  z1 <- 2*x + y
  z2 <- x + 2*y
  z3 <- 2*x + 2*y
  z4 <- x/y
  
  return(c(z1, z2, z3, z4))
  
}

f2(5, 3)
## [1] 13.000000 11.000000 16.000000  1.666667
f2.res <- f2(5, 3)
f2.res
## [1] 13.000000 11.000000 16.000000  1.666667

 

La función return() puede obviarse y se devolverá el último objeto creado:

# Función con "return statement"  
f3 <- function(x, y) {
  
  z1 <- 2*x + y
  z2 <- x + 2*y
  z3 <- 2*x + 2*y
  z4 <- x/y
  
  c(z1, z2, z3, z4)
  
}


f3(5, 3)
## [1] 13.000000 11.000000 16.000000  1.666667
f3.res <- f3(5, 3)
f3.res
## [1] 13.000000 11.000000 16.000000  1.666667

 

Siempre es más seguro usar return().

 

Cuando tenemos una función que ejecuta múltiples procesos podemos guardar los resultados en una lista. Esto es especialmente útil cuando los elementos que se devuelven son de diferentes clases (e.g. vectores y matrices) o número de elementos:

f4 <- function(x, y) {
  
  # vector con 1 elemento
  z1 <- x + y
  
  # vector con 2 elementos
  z2 <- c(x, y/3)
  
  # vector lógico
  z3 <- z2 < 10
  
  
  l <- list(z1, z2, z3)
  
  return(l)
}

f4(10, 5)
## [[1]]
## [1] 15
## 
## [[2]]
## [1] 10.000000  1.666667
## 
## [[3]]
## [1] FALSE  TRUE

 

Cuando se devuelven listas podemos acceder nuestros resultados de forma independiente usando los indices de la lista (con paréntesis cuadrados dobles):

f4(10, 5)[[1]]
## [1] 15
f4(10, 5)[[2]]
## [1] 10.000000  1.666667
f4(10, 5)[[3]]
## [1] FALSE  TRUE

 

También podemos almacenar el resultado en una objeto:

resul <- f4(10, 5)
resul[[1]]
## [1] 15
resul[[2]]
## [1] 10.000000  1.666667

 

Ventajas al usar funciones

Código más limpio

Las funciones permiten la manipulación de objetos sin crear un gran número de objetos en los pasos intermedios. Este concepto es más fácil de entender con un ejemplo. La función f5() crea el objeto cuadr y logcuadr internamente. Sin embargo estos objetos no son añadidos al ambiente de trabajo:

# primero removamos todos los objetos en el ambiente
rm(list = ls())

f5 <- function(x) {
    cuadr <- x * x
    logcuadr <- log(x)
        return(logcuadr)
}


f5(7)
## [1] 1.94591
exists("cuadr")
## [1] FALSE
exists("logcuadr")
## [1] FALSE

 

Si esta operación se hiciera sin usar funciones los objetos si estarían en el ambiente, lo cual puede ser inconveniente (e.g. podría sobrescribir otros objetos):

x <- 7
    cuadr <- x * x
    logcuadr <- log(x)
      

exists("cuadr")
## [1] TRUE
exists("logcuadr")
## [1] TRUE

 

Las funciones ni siquiera tienen que ser definidas en el código en uso. Una función se puede guardar como un archivo de código de R y cargarla en el ambiente usando la función source():

sink(file="mi_super_funcion.R")
cat("funcionX <-")
funcion3
sink()

 #remover todos los objetos
rm(list = ls())

 

Ahora podemos llamar la función (ojo que le cambiamos el nombre a funcionX):

#cargar funcion
source("mi_super_funcion.R")

# aplicarla
funcionX(cuad.matriz = cbind(c(1, 2), c(3, 4)), vector = c(2, 3))

 

## [[1]]
##      [,1] [,2]
## [1,]    1    2
## [2,]    3    4
## 
## [[2]]
## [1] 4 9

Fácil de aplicar a nuevos objetos

Los procedimientos de programación “no-encapsulados” en una función generalmente están definidos para trabajar sobre objetos específicos. Por ejemplo, el siguiente código genera información sobre la estructura de un juego específico de datos (utilizaremos el juego de datos iris que viene por defecto en R):

head(iris)
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
?iris

# número de filas
nrow(iris)
## [1] 150
# número de columnas
ncol(iris)
## [1] 5
# número de variables numericas
length(which(sapply(iris, is.numeric)))
## [1] 4
# nombre de variables
names(iris)
## [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width" 
## [5] "Species"

 

Si estuviéramos interesados en extraer esta información para un gran número de juegos de datos, tendríamos que cambiar el nombre del objeto en cada una de las funciones anteriores. En este caso construir una función nos permitiría aplicar el mismo procedimiento a cualquier otro juego de datos simplemente cambiando un único argumento:

info.bd <- function(bd) {

  l <- list(head = head(bd), 
          nrow = nrow(bd), 
          ncol = ncol(bd),
    n.num = length(which(sapply(bd, is.numeric))), 
    nombre.vars = names(bd))

return(l)
  }

info.bd(iris)
## $head
##   Sepal.Length Sepal.Width Petal.Length Petal.Width Species
## 1          5.1         3.5          1.4         0.2  setosa
## 2          4.9         3.0          1.4         0.2  setosa
## 3          4.7         3.2          1.3         0.2  setosa
## 4          4.6         3.1          1.5         0.2  setosa
## 5          5.0         3.6          1.4         0.2  setosa
## 6          5.4         3.9          1.7         0.4  setosa
## 
## $nrow
## [1] 150
## 
## $ncol
## [1] 5
## 
## $n.num
## [1] 4
## 
## $nombre.vars
## [1] "Sepal.Length" "Sepal.Width"  "Petal.Length" "Petal.Width" 
## [5] "Species"
info.bd(iris3)
## $head
## [1] 5.1 4.9 4.7 4.6 5.0 5.4
## 
## $nrow
## [1] 50
## 
## $ncol
## [1] 4
## 
## $n.num
## [1] 600
## 
## $nombre.vars
## NULL
info.bd(CO2)
## $head
##   Plant   Type  Treatment conc uptake
## 1   Qn1 Quebec nonchilled   95   16.0
## 2   Qn1 Quebec nonchilled  175   30.4
## 3   Qn1 Quebec nonchilled  250   34.8
## 4   Qn1 Quebec nonchilled  350   37.2
## 5   Qn1 Quebec nonchilled  500   35.3
## 6   Qn1 Quebec nonchilled  675   39.2
## 
## $nrow
## [1] 84
## 
## $ncol
## [1] 5
## 
## $n.num
## [1] 2
## 
## $nombre.vars
## [1] "Plant"     "Type"      "Treatment" "conc"      "uptake"

 

Fácil de compartir

Al ser unidades de código discretas que son aplicables de forma general, las funciones pueden ser fácilmente compartidas y utilizadas por otros usuarios. En este ejemplo cargamos la función “info.bd” que creamos anteriormente desde un sitio remoto:

#remover todo
rm(list = ls())
ls()
## character(0)
#cargamos la funcion remotamente
source("http://marceloarayasalas.weebly.com/uploads/2/5/5/2/25524573/info.bd.r")

# ahora esta en el ambiente
ls()
## [1] "info.bd"
info.bd(CO2)
## $head
##   Plant   Type  Treatment conc uptake
## 1   Qn1 Quebec nonchilled   95   16.0
## 2   Qn1 Quebec nonchilled  175   30.4
## 3   Qn1 Quebec nonchilled  250   34.8
## 4   Qn1 Quebec nonchilled  350   37.2
## 5   Qn1 Quebec nonchilled  500   35.3
## 6   Qn1 Quebec nonchilled  675   39.2
## 
## $nrow
## [1] 84
## 
## $ncol
## [1] 5
## 
## $n.num
## [1] 2
## 
## $nombre.vars
## [1] "Plant"     "Type"      "Treatment" "conc"      "uptake"

Recomendaciones adicionales


Ejemplo

Volvamos al ejemplo de tiburones. Ya vimos como podemos generar gráficos de la profundidad a la que nada cada individuo de acuerdo a la hora del día usando la función for-loop. También vimos como hacer gráficos que muestren la profundidad a la que nadan machos y hembras durante cada mes del año. Podríamos “encapsular” algunas de estos procedimientos en una función.

A continuación usaremos la base de datos de elasmobranquios (“ElasmosCR.csv”) asociados a la pesca de arrastre en el Pacífico de Costa Rica. En este estudio se colectaron muestras de músculo de varias especies para analizar la composición isotópica de 13δC y 15δN. La base de datos muestra la variación del isótopo de 15δN de cuatro especies comunes (Raja velezi, Mustelus henlei, Zapteryx xyster y Torpedo peruana) de acuerdo al sexo y talla.

Usando lo aprendido anteriormente vamos a escribir una función para hacer un gráfico de la talla y la variación isotópica por sexo:

# Importar base de datos "ElasmosCR.csv"
db <- read.csv("http://marceloarayasalas.weebly.com/uploads/2/5/5/2/25524573/elasmoscr.csv", header = T, sep = ",")

levels(db$spp) <-c("Mustelus henlei", "Raja velezi", "Zapteryx xyster", "Torpedo peruana")

head(db)
##               spp     TL    sex     dN15       LL       UL
## 1 Mustelus henlei 21.800 Female 14.42316 13.77718 15.06915
## 2 Mustelus henlei 22.396 Female 14.44896 13.82029 15.07762
## 3 Mustelus henlei 22.992 Female 14.47475 13.86333 15.08617
## 4 Mustelus henlei 23.588 Female 14.50055 13.90629 15.09480
## 5 Mustelus henlei 21.800   Male 14.50590 13.85909 15.15271
## 6 Mustelus henlei 24.184 Female 14.52634 13.94917 15.10352
# Subset by species
dbs <- split(db, db$spp)

# Crear Función
plot.spp <- function(data){
  
  # Separar machos y hembras
  machos <- data[data$sex=="Male", ]
  hembras <- data[data$sex=="Female", ]

  #Rango de los ejes "x" y "y"
  min.x <- floor(min(data$TL))
  max.x <- ceiling(max(data$TL))

  min.y <- floor(min(data$dN15))
  max.y <- ceiling(max(data$dN15))

  plot(0,0, type="n", xaxt='n', yaxt='n', ylim=c(min.y, max.y),
       xlim=c(min.x, max.x), xlab="", ylab="", frame.plot=F)

  axis(1, at=seq(min.x, max.x, by=10), cex.axis=1)
  axis(2, at=seq(min.y, max.y, by=0.5), cex.axis=1,las=1)

    
  # machos
  polygon(c(seq(min(machos$TL),max(machos$TL),l=length(machos$LL)),
            rev(seq(min(machos$TL),max(machos$TL),l=length(machos$UL)))),
          c(machos$LL,rev(machos$UL)),col=adjustcolor("green4", 0.3),border=F)  
  # linea de mejor ajuste
  lines(x=seq(min(machos$TL),max(machos$TL),l=length(machos$LL)),
        y=machos$dN15,col="green4",lty=2,lwd=2.5)
  
  # females
  polygon(c(seq(min(hembras$TL),max(hembras$TL),l=length(hembras$LL)),
            rev(seq(min(hembras$TL),max(hembras$TL),l=length(hembras$LL)))),
          c(hembras$LL,rev(hembras$UL)),col= adjustcolor("blue4", 0.3),border=F)  
  
  lines(x=seq(min(hembras$TL),max(hembras$TL),l=length(hembras$LL)),
        y=hembras$dN15,col="blue4",lty=2,lwd=2.5)

  # Titulo
  mtext(data$spp[1], side=3, line=0, cex=1)

  # cuadro 
  box(bty="l",lwd=1.1)  
  
}

# graficar la primer especie
plot.spp(dbs[[1]])

# hacer un grafico para cada especie
for(i in 1:length(dbs)) plot.spp(dbs[[i]])

 

Podemos también usar estas funciones para crear gráficos en paneles:

par(mfcol=c(2,2))
par(mar=c(2,3,2,1), oma=c(2.5,2.5,0,0))

for(i in 1:length(dbs)) plot.spp(dbs[[i]])

mtext("Longitud total (cm)", 1, outer = T,line = 0.5, cex = 1.5)
mtext("dN15", 2, outer = T,line = 0.5, cex = 1.5)