neu

Become a useR, not a loseR
Módulo 2: Iteración y funciones

Joan Jiménez-Balado

1 Iteración

1.1 Concepto

El concepto es el concepto.

Las estructuras de control permiten controlar el flujo de las operaciones en R. Más allá de escribir un código línea a línea, podremos decidir qué operaciones queremos realizar en cada momento en función de elementos condicionales e iteraciones. Esto hará que nuestro código sea más sintético y no tengamos que programar tantas operaciones.

Los elementos básicos del control de flujo son:

  • Bloques condicionales: if, else.

  • Bucles: for, while, break.

1.2 Bloques condicionales.

Nos permite poner a prueba una operación lógica y, en función del resultado, realizar una operación u otra.

La sintaxis básica sería:

if(CONDICIÓN){ si se cumple HAZ esto } else si no se cumple haz lo otro.

En el primer bloque hemos aprendido que existen operadores lógicos, los cuales van a ser la base de la sintaxis del bloque if. La gramática que hemos aprendido con los operadores lógicos (que como sabemos pueden ser aplicados de muchas maneras y en muchos objetos) la podremos usar para poner a prueba condiciones. Por ejemplo:

año_tesis <- 1:4 # Generamos un vector c(1L,2L,3L...)

if(año_tesis[1] >= 3){# Miramos si el primer elemento és superior a tres. En este caso, nos devuelve FALSE
  print('Empieza a escribir, membrillo!') # Esto no se llega a ejecutar
} else {
  print('Todavía queda para defender!') # Qué bien, todavía nos falta!
}
## [1] "Todavía queda para defender!"

Es importante darse cuenta que la condición dentro del paréntesis del IF siempre será un lógico. Lo podemos comprobar:

año_tesis[1] >= 3
## [1] FALSE
año_tesis[2] >= 3
## [1] FALSE
año_tesis[3] >= 3
## [1] TRUE
año_tesis[4] >= 3
## [1] TRUE

Así pues, podemos reescribir la fórmula del siguiente modo:

if(TRUE){ haz esto } else haz lo otro.

Podemos ver que funciona:

if(año_tesis[3] >= 3){# Ahora año tesis vale 3
  print('Empieza a escribir, membrillo!') # Esto sí se ejecuta, ya podéis empezar a escribir!
} else print('Todavía queda para defender!') 
## [1] "Empieza a escribir, membrillo!"

No obstante, el else no es obligatorio. Por tanto, si no especificamos és equivalente a:

if(TRUE){ haz esto } else haz nada.

if(año_tesis[1] >= 3){# esto vuelve a valer 1
  print('Empieza a escribir, membrillo!') # Esto no se ejecuta
} # no sucede nada

Como en todos los lenguajes de programación, podemos anidar los bloques if:

papers_publicados <- 0:6

if(año_tesis[3] >= 3){ # Esta condición se vuelve a cumplir, ya que años_tesis[3] = 3.
  print('Empieza a escribir, membrillo!') # Esto se ejecuta. 
  if(papers_publicados[3] >= 2){ # Ponemos a prueba si papers_publicados es igual o superior a 2. En este caso se cumple
    print('Tranqui, yo escribo la tesis por artículos! Lol')  # Esto se ejecuta. 
  }  
} else print('Todavía me queda para defender!') # Esto no se llega a ejecutar. 
## [1] "Empieza a escribir, membrillo!"
## [1] "Tranqui, yo escribo la tesis por artículos! Lol"

Es lógico, por tanto, pensar que también podremos añadir más bloques else:

if(año_tesis[3] >= 3){ # Se cumple.
  print('Empieza a escribir, membrillo!') # Se vuelve a ejecutar
  if(papers_publicados[1] >= 2){ # Esta vez esto no se cumple
    print('Tranqui, yo escribo la tesis por artículos! LoL')  # Esto NO se ejecuta. 
  } else print('Y encima la escribes por capítulos...estás jodido!') # esto sí se ejecuta
} else print('Todavía me queda para defender!') # Esto no se llega a ejecutar. 
## [1] "Empieza a escribir, membrillo!"
## [1] "Y encima la escribes por capítulos...estás jodido!"

Los bloques else los podremos completar con condiciones, generando de este modo bloques else if (muy útiles)

predoc_agonias <- c('No', 'Si')

if(año_tesis[3] >= 3){ 
  print('Empieza a escribir, membrillo!') # esto se ejecuta, año tesis = 3
  if(papers_publicados[1] >= 2){ 
    print('Tranqui, yo escribo la tesis por artículos! LoL')  # Esto NO se ejecuta, papers = 1. 
  } else if (predoc_agonias[2] == 'Si'){ # Se mira esta condición, que esta vez se cumple
    print('Oh mierda, vamos a morir todos!') # Se ejecuta
  } else print('Y encima la escribes por capítulos...estás jodido!') # Esto no se llega a ejecutar
} else print('Todavía me queda para defender!') # Esto no se llega a ejecutar. 
## [1] "Empieza a escribir, membrillo!"
## [1] "Oh mierda, vamos a morir todos!"

Así pues la estructura de los bloques condicionales siempre va a depender del problema que queramos resolver. Para ello podremos usar bloques if, sucesión de bloques if, bloques else, else if

1.3 Loops

1.3.1 Bucle for

El bucle for es la estructura básica de iteración en R, y de los pocos que vamos a necesitar. Por iteración entendemos repetir una misma operación un determinado número de veces. El bucle for es adecuado usarlo cuando conocemos a priori el número de iteraciones que queremos realizar (ya que corresponderán a la longitud de un vector, al número de columnas de un data frame…).

La estructura básica del bucle for sería:

for(i in “something”){ haz algo[i] }

Dónde \(i\) será una variable temporal en la que se asignará el valor de la iteración contenido en ‘in something’, que es el elemento iterable. Un iterable es algo que se puede iterar.

Vamos a ver ejemplos:

n <- 1:7
for(i in n){ # iteramos i en los valores de 1L a 10L. 
  print(i) # printamos i en su valor temporal. 
} #cuando acaban las 10 iteraciones el bucle for termina. 
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
## [1] 7

Este era un ejemplo muy simple en el que \(i\) toma valores del 1 al 7. Por definición los vectores son elementos iterables (como casi todos los objetos de R). En análisis de datos, \(i\) suele servir para hacer subsetting. Veamos un ejemplo:

frutas <- c('platano', 'pera', 'fresa', 'naranja', 'manzana', 'piña', 'kaki') # partimos de un vector de strings de longitud 7
for(j in 1:7){ # i ahora toma valores de 1 a 7.
  print(frutas[j]) # printamos la fruta que se encuentre en la posición 'i'
}
## [1] "platano"
## [1] "pera"
## [1] "fresa"
## [1] "naranja"
## [1] "manzana"
## [1] "piña"
## [1] "kaki"

No obstante, muchas veces para conocer el número de iteraciones que queremos realizar nos podremos ayudar de funciones auxiliares que ya conocemos:

for(i in 1:length(frutas)){ # iteramos de 1 a la longitud de frutas (1:7)
  print(frutas[i]) # printamos la fruta i 
}
## [1] "platano"
## [1] "pera"
## [1] "fresa"
## [1] "naranja"
## [1] "manzana"
## [1] "piña"
## [1] "kaki"

R también permite iterar sobre los elementos de un vector, sin necesidad de especificar el indice, ya que como hemos dicho los vectores son elementos iterables:

for(fruta in frutas){ # en este caso es equivalente a decir que queremos iterar sobre cada elemento del vector, que en este caso es una fruta. 
  print(fruta)
}
## [1] "platano"
## [1] "pera"
## [1] "fresa"
## [1] "naranja"
## [1] "manzana"
## [1] "piña"
## [1] "kaki"

Además, para bucles for de una línea no son necesarios los brackets:

for(fruta in frutas) print(fruta)
## [1] "platano"
## [1] "pera"
## [1] "fresa"
## [1] "naranja"
## [1] "manzana"
## [1] "piña"
## [1] "kaki"

No hay problema en anidar bucles for. Esto será sobretodo útil cuando iteremos sobre objetos de más de una dimensión. Imaginemos que queremos iterar una matriz por columnas y filas:

También podríamos aplicar los bucles sobre una matriz (recordad que las matrices pueden ser representadas como vectores):

matriz_letras <- matrix(letters[1:9], 3,3)
print(matriz_letras)
##      [,1] [,2] [,3]
## [1,] "a"  "d"  "g" 
## [2,] "b"  "e"  "h" 
## [3,] "c"  "f"  "i"
for(i in 1:nrow(matriz_letras)){ # iteramos sobre el número de filas de la matriz.
  # Anidamos otro bucle
  for(j in 1:ncol(matriz_letras)){ # iteramos sobre las columnas
    print(paste0('Estamos en la iteración: ', i, '-', j)) 
    message(matriz_letras[i,j])# printamos la tupla fila-columna
  }
}
## [1] "Estamos en la iteración: 1-1"

a

## [1] "Estamos en la iteración: 1-2"

d

## [1] "Estamos en la iteración: 1-3"

g

## [1] "Estamos en la iteración: 2-1"

b

## [1] "Estamos en la iteración: 2-2"

e

## [1] "Estamos en la iteración: 2-3"

h

## [1] "Estamos en la iteración: 3-1"

c

## [1] "Estamos en la iteración: 3-2"

f

## [1] "Estamos en la iteración: 3-3"

i

Esto cuesta un poco de verse al principio, pero lo podemos intentar entender con el siguiente ejemplo, para saber los valores que toma \(i\) y \(j\)

n <- 1:4
for(i in n){ # i ahora toma valores de 1 a 4. 
  for(j in n){ # j toma también valores de 1 a 4.
    print(c(i,j)) # printamos el vector (i,j)
  }
}
## [1] 1 1
## [1] 1 2
## [1] 1 3
## [1] 1 4
## [1] 2 1
## [1] 2 2
## [1] 2 3
## [1] 2 4
## [1] 3 1
## [1] 3 2
## [1] 3 3
## [1] 3 4
## [1] 4 1
## [1] 4 2
## [1] 4 3
## [1] 4 4

Podremos también incluir las condiciones que creamos pertinentes, para hacer una operación u otra dentro del bucle. Imaginemos que queremos realizar las mismas iteraciones que antes. Pero solo imprimiremos aquellas combinaciones en que ambos números son pares:

for(i in 1:4){
  for(j in 1:4){
    if(i %% 2 == 0 & j %% 2 == 0) print(c(i,j))
  }
}
## [1] 2 2
## [1] 2 4
## [1] 4 2
## [1] 4 4

Estas condiciones serán sobretodo útiles en los bucles for aplicados a matrices/data frames en las que queramos hacer determinados análisis.

Podemos ver otro ejemplo volviendo a la mente de los predocs:

# iniciamos con 0 papers
papers <- 0
# iniciamos randomizando al predoc a tener una mente agonías o ser tranquilp
predoc_agonias <- sample(c('No', 'Si'), 1)
# Iniciamos la tesis: iteramos en cada año de tesis
for(i in 1:4){
  # Informamos del año de tesos
  message(paste0('Iinicio  del año ', i, ' de tesis'))
  # comprobamos el año de tesis
  if(i >= 3){ 
    warning('Empieza a escribir, membrillo!') 
    # anidamos los bloques condicionales de antes
    if(papers >= 2){ 
      print('Tranqui, tengo los artículos escritos! LoL')
    } else if (predoc_agonias == 'Si'){ 
      print('Oh mierda, vamos a morir todos!')
    } else print('Y encima no tienes los papers...estás jodido!') 
  } else print('Todavía me queda para defender!') # Esto no se llega a ejecutar
  # esto siempre se ejecuta
  message(paste0('Fin del año ', i, ' de tesis'))
  # a cada año asignamos una probabilidad 4 vs i de publicar un paper
  papers <- papers + sample(0:1, 1, prob = c(4, i))
  message(paste0('Número de papers publicados: ', papers))
}

Iinicio del año 1 de tesis

## [1] "Todavía me queda para defender!"

Fin del año 1 de tesis

Número de papers publicados: 0

Iinicio del año 2 de tesis

## [1] "Todavía me queda para defender!"

Fin del año 2 de tesis

Número de papers publicados: 1

Iinicio del año 3 de tesis

Warning Empieza a escribir, membrillo!

## [1] "Oh mierda, vamos a morir todos!"

Fin del año 3 de tesis

Número de papers publicados: 2

Iinicio del año 4 de tesis

Warning Empieza a escribir, membrillo!

## [1] "Tranqui, tengo los artículos escritos! LoL"

Fin del año 4 de tesis

Número de papers publicados: 3

1.4 While:

While es un tipo de bucle que ejecuta un procedimiento siempre y cuando una condición (que hemos definido nosotros) se mantiene como verdadera. En general, en análisis de datos no lo solemos emplear tanto. Aunque pueden existir escenarios que requieran del uso del bucle while. Será importante vigilar especialmente la condición que indicamos, ya que si no aseguramos que pueda llegar el momento que dicha condición se evalúe como FALSE, el bucle se ejecutará de manera infinita. Esta sería la estructura básica del while:

while(#condición){ haz esto }

Veamos un ejemplo:

cont = 0
while(cont < 7){
  print(cont)
  cont = cont+1
}
## [1] 0
## [1] 1
## [1] 2
## [1] 3
## [1] 4
## [1] 5
## [1] 6
print(cont)
## [1] 7

Como vemos hemos utilizado una variable auxiliar para regular el bucle while. El “cuerpo” del bucle se sigue ejecutando hasta que la condición indicada es falsa (cont > 7).

Un ejemplo de uso del bucle while es cuando necesitamos asegurarnos que el usuario de nuestro programa introduzca una variable/respuesta del tipo determinado. Por ejemplo, queremos generar un programa que evalúe si se ha introducido un valor de minimental válido (números de 0 a 30). Esto lo podríamos encapsular en un bucle while para capturar errores (p.e., si el usuario introduce 32) y corregirlo:

mmse <- NA
cont = 0
while(mmse < 0 | mmse > 30 | is.na(mmse)){ 
  mmse <- readline(prompt = 'Introduzca el valor del MMSE (0 a 30): ')
  mmse <- suppressWarnings(as.numeric(mmse))
  if(cont > 1 & cont < 4) print('tate, espavila')
  if(cont > 3) print('Eres un poco tonto...')
  cont = cont + 1
}
print(paste0('Valor del minimental introducido: ', mmse))

En el ejemplo hemos añadido diferentes condiciones y bucles para controlar diferentes tipos de errores al introducir el minimental. Por ejemplo, introducir valores no numéricos, introducir valores fuera de rango…Se podría también capturar errores de introducir decimales, por ejemplo.

Así pues, las dos diferencias principales entre el bucle while y el bucle for son que el bucle while lo usamos cuando no conocemos el número de iteraciones a priori.

1.5 Apply

La familia de funciones apply es otro modo (muy útil de realizar iteraciones en R). Hay muchas de estas funciones, pero todas comparten el mismo principio: aplican una función sobre un iterable.

La más famosa (y útil) es apply, la cual aplica una función sobre una dimensión de una matriz o un data.frame.

Por ejemplo, volvamos al data-set de ejemplo de las flores de iris.

# subset de iris dataset
iris_sub <- iris %>%
  select(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width)

Como vemos, hemos hecho un subset de las variables contínuas de esta base. Ahora imaginemos que quisiéramos encontrar el percentil 90 de cada variable. Una manera, si quisiéramos iterar, sería hacer lo siguiente:

for(var in names(iris_sub)){
  print(paste0('Pc 90 ', var, ' ', quantile(iris_sub[,var], probs = .9)))
} 
## [1] "Pc 90 Sepal.Length 6.9"
## [1] "Pc 90 Sepal.Width 3.61"
## [1] "Pc 90 Petal.Length 5.8"
## [1] "Pc 90 Petal.Width 2.2"

Otra manera más fácil sería hacer uso de apply. Apply toma como argumentos:

  • Array de interés.

  • Margen sobre el que aplicar la función: filas (1) o columnas (2) generalmente.

  • Función a aplicar.

  • Parámetros extra de la función:

apply(iris_sub, # data.frame a usar 
      2, # aplicamos la función sobre las columnas
      quantile, # función
      probs = .9 # parámetros adicionales de la función. 
      )
## Sepal.Length  Sepal.Width Petal.Length  Petal.Width 
##         6.90         3.61         5.80         2.20

Bastante más fácil, no?

Otra de esta familia de funciones es tapply, la cual aplica una función sobre una variable en función de los niveles de un factor. Por ejemplo, imaginemos que queremos la media de la variable Petal.Width de cada especie de flor:

tapply(iris$Petal.Width, # vector a aplicar la función
       iris$Species, # variable grupo
       mean # función a aplicar
       ) 
##     setosa versicolor  virginica 
##      0.246      1.326      2.026

1.6 Next & Break

Tanto los bucles for como los bucles while los podemos completar con las expresiones next y break.

  • Next nos permite saltar un paso en la iteración.

  • Break nos permite “salir del bucle”

Veamos un ejemplo con next. Imaginemos que tenemos la concentración de 20 proteínas de 100 pacientes. A las proteínas les pondremos nombres aleatorios de 7 letras.

Intentamos simular estos datos:

set.seed(777)
datos_proteinas <- matrix(-777, 100, 20) # creamos una matriz vacía,
nombres <- c() # también un vector vacío que contendrá los nombres de las variables del dataframe

for(i in 1:20){ # iteramos por cada proteína
  # no norm
  no_norm <- sample(c(T,F), 1, prob = c(1, 5)) # algunas variables serán no normales
  if(no_norm) datos_proteinas[,i] = rexp (100, 10) # si es no normal
  else datos_proteinas[,i] = rnorm(100, 0, 1) # si es normal
  rand <- sample(1:26, 7, replace = T) # generamos los índices aleatorios de las letras a usar
  nombre <- paste(LETTERS[rand], collapse = '') #  generamos el nombre aleatorio
  nombres <- c(nombres, nombre) # lo concatenamos a la lista de nombres
}
# lo convertimos a una dataa-frame
df_proteinas <- data.frame(datos_proteinas)
names(df_proteinas) <- nombres

Ahora imaginemos que nos piden comparar la concentración de estas proteínas entre casos y controles, pero solos queremos hacer estos tests en las que son normales (no sabemos cómo analizar las no normales). Además, nos piden que en el report aparezca el nombre de la proteína, el fold change y el p-valor.

Primero generemos una variable caso/control:

set.seed(779)
grupo <- factor(sample(0:1, 100, replace = T))

Generaremos una vector dónde pondremos los resultados (nombre, p-valor, fold change).

resultados <- c()

Ahora ya podemos generar el loop:

for(i in 1:ncol(df_proteinas)){ # Realizaremos tantas iteraciones como proteínas. 
  target <- names(df_proteinas)[i] # cogemos el nombre de la proteína.
  prot <- df_proteinas[,i] # valores de la proteína en la iteración actual.
  p_norm <- nortest::lillie.test(prot)$p
  if(p_norm < .05) {
    print(paste0('La proteína ', target, ' no es normal'))
    next # si es del seguimiento pasamos a la siguiente iteración
  }
  p_valor = t.test(prot ~ grupo)$p.value # obtenemos el p valor
  group_means <- tapply(prot, grupo, mean) # obtenermos las medias de cada grupo con tapply
  fold_change <- group_means[2]/group_means[1] # calculamos el foldcchage
  resultados <- rbind(resultados, c(target, p_valor, fold_change)) # vamos generanndo una matriz
}
## [1] "La proteína CJVDVPS no es normal"
## [1] "La proteína NIGVKBK no es normal"
## [1] "La proteína VBSQWKU no es normal"
## [1] "La proteína QWFQYGE no es normal"
## [1] "La proteína IVAORJA no es normal"
## [1] "La proteína XDEJNNB no es normal"
resultados <- data.frame(resultados)
names(resultados) <- c('Prot', 'p-value', 'FC')
resultados

Como vemos, next nos permite saltar operaciones dentro del bucle for. De esta manera podemos generar códigos más eficientes que ejecuten solo aquellas operaciones estrictamente necesarias.

Break, en cambio, nos permite parar la ejecución. Por ejemplo, imaginemos que solo queremos saber cual es la primera proteína que muestra diferencias significativas, sin importar el resto:

for(i in 1:ncol(df_proteinas)){
  target <- names(df_proteinas)[i]
  p_valor = t.test(df_proteinas[,i] ~ grupo)$p.value
  if(p_valor < 0.05){
    print(target)
    print(paste('Iteración nº ', i, collapse = ''))
    break
  }
}
## [1] "CJVDVPS"
## [1] "Iteración nº  6"

Este código sólo nos imprime la primera proteína significativa.

Antes decíamos que while puede conllevar fácilmente la creación de bucles infinitos. Break nos permite controlar mejor este tipo de bucles. Por ejemplo, vamos a crear una aplicación en la que preguntemos al usuario 2+2. Le daremos 3 intentos. Si al tercer intento falla, detenemos la ejecución:

result = 0 
cont = 0

while(result != 4 | is.na(result)){
  if(cont > 0) print('Eres un poco tonto...vuelve a intentarlo')
  result <- suppressWarnings(as.numeric(readline(prompt = '2+2: ')))
  cont = cont + 1
  if(cont > 2 & (result != 4 | is.na(result))) {
    print('Aprende a sumar!')
    break
  }
}

Vamos a cambiar un poco el tema de los ejemplos…nos vamos a ir a las mates. Uno de los problemas que ha roto más la cabeza de los matemáticos ha sido el problema de los tres cubos (https://www.bbc.com/mundo/noticias-49675503). Este problema consiste en hallar tres cubos (x, y, z) enteros la suma de los cuales nos proporcione otro número entero específico (https://www.unocero.com/ciencia/reto-ludico-problema-de-tres-cubos/). A modo de ecuación:

\[ x^3 + y^3 + z^3 = n \]

De hecho se ha demostrado que todos los números enteros del 1 al 100 pueden ser representados así. No obstante, existen números como el 33 o el 42 que requiere utilizar números muy altos:

\[ (8,866,128,975,287,528)³ + (–8,778,405,442,862,239)³ + (–2,736,111,468,807,040)³ = 33 \]

Aquí teneis un video muy divertido de esto: https://www.youtube.com/watch?v=jg7PzVxzDY4

Nosotros vamos a hacer una versión simplificada. Queremos aplicar este problema a números ‘n’ del 1 al 100, pero con combinaciones de x, y, z que solo presenten valores de -100 a 100. No obstante, consideraremos todas las posibles combinaciones, que serían unos 8 millones aproximadamente. Si en estas combinaciones, encontramos una combinación que genere nuestre número ‘n’ pararemos la ejecución.

n <- 15
stop <- FALSE # este objeto nos ayudará a parar la ejecución

for(i in -100:100){
  if(stop) break # flag - NIVEL 1
  for(j in -100:100){ 
    if(stop) break # flag - NIVEL 2
    for(k in -100:100){ # aquí llegamos a la combinación i-j-k
      result = i^3+j^3+k^3 # aplicamos la fórmula
      if(result == n){ # si hemos encontrado una solución
        print('Solución encontrada!!!')
        print(c(i,j,k)) # printamos
        stop = TRUE # el flag pasa a true. 
        break # se detiene este bucle, y pasamos a los niveles superiores. Que serán interrumpidos gracias al 'stop'.
      }
    }
  }
}
## [1] "Solución encontrada!!!"
## [1] -46  23  44

Fijémonos que no basta con indicar que queremos para el 3r bucle (el ‘k’). Ya que cada break, actúa a nivel de bucle. Por eso es importante incluir los ‘flags’ para que nos ayuden a detener ejecuciones superiores.

2 Las funciones en R

2.1 Concepto

A medida que vamos aprendiendo a crear nuestro código en R, nos iremos encontrado con situaciones más complejas. No es raro encontrarnos ante situaciones en las que no exista ninguna función prevista para lo que queramos hacer. Es por ello, que podremos crear nuestras propias funciones en R. Una función es un pequeño (o gran) programa que encapsula un código para ser utilizado en un contexto determinado. A las funciones les introducimos valores (input) y nos van a devolver resultados (output).

2.2 Sintaxis básica

Las funciones en R podemos definirlas con la función function(). La sintaxis básica tomará la siguiente forma:

my_fun <- function(argumentos){ Do_something(argumentos) }

Veamos a crear nuestra primera función! Esta función imprimirá por pantalla la frase “Leroy Jenkins” tantas veces como lo indique el usuario. Vamos a ver como programarlo:

my_first_fun <- function(x){
  for(i in 1:x) print('Leroooy Jenkins')
}

my_first_fun(5)
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"

Funciona correctamente!

Fijémonos en lo siguiente:

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

Las funciones son objetos de tipo function y se guardan en nuestro espacio de trabajo, como cualquier objeto de R. De hecho, las funciones son “first-class objects”, estando al nivel de los vectores, matrices…

Si hacemos una llamada de la función anterior sin argumentos veremos que nos devuelve un mensaje de error conforme “x no tiene ningún valor por defecto”.

Cuando especificamos una función podemos incluir valores por defecto a nuestros argumentos (la parte de dentro del paréntesis). Por ejemplo:

my_first_fun <- function(x = 2){ # Especificamos el valor por defecto, que será 2. 
  for(i in 1:x) print('Leroooy Jenkins')
}

my_first_fun()
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"

Como vemos, ahora cuando no especificamos ningún argumento, lo printa dos veces.

No obstante, la función que hemos creado hasta ahora, solo printa por pantalla, no devuelve ningún valor:

useless <- my_first_fun()
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
print(useless)
## NULL

Toda función devuelve por defecto NULL. Imaginemos ahora, que queremos que la función nos devuelva el número de letras que se han impreso por pantalla. Lo podríamos hacer así:

my_first_fun <- function(x = 1){ 
  for(i in 1:x) print('Leroooy Jenkins')
  chars = nchar('Leroooy Jenkins') * x
  chars
}

my_chars <- my_first_fun(4)
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
my_chars
## [1] 60

Como vemos la función nos devuelve el valor de caracteres, que puede ser instanciado en un objeto. Nos devuelvo este valor porque es la última orden que se ha evaluado en la función my_first_fun. En R las funciones siempre devuelven el último valor evaluado, por defecto. No obstante, lo podemos hacer explícito:

my_first_fun <- function(x = 1){ 
  for(i in 1:x) print('Leroooy Jenkins')
  chars = nchar('Leroooy Jenkins') * x
  return(chars)
}

my_chars <- my_first_fun(4)
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
my_chars
## [1] 60

Como vemos el resultado es el mismo, y solo hemos tenido que usar la función return().

Obviamente una función puede tener muchos argumentos. Imaginemos que queramos generar un argumento que gestione si ‘Leroy Jenkins’ se imprima por la pantalla o no.

my_first_fun <- function(x = 1, y = T){ # añadimos un argumento 'y' que por defecto es TRUE.  
  if(y){ # añadimos un bloque if.
    for(i in 1:x) print('Leroooy Jenkins')
  }
  chars = nchar('Leroooy Jenkins') * x
  return(chars)
}

my_chars <- my_first_fun(x = 7, y = FALSE)
my_chars
## [1] 105

Es interesante observar que la llamada anterior es equivalente a esta:

my_chars <- my_first_fun(7, FALSE) # No indicamos el nombre de los argumentos. 
my_chars
## [1] 105

Tanto “x” como “y” son argumentos con nombre (named arguments). Esto simplemente quiere decir que les hemos puesto un nombre. Esto es cómodo de usar sobretodo cuando la función tiene muchos argumentos y no recordamos en que orden están. Fijémonos que mientras especifiquemos el nombre, no importa el orden en que introduzcamos los argumentos en su llamada:

my_chars <- my_first_fun(y = F, x = 3) # Invertimos el orden, pero indicamos el argumento
my_chars
## [1] 45

Vemos que funciona. No obstante, la siguiente llamada no sería correcta:

my_chars <- my_first_fun(FALSE, 1) # Invertimos el orden SIN especificar el nombre del argumento. 
## [1] "Leroooy Jenkins"
## [1] "Leroooy Jenkins"
my_chars 
## [1] 0

Fijémonos que nos ha imprimido 2 iteraciones y nos devuelve, contrariamente, 0 carácteres. La explicación a esto es que hay que recordar que en R FALSE equivale a 0.

Por tanto, cuando no indiquemos el nombre, lo que predomina es la posición.

Tampoco importa si algunos argumentos los especificamos por el nombre y otros por la posición (siempre y cuando los que son determinados por posición, la posición sea correcta).

my_chars <- my_first_fun(y = FALSE, 2) # Invertimos el orden SIN especificar el nombre del segundo argumento. 
my_chars 
## [1] 30

R lo que hace en este caso es instanciar el argumento que indicamos por el nombre y “extraerlo” de la lista de argumentos. Los demás argumentos se espera que respeten el orden.

Así pues, tenemos dos maneras principales de hacer llamadas a funciones:

  • Especificando el nombre de los argumentos.

  • Introduciendo los argumentos en el orden correcto.

En líneas generales, si estamos haciendo un código para nosotros no importa como lo hagamos (mientras el código haga lo que queremos). No obstante, se considera pulcro que cuando escribimos código indiquemos el nombre de los argumentos en las funciones y, en la medida de lo posible, también intentemos mantener el orden. Esta recomendación ayuda a que el código sea más fácil de leer. Esto ayudará a otras personas a entender mejor nuestro código.

2.3 Evaluación Perezosa

La evaluación perezosa es una de esas cosas que son curiosas en R. Esto básicamente quiere decir que las funciones se evalúan a nivel de sentencia, y no de transacción. O lo que es lo mismo, las líneas de código se evalúan 1 a 1. Del mismo modo, los argumentos se van utilizando a medida que se necesitan. Por tanto, podríamos tener un argumento que no se usara en una función:

aux_fun <- function(a, b){
  a+1 
}
aux_fun(a = 3)
## [1] 4

Como vemos, el argumento \(b\) nunca se llega a evaluar. También podría pasar:

aux_fun <- function(a, b){
  print(a)
  print(b)
}
# aux_fun(a = 3)

Fijémonos que antes de producirse el error, se ha printado correctamente \(a\). Esto es debido a que se ha evaluado línea a línea. La ejecución se ha detenido en el momento de encontrar el error, pero print(a) ya se había ejecutado.

2.4 Complicando la cosa

Antes de empezar introduciremos algún tema de representación gráfica. Vamos a introducir la función plot. Los argumentos más importantes de esta función son especificar los ejes \(x\) e \(y\), los cuales corresponden a vectores.

plot(x, y, type = {‘d’|‘l’|…}, main = ‘titulo principal’, col = ‘color’).

x <- seq(-pi, pi, 0.01)
y <- sin(x)

plot(x, y, 'l', col = 'red', ylim = c(-3,3))
lines(y, x, col = 'blue') # Podemos introducir líneas adicionales. Habrá que volver a especificar 'x' e 'y'. 
lines(y, -x, col = 'green')
lines(x, -y, col = 'black')

2.4.1 Evaluación del valor atómico de los argumentos

Como hemos visto, cuando especificamos una función podemos incluir en su sintaxis bucles, bloques condicionales, etc. Estos últimos nos pueden ayudar a asegurar que en la función se hayan indicado el tipo de argumentos que correspondan. Imaginemos que hacemos la siguiente función que en base a un parámetro ‘x’ nos calcula la función:

\[f(x) = x^2+2x+3\]

aux_fun <- function(x){
  2*x+2*x+2
}
aux_fun(3)
## [1] 14

El valor de \(x\) debe ser numérico/entero. Esto lo podemos gestionar del siguiente modo:

aux_fun <- function(x){
  if(is.numeric(x) | is.integer(x)){ # ponemos a prueba la condición
    2*x+2*x+2
  } else print('Error: El argumento debe ser numérico') # en caso de que no sea numérico devolvemos este mensaje
}
aux_fun('a')
## [1] "Error: El argumento debe ser numérico"

Incluir estas condiciones puede ayudar a realizar códigos mejor puestos a punto. Es importante prevenir y capturar los errores. Por ejemplo, este error podría gestionarse pidiendo al sujeto que introduzca un número válido:

aux_fun <- function(x){
  if(is.numeric(x) | is.integer(x)){ # comprobamos que es numérico
    2*x^2+2*x+2
  } else { # si no lo es se ejecuta este código
    while(is.numeric(x) == F | is.na(x)){ 
      print('Error: El argumento debe ser numérico')
      x <- suppressWarnings(as.numeric(readline(prompt = 'Introduzca un valor numérico:')))
    } 
    2*x+2*x+2
  }
}
aux_fun('a')

La gestión de errores es uno de los pasos más importantes a la hora de diseñar una función. Normalmente a los errores que aparecen en una función se les llama excepciones (del inglés, exceptions). La mala noticia es que R tiene un sistema de gestión de errores bastante pobre en comparación a otros programas. Esto hace que sea difícil gestionar errores complejos.

Aprovecho esta función “hecha a mano” para recordar que, como toda función, puede aplicarse a todos los elementos de un vector:

x <- seq(-20, 20, 0.001)
y <- aux_fun(x)

plot(x, y, 'l', col = 'red')

2.4.2 Instacia funciones

Las funciones en R tienen otra peculiaridad. Cuando generamos una función R aborda la asignación de objetos usados en la función de una manera bastante curiosa. En resumen, no se va a generar una instancia independiente. Aunque va a priorizar los elementos definidos en los argumentos de la función, también va usar el ambiente de trabajo (global environment) que estemos usando (en el que se habrá definido la función). Por ejemplo, observemos este código:

foo_fun <- function(x, y){
  x*y/z
}

Fijémonos que \(z\) no está incluida en los argumentos de la función. A esto se le llama una variable libre y su valor se asignan en función de una serie de normas (que vienen predeterminadas). Por defecto, las variables libres se buscan en el ambiente de trabajo dónde la función ha sido definida. Entonces, la unión de un ambiente (colección de variables) y una función se le denomina el closure (cierre).

z <- 3

foo_fun(3,3)
## [1] 3

2.4.3 Funciones anidadas

Dentro de una función podemos anidar otras funciones (que serán definidas in situ). Esto es un recurso bastante útil cuando estamos creando funciones complejas para realizar análisis de datos. De este modo podremos hacer que una función sea el wrapper de otras funciones.

Por ejemplo, crearemos una función que nos devuelva el histograma+función de densidad de una columna indicada. Vamos a usar de ejemplo el iris data-set, que ya lo conocemos:

hist_fun <- function(x, data){
  hist(data[,x], freq = F, main = x, xlab = '') # Como vemos estados usando una función del sistema, dentro de nuestra función.
  lines(density(data[,x]), col = 'red')
}

hist_fun('Petal.Length', datasets::iris)

Como hemos visto, no hay problema en usar funciones del sistema dentro de nuestra función. Otro ejemplo con el iris. Vamos a crear una función que nos haga los boxplots de un conjunto de columnas target:

iris_df <- datasets::iris
targets <- names(iris_df[1:4])
group <- names(iris_df[5])

boxplot_fun <- function(targets, group, data){
  for(target in targets){
    boxplot(data[,target] ~ data[,group], 
            frame = F, ylab = target, xlab = group, 
            notch = T, col = colors()[73:(73+length(targets))],
            boxwex = 0.32)
  }
}

par(mfrow = c(2,2))
boxplot_fun(targets, group, iris_df)

Warning in (function (z, notch = FALSE, width = NULL, varwidth = FALSE, : some

notches went outside hinges (‘box’): maybe set notch=FALSE

De hecho, incluso podemos generar funciones anidadas con nuestras funciones. Por ejemplo, imaginemos que queremos crear una función que, dada una función matemática y un punto en esta x_0, nos devuelva su derivada, y además nos realice el gráfico pertinente en el que observemos la tangente en un punto:

f1 <- expression(x^2+3*x+2) # con expression() podemos incluir patrones de fórmulas

derivative <- function(formula, x_0){
  x <- seq(-100, 100, 0.01) # Definimos x.
  y <- eval(formula) # generamos y evaluamos la expresión, con la instancia de x definida en la línea anterior
  formula_D <- D(formula, 'x') # generamos la derivada
  
  aux <- function(formula_D, x = x_0){ # Definimos una nueva función. En esta función x será x_0. 
    y_0 <- eval(formula)
    slope <- eval(formula_D) # Evaluamos y_0 y la pendiente.
    intercept <- slope*-x+y_0 # Calculamos la intersección
    return(c(y_0, slope, intercept)) # Nos devuelve un vector con los parámetros de la recta
  }
  
  recta <- aux(formula_D, x_0) # generamos la llamada. 
  plot(x, y, 'l', col = 'black') # Realizamos el plot
  points(x_0, recta[1], pch = 16, col = 'red')
  abline(recta[3], recta[2], col = 'red')
  
}

derivative(f1, -43)

2.4.4 Instancias y funciones anidadas

Fijémonos en estas tres funciones:

potencia_1 <- function(n){
  x <- 2 # Definimos aquí la 'x'
  elevado_a <- function(){
    x <- 4 # Aquí también
    n^x
  }
  elevado_a()
} 

potencia_2 <- function(n){
  x <- 2 # Definimos aquí la 'x'
  elevado_a <- function(){
    n^x
  }
  elevado_a()
}

potencia_3 <- function(n){
  elevado_a <- function(){
    n^x
  }
  elevado_a()
}

Ahora veamos que sucede:

x <- 3
potencia_1(2)
## [1] 16
potencia_2(2)
## [1] 4
potencia_3(2)
## [1] 8

Como vemos la priorización de que \(x\) usar es un proceso de “abajo” a “arriba”.

  • Primero se buscar si la función está definiendo el elemento de interés.

  • Si esto falla se busca en el ambiente de trabajo de la función.

  • Si esto también falla, se busca en los niveles de funciones superiores.

2.4.5 Try

Como decíamos antes, es importante gestionar los errores de nuestras funciones. Miremos que pasa en la siguiente función:

fus_fun <- function(x, y){
  result1 = round(log(x), 2)
  result2 = round(log(y), 2)
  print(paste(c('El resultado es: ', result1), collapse = ''))
}

fus_fun(3,2)
## [1] "El resultado es: 1.1"

Funcionaría correctamente.

Miremos que pasa ahora:

#fus_fun(3, 'a')

La función no se ejecuta del todo ya que en la segunda línea hay un error. Fijémonos que result2 no se llega a usar en la función, así que debería ser independiente de la última línea de código. Try nos permitirá que la ejecución de la función siga, aunque se haya producido un error.

fus_fun <- function(x, y){
  result1 = try(round(log(x), 2))
  result2 = try(round(log(y), 2))
  print(paste(c('El resultado es: ', result1), collapse = ''))
}

fus_fun(3,'a')
## Error in log(y) : non-numeric argument to mathematical function
## [1] "El resultado es: 1.1"

Como vemos, nos imprime el error. Pero esta vez la ejecución sigue. No obstante, try, aunque es útil, no nos permite gestionar errores específicos. Para eso tenemos tryCatch().

2.4.6 TryCatch

TryCatch() nos permite capturar los warnings i errores de nuestras funciones y gestionarlos por separado.

fus_fun <- function(x, y){
  result1 = round(log(x), 2)
  result2 = tryCatch(round(log(y), 2),  # El primer argumento es para la función a intentar.
                     warning = function(w){print('Número negativo'); NA}, # Función si warning.
                     error = function(w){print('Valor no numérico'); NA}) # Función si error. 
  print(paste(c('Los resultados son: ', result1, ' i ', result2), collapse = ''))
}

fus_fun(3, -3)
## [1] "Número negativo"
## [1] "Los resultados son: 1.1 i NA"
fus_fun(3,'a')
## [1] "Valor no numérico"
## [1] "Los resultados son: 1.1 i NA"