Estructuras de control, bucles y funciones

Cuadernos prácticos del Máster de Bioinformática (curso 2024-2025)

Author

Javier Álvarez Liébana

Una estructura de control se compone de una serie de comandos orientados a decidir el camino que tu código debe recorrer

 

Si has programado antes, quizás te sea familiar las conocidas como estructuras condicionales tales como if (blabla) {...} else {...} o bucles for/while (a evitar siempre que podamos).

1 Estructuras condicionales

Una de las estructuras de control más famosas son las conocidas como estructuras condicionales: si pasase algo… entonces…

1.1 If

La más simple es la conocida como if.

SI (IF) un conjunto de condiciones se cumple (TRUE), entonces ejecuta lo que haya dentro de las llaves

Por ejemplo, la estructura

if (x == 1) { código A }

lo que hará será ejecutar el código A entre llaves pero SOLO SI la condición entre paréntesis es cierta (solo si x es 1). En cualquier otro caso, no hará nada.

Para ilustrarlo definamos un vector de edades de 8 personas

edad <- c(14, 17, 24, 56, 31, 20, 87, 73)
edad < 18
[1]  TRUE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE

Nuestra estructura condicional hará lo siguiente: si existe algún menor de edad, imprimirá por pantalla un mensaje.

if (any(edad < 18)) { 
  
  print("Existe alguna persona menor de edad")
  
}
[1] "Existe alguna persona menor de edad"

En caso de que las condiciones no sean ciertas dentro de if() (FALSE), no sucede nada

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
}

No obtenemos ningún mensaje porque la condición all(edad >= 18) no es TRUE, así que no ejecuta nada.

1.2 If-else

La estructura if (condicion) { código A } puede combinarse con un

if (condicion) { código A } else { código B }

cuando la condición se verifica se hará A, pero cuando la condición no se cumpla, se ejecutará el código alternativo B dentro de else { }, permitiéndonos decidir que sucede cuando se cumple y cuando no.

Por ejemplo, if (x == 1) { código A } else { código B } ejecutará A si x es igual a 1 y B en cualquier otro caso. Piensa cuánto valdrá y en el código inferior:

x <- 3

if (x == 1) {
  
  y <- 2
  
} else {
  
  y <- -1
  
}

En el ejemplo anterior y valdrá -1 ya que la condición no se cumple y el código tomará el camino del else { ... }. En el ejemplo anterior de las edades, vamos a imprimir por pantalla una frase si todos son mayores de edad y otra si no.

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
} else {
  
  print("Existe alguna persona menor de edad")
}
[1] "Existe alguna persona menor de edad"

 

Esta estructura if - else puede ser anidada: imagina que queremos ejecutar un código si todos son menores; si no sucede, pero todos son mayores de 16, hacer otra cosa; en cualquier otra cosa, otra acción.

if (all(edad >= 18)) { 
  
  print("Todos son mayores de edad")
  
} else if (all(edad >= 16)) {
  
  print("Hay algún menor de edad pero todos con 16 años o más")
  
} else { print("Hay alguna persona con menos de 16 años") }
[1] "Hay alguna persona con menos de 16 años"
Truco

Puedes colapsar las estructuras haciendo click en la flecha a la izquierda que aparece en tu script.

1.3 If-else vectorizado

Esta estructura condicional se puede vectorizar (en una sola línea) con if_else() (del paquete {dplyr} que veremos en profundidad más adelante), cuyos argumentos son

  • la condición a evaluar
  • lo que sucede cuando se cumple y cuando no
  • un argumento opcional para cuando la condición a evaluar es NA

Vamos a etiquetar sin son mayores/menores y un “desconocido” cuando no conocemos

library(dplyr)

Attaching package: 'dplyr'
The following objects are masked from 'package:stats':

    filter, lag
The following objects are masked from 'package:base':

    intersect, setdiff, setequal, union
edad <- c(NA, edad)
if_else(edad >= 18, "mayor", "menor", missing = "desconocido")
[1] "desconocido" "menor"       "menor"       "mayor"       "mayor"      
[6] "mayor"       "mayor"       "mayor"       "mayor"      

En R base existe ifelse(): no deja especificar que hacer con los ausentes pero permite especificar distintos tipos de datos en TRUE y en FALSE.

1.4 💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 ¿Cuál es la salida del siguiente código?

if_else(sqrt(9) < 2, sqrt(9), 0)
Code
La salida es 0 ya que sqrt(9) es igual 3, y dado que no es menor que 2, devuelve el segundo argumento que es 0

📝 ¿Cuál es la salida del siguiente código?

x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1)
Code
La salida es el vector c(0, NA, NA, 1) ya que sqrt(1) sí es menor que 2, sqrt(9) no lo es, y tanto en el caso de sqrt(NA) (raíz de ausente) como sqrt(-1) (devuelve NaN, not a number), su raíz cuadrada no puede verificarse si es menor que 2 o no, así que la salida es NA.

📝 Modifica el código inferior para que, cuando no se pueda verificar si la raíz cuadrada de un número es menor que 2, devuelva -1

x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1)
Code
x <- c(1, NA, -1, 9)
if_else(sqrt(x) < 2, 0, 1, missing = -1)

📝 ¿Cuál es son los valores de x e y del código inferior para z <- 1, z <- -1 y z <- -5?

z <- -1
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Code
En primero caso x = 1 e y = -1. En el segundo caso x = 1 e y = 1. En el tercer caso -2.5 y 5

📝 ¿Qué pasaría si ejecutamos el siguiente código? Spoiler: da error. ¿Por qué? ¿Cómo solucionarlo?

z <- c(-1, 1, 5)
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Code
Da error ya que en los `if (condición) { } else { }` "clásicos" necesitamos que
la condición tenga longitud uno (un solo valor TRU/FALSE)
Code
# para arreglarlo podemos hacer un if_else vectorial
z <- c(-1, 1, -5)
library(dplyr)
x <- if_else(z > 0, z^3, if_else(abs(z) < 2, z^4, z/2))
y <- if_else(z > 0, -sqrt(z), if_else(abs(z) < 2, sqrt(-z), abs(z)))

📝 ¿Qué sucederá si ejecutamos el código inferior?

z <- "a"
if (z > 0) {
  
  x <- z^3
  y <- -sqrt(z)
  
} else if (abs(z) < 2) {
  
  x <- z^4
  y <- sqrt(-z)
  
} else {
  
  x <- z/2
  y <- abs(z)
  
}
Code
# dará error ya que no es un argumento numérico
Error in z^3 : non-numeric argument to binary operator

📝 Del paquete {lubridate}, la función hour() nos devuelve la hora de una fecha dada, y la función now() nos devuelve fecha y hora del momento actual. Con ambas funciones haz que se imprima por pantalla (cat()) “buenas noches” solo a partir de las 21 horas.

Code
# Cargamos librería
library(lubridate)

# Fecha-hora actual
fecha_actual <- now()

# Estructura if
if (hour(fecha_actual) > 21) {
  
  cat("Buenas noches") # print/cat dos formas de imprimir por pantalla
}

2 Bucles

Aunque en la mayoría de ocasiones se pueden reemplazar por otras estructuras más eficientes y legibles, es importante conocer una de las expresiones de control más famosas: los bucles.

  • for { }: permite repetir el mismo código en un número prefijado y conocido de veces.

  • while { }: permite repetir el mismo código pero en un número indeterminado de veces (hasta que una condición deje de cumplirse).

2.1 Bucles for

Un bucle for es una estructura que permite repetir un conjunto de órdenes un número finito, prefijado y conocido de veces dado un conjunto de índices.

 

Vamos a definir un vector x <- c(0, -7, 1, 4) y otra variable vacía y. Tras ello definiremos un bucle for con for () { }: dentro de los paréntesis indicaremos un índice y unos valores a recorrer, dentro de las llaves el código a ejecutar en cada iteración (en este caso, rellenar y como x + 1)

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:4) {
  
  y[i] <- x[i] + 1
  
}

Fíjate que debido a que R funciona de manera vectorial por defecto, el bucle es lo mismo que hacer x + 1 directamente.

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:4) {
  
  y[i] <- x[i] + 1
  
}
y
[1]  1 -6  2  5
y2 <- x + 1 # hacen lo mismo
y2
[1]  1 -6  2  5

Otra opción habitual es indicar los índices de manera «automática»: desde el primero 1 hasta el último (que corresponde con la longitud de x length(x))

x <- c(0, -7, 1, 4)
y <- c()

for (i in 1:length(x)) {
  
  y[i] <- x[i] + 1
  
}
y
[1]  1 -6  2  5

Así la estructura general de un bucle for será siempre la siguiente

for (índice in conjunto) { 
  código (dependiente de i)
}

SIEMPRE sabemos cuántas iteraciones tenemos (tantas como elementos haya en el conjunto a indexar).

 

Podemos ver otro ejemplo de bucle combinando números y textos: definimos un vector de edades y de nombres, e imprimimos el nombre y edad i-ésima.

nombres <- c("Javi", "Sandra", "Carlos", "Marcos", "Marta")
edades <- c(33, 27, 18, 43, 29)
library(glue)

for (i in 1:5) { 
  
  print(glue("{nombres[i]} tiene {edades[i]} años")) 
  
}
Javi tiene 33 años
Sandra tiene 27 años
Carlos tiene 18 años
Marcos tiene 43 años
Marta tiene 29 años

Aunque normalmente se suelen indexar con vectors numéricos, los bucles pueden ser indexados sobre cualquier estructura vectorial, da igual de que tipo sea el conjunto

library(stringr)
week_days <- c("monday", "tuesday", "wednesday", "thursday",
               "friday", "saturday", "sunday")

for (days in week_days) {
  
  print(str_to_upper(days))
}
[1] "MONDAY"
[1] "TUESDAY"
[1] "WEDNESDAY"
[1] "THURSDAY"
[1] "FRIDAY"
[1] "SATURDAY"
[1] "SUNDAY"

2.1.1 Bucles + condicionales

Vamos a combinar las estructuras condicionales y los bucles: usando el conjunto swiss del paquete {datasets}, vamos a asignar NA si los valores de fertilidad son mayores de 80.

for (i in 1:nrow(swiss)) {
  
  if (swiss$Fertility[i] > 80) { 
    
    swiss$Fertility[i] <- NA
    
  }
}

Esto es exactamente igual a un if_else() vectorizado

data("swiss")
swiss$Fertility <- if_else(swiss$Fertility > 80, NA, swiss$Fertility)

2.1.2 Evitando bucles

Como ya hemos aprendido con el paquete{microbenchmark} podemos chequear como los bucles suelen ser muy ineficientes (de ahí que debamos evitarlos en la mayoría de ocasiones

library(microbenchmark)
x <- 1:1000
microbenchmark(y <- x^2, 
               for (i in 1:100) { y[i] <- x[i]^2 },
               times = 500)
Warning in microbenchmark(y <- x^2, for (i in 1:100) {: less accurate
nanosecond times to avoid potential integer overflows
Unit: nanoseconds
                                    expr    min       lq      mean median
                                y <- x^2    943   1107.0   1325.53   1189
 for (i in 1:100) {     y[i] <- x[i]^2 } 686012 703416.5 765622.11 713523
     uq     max neval
   1271    7339   500
 732670 2903415   500

2.2 Bucles while

Otra forma de crear un bucle es con la estructura while { }, que nos ejecutará un bucle un número desconocido de veces, hasta que una condición deje de cumplirse (de hecho puede que nunca termine). Por ejemplo, vamos a inializar una variable ciclos <- 1, que incrementaremos en cada paso, y no saldremos del bucle hasta que ciclos > 4.

ciclos <- 1
while(ciclos <= 4) {
  
  print(glue("No todavía, vamos por el ciclo {ciclos}")) 
  ciclos <- ciclos + 1
  
}
No todavía, vamos por el ciclo 1
No todavía, vamos por el ciclo 2
No todavía, vamos por el ciclo 3
No todavía, vamos por el ciclo 4

Un bucle while será siempre como sigue

while(condición) {
  
  código a hacer mientras la condición sea TRUE
  # normalmente aquí se actualiza alguna variable
  
}

¿Qué sucede cuando la condición nunca es FALSE? Pruébalo tu mismo

while (1 > 0) {
  
  print("Presiona ESC para salir del bucle")
  
}

 

Cuidado

Un bucle while { } puede ser bastante «peligroso» sino controlamos bien cómo pararlo.

2.2.1 parada del while

Debido a que puede ser problemático no saber el número predeterminado de veces que un while va a ejecutarse, contamos con dos palabras reservadas para abortar un bucle o forzar su avance:

  • break: permite abortar un bucle incluso si no se ha llegado a su final
for(i in 1:10) {
  if (i == 3) {
    
    break # si i = 3, abortamos bucle
    
  }
  print(i)
}
[1] 1
[1] 2
  • next: fuerza un bucle a avanzar a la siguiente iteración
for(i in 1:5) {
  if (i == 3) {
    
    next # si i = 3, la obvia y continua al siguiente
    
  }
  print(i)
}
[1] 1
[1] 2
[1] 4
[1] 5

2.3 Bucles repeat

Aunque no es tan usado como las opciones anteriores, también contamos con repeat { } que ejecuta un bucle de manera infinita hasta que se indique abortar con un break

count <- 0
repeat { 
  
  count <- count + 1
  if (count >= 100) { break }
  
}
count
[1] 100

2.4 Replicate

Aunque no es formalmente un bucle, otra forma de repetir código un número de veces es hacer uso de replicate(): simplemente permite repetir lo mismo n veces

x <- 1:3
replicate(n = 3, x^2)
     [,1] [,2] [,3]
[1,]    1    1    1
[2,]    4    4    4
[3,]    9    9    9

La función replicate() se suele usar para generar distintas repeticiones de elementos aleatorios. Por ejemplo, imaginemos que queremos generar 3 muestras de distribuciones normales, en la que cada muestra tendrá 7 elementos. Para generar una se usa rnorm(n = 7) (r de resample, norm de normal, y si no se dice nada es media 0 y desv 1).

replicate(n = 3, rnorm(n = 7))
           [,1]       [,2]       [,3]
[1,]  0.3845911  0.1310892  0.1122963
[2,]  0.2172657 -0.6958405 -0.5147204
[3,]  0.1375367  2.1252474 -1.6662932
[4,] -1.3686845  0.4330778 -0.6088050
[5,] -0.2645341  0.6484920  1.4587783
[6,] -1.4055496 -1.7149096  1.2179300
[7,]  0.4528927  0.5835039 -2.6372989

2.5 💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para que se imprima un mensaje por pantalla si y solo si todos los datos de airquality son con mes distinto a enero

library(datasets)
months <- airquality$Month

if (months == 2) {
  print("No hay datos de enero")
}
Code
library(datasets)
months <- airquality$Month

if (all(months != 1)) {
  print("No hay datos de enero")
}

📝 Modifica el código inferior para guardar en una variable llamada temp_alta un TRUE si alguno de los registros tiene una temperatura superior a 90 grados Farenheit y FALSE en cualquier otro caso

temp <- airquality$Temp

if (temp == 100) {
  print("Algunos de los registros tienen temperaturas superiores a 90 grados Farenheit")
}
Code
# Option 1
temp <- airquality$Temp
temp_alta <- FALSE
if (any(temp > 90)) {
   temp_alta <- TRUE
}

# Option 2
temp_alta <- any(airquality$Temp > 90)

📝 Modifica el código inferior para diseñar un bucle for de 5 iteraciones que solo recorra los primeros 5 impares (y en cada paso del bucle los imprima)

for (i in 1:5) {
  
  print(i)
}
Code
for (i in c(1, 3, 5, 7, 9)) {
  
  print(i)
}

📝 Modifica el código inferior para diseñar un bucle while que empiece con un contador count <- 1 y pare cuando llegue a 6

count <- 1
while (count == 2) {
  
  print(count)
}
Code
count <- 1
while (count < 6) {
  
  print(count)
  count <- count + 1
  
}

3 🐣 Caso práctico I: bucles y estructuras condicionales

Para practicar estructuras de control vamos a realizar un ejercicio de simulación

3.1 Pregunta 1

Define una varible llamada importe que empiece en 100. Diseña un bucle de 20 iteraciones donde en cada iteración, importe se reduzca a la mitad de su valor. Piensa que tipo de estructura de bucle deberías usar. El valor final de importe deberia ser 0.000095367 (aprox)

Code
# Usamos un for ya que sabemos el número de iteraciones 
# de manera prefijada (y no depende de nada)

# definimos inicialmente importe en 100
importe <- 100 

# para el bucle usamos por ejemplo i como índice, que va de 1 a 20
for (i in 1:20) {
  
  # el código fíjate que es el mismo y no depende de i
  importe <- importe/2
}
importe

3.2 Pregunta 2

Diseña una estructura de bucle de manera que encuentres la iteración en la que importe es menor que 0.001 por primera vez. Una vez encontrado guárdalo en iter y para el bucle.

Code
# dos formas de hacerlo: for y while

# con for
importe <- 100 

# ya sabemos que en 20 es menor que 0.001 así que podemos poner
# dicha cantidad como tope sabiendo que no llegará
for (i in 1:20) {
  
  # si todavía no es menor, seguimos dividiendo
  if (importe >= 0.001) {
    
    importe <- importe/2
    
  } else {
    
    # si ya es menor, guardamos la iteración (piensa por qué i - 1)
    iter <- i - 1 
    
    # y paramos
    break
  }
  
}

# con while
importe <- 100 

iter <- 0 # debemos inicializar las iteraciones

# no sabemos cuantas iteraciones, solo que debe parar cuando
# importe esté por debajo de dicha cantidad
while (importe >= 0.001) {
  
  importe <- importe/2
    
  # estructura clásica de while: si se corre iteración
  # actualizamos un valor (en este caso que cuente una iteración)
  iter <- iter + 1
}

iter

3.3 Pregunta 3

En R tenemos la función %%: si ponemos a %% b nos devuelve el resto que daría la división \(a/b\). Por ejemplo, 4 %% 2 da 0 ya que 4 es un número par (es decir, su resto al dividir entre 2 es 0). Si ponemos 13 %% 5 nos devuelve 3, ya que el resto de dividir 13 entre 5 es 3.

# Resto al dividir entre 2
3 %% 2
[1] 1
4 %% 2
[1] 0
5 %% 2
[1] 1
6 %% 2
[1] 0
# Resto al dividir entre 3
9 %% 3
[1] 0
10 %% 3
[1] 1
11 %% 3
[1] 2
12 %% 3
[1] 0

Empezando en un importe inicial importe_inicial de 100 (euros), diseña un bucle que te sume 3€ más la iteración por la que estés si el importe actual es par y te reste 5€ menos la iteración por la que estés si es impar, SALVO que el importe ya esté igual o por debajo de 0 (en ese caso no debe sumar ni retar). Ejemplo: si importe tiene 50 euros y estás en la iteración 13, sumará 3 + 13 (66 en total); si importe tiene 51 euros y estás en la iteración 13, restará 5 + 13 (33 en total); si importe tiene -2 euros y estás en la iteración 13, sumará 3 + 13 (14 en total); si importe tiene -1 euros y estás en la iteración 13, no hará nada. Guarda los importes resultantes de cada iteración (máximo de 150 iteraciones). Empieza a partir de la iteración 2

Code
importe_inicial <- 100
importe <- c(importe_inicial, rep(NA, 149))
for (i in 2:150) {
  
  if (importe[i - 1] %% 2 == 0) {
    
    importe[i] <- importe[i - 1] + 3 + i
    
  } else if (importe[i - 1] > 0) {
    
    importe[i] <- importe[i - 1] - (5 + i)
    
  } else {
    
    importe[i] <- importe[i - 1]
    
  }
}

¿Qué ha pasado?

3.4 Pregunta 4

En R la función sample(x = ..., size = ...) va sernos muy útil: de una colección de elementos x, selecciona un número size al azar de ellos.

Por ejemplo, si queremos simular 3 veces el lanzamiento de un dado tenemos 6 elementos posibles (x = 1:6) y lo seleccionamos 3 veces (size = 3)

sample(x = 1:6, size = 3)
[1] 2 1 5

Al ser aleatorio, cada vez que lo ejecutas saldrá algo distinto

sample(x = 1:6, size = 3)
[1] 2 6 5

¿Y si queremos tirar 10 veces?

sample(x = 1:6, size = 10)
Error in sample.int(length(x), size, replace, prob): cannot take a sample larger than the population when 'replace = FALSE'

Al tener solo 6 elementos posibles y elegir 10, no puede, así que le tenemos que indicar que queremos un sample (muestreo) con reemplazamiento (como sucede en el dado, cada cara puede repetirse al volver a tirarlo)

sample(x = 1:6, size = 10, replace = TRUE)
 [1] 4 1 2 1 5 5 6 2 1 6

3.4.1 Pregunta 4.1

Con lo anterior, imagina que estás en un concurso de televisión donde te dan a elegir 3 puertas: en una hay un premio millonarios y en las otras 2 una galleta oreo. Diseña el estudio de simulación con bucles for para aproximar la probabilidad de que te toque el premio (obviamente tiene que darte aprox 0.3333). Realiza el experimento para 10, 50 intentos, 100 intentos, 500 intentos, 1000 intentos, 10 000 intentos, 20 000 intentos y 30 000 intentos (pista: necesitas un bucle dentro de otro). ¿Qué observas?

Code
library(dplyr)

# Definimos las posibilidades
puertas <- c(1, 2, 3)

# Definimos los intentos
intentos <- c(10, 50, 100, 500, 1000, 10000, 20000, 30000)

# Para cada escenario de intentos, definimos las veces que hemos ganado
# (al inicio empieza en 0 claro)
exitos <- rep(0, length(intentos))

# primer bucle: cantidad de intentos permitidos
for (i in 1:length(intentos)) {
  
  # segundo bucle: para cada intento, simulaciones una eleccion de 
  # puerta y un premio
  for (j in 1:intentos[i]) {
    
    # premio: de 3 puertas, solo está en una
    premio <- sample(x = puertas, size = 1)
    
    # puerta que seleccionas como concursante: de 3 puertas, te quedas con una
    eleccion <- sample(x = puertas, size = 1)
    
    # si la puerta seleccionada coincide con la que tiene el premio
    # sumas un éxito, sino te quedas como estás
    exitos[i] <- if_else(eleccion == premio, exitos[i] + 1, exitos[i])
    
  }
  # Tras jugar, lo dividimos entre el número de veces que has jugado
  # para tener una proporción
  exitos[i] <- exitos[i] / intentos[i]
}
exitos

3.4.2 Pregunta 4.2

¿Y si en cada ronda, te abriesen una de las puertas no premiadas que no has elegido, cambiarías de puerta o te mantendrías? Simula ambos casos y descubre cuál es la estrategia correcta (este problema se conoce como problema de Monty Hall y aparece incluso en películas como 21 Black Jack)

Code
puertas <- c(1, 2, 3)
intentos <- c(10, 50, 100, 500, 1000, 10000, 20000, 30000)
exitos_mantengo <- exitos_cambio <- rep(0, length(intentos))

for (i in 1:length(intentos)) {
  for (j in 1:intentos[i]) {
    
    # puerta que seleccionas como concursante: de 3 puertas, te quedas con una
    eleccion_inicial <- sample(x = puertas, size = 1)
    
    # premio: de 3 puertas, solo está en una
    premio <- sample(x = puertas, size = 1)
    
    # De la no elegida, el presentador te abre una no premiada
    puerta_abierta <-
      puertas[puertas != eleccion_inicial & puertas != premio]
    
    # si solo hay una opción (es decir que tu eleccion inicial
    # y el premio son puertas distintas) no haces nada
    
    # Si hubiese 2 opciones (si tu eleccion y el premio coinciden)
    # te abrirá una al azar
    if (length(puerta_abierta) > 1) {
      
      puerta_abierta <- sample(x = puerta_abierta, size = 1)
    }
      
    
    # si mantienes es como antes
    exitos_mantengo[i] <-
      if_else(eleccion_inicial == premio, exitos_mantengo[i] + 1, exitos_mantengo[i])
    
    # si cambias es a una puerta distinta de la inicial y de la abierta, la que quede
    cambio <- puertas[puertas != eleccion_inicial & puertas != puerta_abierta]
    exitos_cambio[i] <-
      if_else(cambio == premio, exitos_cambio[i] + 1, exitos_cambio[i])
    
  }
  # Tras jugar, lo dividimos entre el número de veces que has jugado
  # para tener una proporción
  exitos_mantengo[i] <- exitos_mantengo[i] / intentos[i]
  exitos_cambio[i] <- exitos_cambio[i] / intentos[i]
}
exitos_mantengo
exitos_cambio

¿Qué sucede?

4 Funciones en R

Cuando programamos no solo podemos usar funciones predeterminadas que vienen ya cargadas en paquetes, además podemos crear nuestras propias funciones para automatizar tareas o poder incluso exportarlo para que otras personas las usen.

4.1 Estructura básica

¿Cómo crear nuestra propia función? Veamos su esquema básico:

  • Nombre: por ejemplo name_fun (sin espacios ni caracteres extraños). Al nombre le asignamos la palabra reservada function().

  • Definir argumentos de entrada (dentro de function()).

  • Cuerpo de la función dentro de { }.

  • Finalizamos la función con los argumentos de salida con return().

name_fun <- function(arg1, arg2, ...) {
  
  código a ejecutar
  
  return(var_salida)
  
}

Si te fijas en el código anterior, arg1, arg2, ... serán los argumentos de entrada, los argumentos que toma la función para ejecutar el código que tiene dentro.

Además hay otra parte que normalemnte siempre aparecerá en la función (no es necesario pero suele ser habitual que esté): return(var_salida), de manera que dentro de return() se introducirán los argumentos de salida, lo que queremos que devuelva la función

Importante

Todas las variables que definamos dentro de la función son variables LOCALES: solo existirán dentro de la función salvo que especifiquemos lo contrario.

4.1.1 Ejemplo: cálcular área

Veamos un ejemplo muy simple de función para calcular el área de un rectángulo.

Dado que el área de un rectángulo se calcula como el producto de sus lados, necesitaremos precisamente eso, sus lados: esos serán los argumentos de entrada y el valor a devolver será justo su área (\(lado_1 * lado_2\)).

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
  area <- lado_1 * lado_2
  return(area)
  
}

También podemos hacer una definición directa de las variables sin almacenar por el camino.

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2) {
  
  return(lado_1 * lado_2)
  
}

¿Cómo aplicar la función? Simplemente usando el nombre que le hemos dado y dentro de los paréntesis, separados por comas, los argumentos de entrada.

calcular_area(5, 3) # área de un rectángulo 5 x 3 
[1] 15
calcular_area(1, 5) # área de un rectángulo 1 x 5
[1] 5
Tip

Aunque no sea necesario, es recomendable hacer explícita la llamada de los argumentos, especificando en el código qué valor es para cada argumento para que no dependa de su orden, haciendo el código más legible

calcular_area(lado_1 = 5, lado_2 = 3) # área de un rectángulo 5 x 3 
[1] 15
calcular_area(lado_2 = 3, lado_1 = 5) # área de un rectángulo 5 x 3 
[1] 15

4.2 Argumentos por defecto

Imagina ahora que nos damos cuenta que el 90% de las veces usamos dicha función para calcular por defecto el área de un cuadrado (es decir, solo necesitamos un lado). Para ello, podemos definir argumentos por defecto en la función: tomarán dicho valor salvo que le asignemos otro.

¿Por qué no asignar lado_2 = lado_1 por defecto, para ahorrar líneas de código y tiempo?

calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado que devolvemos
  return(area)
  
}

Ahora por defecto el segundo lado será igual al primero (si se lo añadimos usará ambos).

calcular_area(lado_1 = 5) # cuadrado
[1] 25
calcular_area(lado_1 = 5, lado_2 = 7) # rectángulo
[1] 35

4.3 Salida múltiple

Compliquemos un poco la función y añadamos en la salida los valores de cada lado, etiquetados como lado_1 y lado_2, empaquetando la salida en una vector.

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(c("area" = area, "lado_1" = lado_1, "lado_2" = lado_2))
  
}

Podemos complicar un poco más la salida añadiendo una cuarta variable que nos diga, en función de los argumentos, si rectángulo o cuadrado, teniendo que añadir en la salida una variable que de tipo caracter (o lógica).

# Definición del nombre de función y argumentos de entrada
library(dplyr)
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(c("area" = area, "lado_1" = lado_1, "lado_2" = lado_2,
           "tipo" = if_else(lado_1 == lado_2, "cuadrado", "rectángulo")))
  
}
calcular_area(5, 3)
        area       lado_1       lado_2         tipo 
        "15"          "5"          "3" "rectángulo" 

Pero fíjate que tenemos un problema: al intentar juntar números y texto, lo convierte todo a números. Podríamos guardarlo todo en un tibble() como hemos aprendido o en un objeto conocido en R como listas

4.4 Breve introducción a listas

Veamos un pequeño resumen de los datos que ya conocemos:

  • vectores: colección de elementos de igual tipo. Pueden ser números, caracteres o valores lógicos, entre otros.

  • matrices: colección BIDIMENSIONAL de elementos de igual tipo e igual longitud.

  • data.frame / tibble: colección BIDIMENSIONAL de elementos de igual longitud pero de cualquier tipo.

 

Las listas serán colecciones de variables de diferente tipo y diferente longitud, con estructuras totalmente heterógeneas (incluso una lista puede tener dentro a su vez otra lista).

 

Vamos a crear nuestra primera lista con list() con tres elementos: el nombre de nuestros padres/madres, nuestro lugar de nacimiento y edades de nuestros hermanos.

var_1 <- c("Paloma", "Gregorio")
var_2 <- "Madrid"
var_3 <- c(25, 30, 26)

lista <- list("progenitores" = var_1, "lugar_nac" = var_2, "edad_hermanos" = var_3)
lista
$progenitores
[1] "Paloma"   "Gregorio"

$lugar_nac
[1] "Madrid"

$edad_hermanos
[1] 25 30 26

Si observas el objeto que hemos definido como lista, su longitud del es de 3 ya que tenemos guardados tres elementos: un vector de caracteres (de longitud 2), un caracter (vector de longitud 1), y un vector de números (de longitud 3)

length(lista)
[1] 3

En una lista solemos tener guardados elementos de distinto tipo (algo que ya podíamos hacer) pero, además, de longitudes dispares.

dim(lista) # devolverá NULL al no tener dos dimensiones
NULL
class(lista) # de tipo lista
[1] "list"

Si los juntásemos con un tibble(), al tener distinta longitud, obtendríamos un error.

library(tibble)
tibble("progenitores" = var_1, "lugar_nac" = va_2, "edad_hermanos" = var_3)
Error in eval_tidy(xs[[j]], mask): object 'va_2' not found

4.4.1 Operaciones básicas

La más básica es poder acceder por índice a sus elementos, con el operador [[i]], accediendo al elemento i-ésimo de la lista.

lista[[1]]
[1] "Paloma"   "Gregorio"

También podemos acceder por nombre con $nombre_elemento.

lista$progenitores
[1] "Paloma"   "Gregorio"

En contraposición, el corchete simple nos permite acceder a varios elementos a la vez

# Varios elementos
lista[1:2]
$progenitores
[1] "Paloma"   "Gregorio"

$lugar_nac
[1] "Madrid"

4.4.2 Salida múltiple: listas

Así haciendo uso de listas podemos hacer que la función devuelva de manera conjunta múltiples argumentos (sin importar su tipo ni longitud)

# Definición del nombre de función y argumentos de entrada
calcular_area <- function(lado_1, lado_2 = lado_1) {
  
  # Cuerpo de la función
  area <- lado_1 * lado_2
  
  # Resultado
  return(list("area" = area, "lado_1" = lado_1, "lado_2" = lado_2,
           "tipo" = if_else(lado_1 == lado_2, "cuadrado", "rectángulo")))
  
}
calcular_area(5, 3)
$area
[1] 15

$lado_1
[1] 5

$lado_2
[1] 3

$tipo
[1] "rectángulo"

Antes nos daba igual el orden de los argumentos pero ahora el orden de los argumentos de entrada importa, ya que en la salida incluimos lado_1 y lado_2.

Recomendación

Como se comentaba, altamente recomendable hacer la llamada a la función indicando explícitamente los argumentos para mejorar legibilidad e interpretabilidad.

# Equivalente a calcular_area(5, 3)
calcular_area(lado_1 = 5, lado_2 = 3)
$area
[1] 15

$lado_1
[1] 5

$lado_2
[1] 3

$tipo
[1] "rectángulo"

4.5 Variables locales vs globales

Un aspecto importante sobre el que reflexionar con las funciones: ¿qué sucede si nombramos a una variable dentro de una función a la que se nos ha olvidado asignar un valor dentro de la misma?

 

Debemos ser cautos al usar funciones en R, ya que debido a la «regla lexicográfica», si una variable no se define dentro de la función, R buscará dicha variable en el entorno de variables.

x <- 1
funcion_ejemplo <- function() {
    
  print(x) # No devuelve nada, solo realiza la acción 
}
funcion_ejemplo()
[1] 1

Si una variable ya está definida fuera de la función (entorno global), y además es usada dentro de cambiando su valor, el valor solo cambia dentro pero no en el entorno global.

x <- 1
funcion_ejemplo <- function() {
    
  x <- 2
  print(x) # lo que vale dentro
}
# lo que vale dentro
funcion_ejemplo() #<<
[1] 2
# lo que vale fuera
print(x) #<<
[1] 1

Si queremos que además de cambiar localmente lo haga globalmente deberemos usar la doble asignación (<<-).

x <- 1
y <- 2
funcion_ejemplo <- function() {
  
  # no cambia globalmente, solo localmente
  x <- 3 
  # cambia globalmente
  y <<- 0 #<<
  
  print(x)
  print(y)
}

funcion_ejemplo() # lo que vale dentro
[1] 3
[1] 0
x # lo que vale fuera
[1] 1
y # lo que vale fuera
[1] 0

4.6 💻 Tu turno

Intenta realizar los siguientes ejercicios sin mirar las soluciones

📝 Modifica el código inferior para definir una función llamada funcion_suma, de forma que dados dos elementos, devuelve su suma.

nombre <- function(x, y) {
  suma <- # código a ejecutar
  return()
}
# Aplicamos la función
suma(3, 7)
Code
funcion_suma <- function(x, y) {
  suma <- x + y
  return(suma)
}
funcion_suma(3, 7)

📝 Modifica el código inferior para definir una función llamada funcion_producto, de forma que dados dos elementos, devuelve su producto, pero que por defecto calcule el cuadrado

nombre <- function(x, y) {
  producto <- # código de la multiplicación
  return()
}
producto(3)
producto(3, -7)
Code
funcion_producto <- function(x, y = x) {
  producto <- x * y
  return(producto)
}
funcion_producto(3)
funcion_producto(3, -7)

📝 Define una función llamada igualdad_nombres que, dados dos nombres, nos diga si son iguales o no. Hazlo considerando importantes las mayúsculas, y sin que importen las mayúsculas. Usa el paquete {stringr}.

Code
# Distinguiendo mayúsculas
igualdad_nombres <- function(persona_1, persona_2) {
  return(persona_1 == persona_2)
}
igualdad_nombres("Javi", "javi")
igualdad_nombres("Javi", "Lucía")

# Sin importar mayúsculas
igualdad_nombres <- function(persona_1, persona_2) {
  return(toupper(persona_1) == toupper(persona_2))
}
igualdad_nombres("Javi", "javi")
igualdad_nombres("Javi", "Lucía")

📝 Crea una función llamada calculo_IMC que, dados dos argumentos (peso y estatura en metros) y un nombre, devuelva una lista con el IMC (\(peso/(estatura_m^2)\)) y el nombre.

Code
calculo_IMC <- function(nombre, peso, estatura) {
  
  return(list("nombre" = nombre, "IMC" = peso/(estatura^2)))
}

📝 Repite el ejercicio anterior pero con otro argumento opcional que se llame unidades (por defecto, unidades = "metros"). Desarrolla la función de forma que haga lo correcto si unidades = "metros" y si unidades = "centímetros".

Code
calculo_IMC <- function(nombre, peso, estatura, unidades = "metros") {
  
  return(list("nombre" = nombre,
              "IMC" = peso/(if_else(unidades == "metros", estatura, estatura/100)^2)))
}

📝 Crea un tibble ficticio de 7 personas, con tres variables (inventa nombre, y simula peso, estatura en centímetros), y aplica la función definida de forma que obtengamos una cuarta columna con su IMC.

Code
datos <-
  tibble("nombres" = c("javi", "sandra", "laura", "ana", "carlos", "leo", NA),
         "peso" = rnorm(n = 7, mean = 70, sd = 1),
         "estatura" = rnorm(n = 7, mean = 168, sd = 5))

# IMPORTANTE: if_else trabaja de manera vectorial, elemento a elemento
# esto significa que la longitud de la condición (en este caso unidades == "metros"
# solo es uno, no un vector lógico) debe ser igual que lo que le decimos que haga 
# cuando es TRUE y cuando es FALSE (en este caso la condicion tiene longitud 1
# pero al aplicarla luego - dentro de un mutate a toda una variable de peso y estatura
# lo que devuelve es un vector de tamaño 7, por eso hay que usar ifelse (sin _))
calculo_IMC <- function(nombre, peso, estatura, unidades = "metros") {
  
  return(list("nombre" = nombre,
              "IMC" = peso/(ifelse(unidades == "metros", estatura, estatura/100)^2)))
}
datos |> 
  mutate(IMC = calculo_IMC(nombres, peso, estatura, unidades = "centímetros")$IMC)

📝 Crea una función llamada atajo que tenga dos argumentos numéricos x e y. Si ambos son iguales, debes devolver "iguales" y hacer que la función acaba automáticamente (piensa cuándo una función sale). OJO: x e y podrían ser vectores. Si son distintos (de igual de longitud) calcula la proporción de elementos diferentes. Si son distintos (por ser distinta longitud), devuelve los elementos que no sean comunes.

Code
atajo <- function(x, y) {
  
  if (all(x == y) & length(x) == length(y)) { return("iguales") }
  else {
   
    if (length(x) == length(y)) {
      
      n_diff <- sum(x != y) / length(x)
      return(n_diff)
      
    } else {
      
      diff_elem <- unique(c(setdiff(x, y), setdiff(y, x)))
      return(diff_elem)
    }
    
  }
}

5 🐣 Caso práctico II: conversor de temperaturas

Para practicar con el uso de funciones vamos a crear un conversor de temperaturas. Empecemos por lo sencillo. Intenta conceptuar la idea antes en un papel.

5.1 Pregunta 1

Define una función llamada celsius_to_kelvin que, dada una temperatura en Celsius (por ejemplo, temp como argumento) la convierta a Kelvin según la fórmula de conversión inferior. Tras definir la función aplícala a un vector de temperaturas.

\[K = °C + 273.15\]

Code
# definimos nombre de la función y argumentos
celsius_to_kelvin <- function(temp) {
  
  # convertimos
  kelvin <- temp + 273.15
  
  # devolvemos
  return(kelvin)
  
}

x <- c(-15, -3, 0, 15, 27.5)
celsius_to_kelvin(x)

5.2 Pregunta 2

Crea la función contraria kelvin_to_celsius y aplícala a otro vector de temperaturas. Tendrás que asegurarte que la temperatura en Kelvin no toma valores negativos (ya que es una escala absoluta). En caso de que no se cumpla, devolver NA.

Code
# definimos nombre de la función y argumentos
kelvin_to_celsius <- function(temp) {
  
  # si es negativa en Kelvin, paramos y devolvemos ausente
  # en caso contrario, convertimos
  celsius <- if_else(temp < 0, NA, temp - 273.15)
  
  # Piensa porque no lo hemos hecho con un if (...) else (...)

  # devolvemos
  return(celsius)
  
}

y <- c(0, 250, 300, 350)
kelvin_to_celsius(y)

5.3 Pregunta 3

Crea una función conjunta conversor_temp que tenga dos argumentos: temperatura y un argumento de texto que nos diga si es kelvin o celsius (y que por defecta la temperatura de entrada sea Celsius). La función debe usar ese string para decidir en que dirección convierte (controla que en el argumento de texto no haya una opción distinta a las dos permitidas; en caso contrario, devolver error usando el comando stop(“mensaje de error…”)). Aplícala a los vectores anteriores y chequea que da lo mismo.

Code
# definimos nombre de la función y argumentos
# por defecto, unidades en celsius
conversor_temp <- function(temp, unidades = "celsius") {
  
  # chequeamos que unidades es correcto
  # dentro de los valroes permitidos
  if (unidades %in% c("celsius", "kelvin")) {
    
    if (unidades == "celsius") {
      
      temp_out <- celsius_to_kelvin(temp) 
      
    } else {
      
      temp_out <- kelvin_to_celsius(temp)
      
    }
    
  } else {
    
    # en caso contrario paramos la función con un mensaje de error
    stop("Error: solo se permiten como unidades 'celsius' o 'kelvin'")
  }
  
  # devolvemos
  return(temp_out)
  
}

# Fíjate que no hemos usado `if_else()` porque el número de elementos
# a evaluar en la condición debe ser igual al número de elementos que # devuelve, al hacerlo de manera vectorial.
conversor_temp(x)
conversor_temp(y, unidades = "kelvin")

5.4 Pregunta 4

Repite la función anterior pero sin que importe si unidades está en mayúscula o minúscula

conversor_temp(y, unidades = "Kelvin")
Error in conversor_temp(y, unidades = "Kelvin"): could not find function "conversor_temp"
Code
# definimos nombre de la función y argumentos
# por defecto, unidades en celsius
library(stringr)
conversor_temp <- function(temp, unidades = "celsius") {
  
  # usamos str_to_lower para pasarlo todo a minúscula
  if (str_to_lower(unidades) %in% c("celsius", "kelvin")) {
    
    if (unidades == "celsius") {
      
      temp_out <- celsius_to_kelvin(temp) 
      
    } else {
      
      temp_out <- kelvin_to_celsius(temp)
      
    }
    
  } else {
    
    # en caso contrario paramos la función con un mensaje de error
    stop("Error: solo se permiten como unidades 'celsius' o 'kelvin'")
  }
  
  # devolvemos
  return(temp_out)
  
}

conversor_temp(y, unidades = "Kelvin")

5.5 Pregunta 5

Repite todo el proceso anterior creando conversor_temp2 pero para convertir entre Celsius y Fahrenheit siguiendo la fórmula inferior

\[ºC = (ºF − 32) * \frac{5}{9}, \quad ºF = 32 + ºC * \frac{9}{5}\]

Code
# definimos las funciones de celsius a fahr y viceversa
celsius_to_fahr <- function(temp) {
  
  # convertimos
  fahr <- 32 + temp * (9/5)
  
  # devolvemos
  return(fahr)
  
}
celsius_to_fahr(x)

fahr_to_celsius <- function(temp) {
  
  # convertimos
  celsius <- (temp - 32) * (5/9)
  
  # devolvemos
  return(celsius)
  
}

z <- c(40, 60, 80, 100)
fahr_to_celsius(z)

# definimos nombre de la función y argumentos
# por defecto, unidades en celsius
conversor_temp2 <- function(temp, unidades = "celsius") {
  
  # usamos str_to_lower para pasarlo todo a minúscula
  if (str_to_lower(unidades) %in% c("celsius", "fahr")) {
    
    if (unidades == "celsius") {
      
      temp_out <- celsius_to_fahr(temp) 
      
    } else {
      
      temp_out <- fahr_to_celsius(temp)
      
    }
    
  } else {
    
    # en caso contrario paramos la función con un mensaje de error
    stop("Error: solo se permiten como unidades 'celsius' o 'fahr'")
  }
  
  # devolvemos
  return(temp_out)
  
}

conversor_temp2(x)
conversor_temp2(z, unidades = "fahr")

5.6 Pregunta 6

Para acabar, crea la superfunción conversor_temp_total que permita como argumento de entrada una temperatura en alguna de las 3 unidades, un texto que indique en qué unidades viene y otro que indique en qué unidades se quiere sacar. Por defecto que convierta de celsius a kelvin.

Code
conversor_temp_total <-
  function(temp, unidades_entrada = "celsius",
           unidades_salida = "kelvin") {
  
  if (str_to_lower(unidades_entrada) %in% c("celsius", "fahr", "kelvin") &
      str_to_lower(unidades_salida) %in% c("celsius", "fahr", "kelvin")) {
    
    # Si las de salida son iguales que las de entrada, no hacemos nada
    if (unidades_entrada == unidades_salida) {
      
      return(temp)
      
    }
    
    else if (unidades_entrada == "celsius") {
      
      if (unidades_salida == "kelvin") {
        
        temp_out <- celsius_to_kelvin(temp) 
        
      # si no es kelvin (ni celsius porque entrada es distinto a salida)
      # solo queda una funcion (que sea fahr)
      } else { 
        
        temp_out <- celsius_to_fahr(temp) 
      }
      
    } else if (unidades_entrada == "kelvin") {
      
      if (unidades_salida == "celsius") {
        
        temp_out <- kelvin_to_celsius(temp) 
    
      } else { 
        
        temp_out <- celsius_to_fahr(kelvin_to_celsius(temp))
      }
      
    } else {
      
      if (unidades_salida == "celsius") {
        
        temp_out <- fahr_to_celsius(temp) 
    
      } else { 
        
        temp_out <- celsius_to_kelvin(fahr_to_celsius(temp))
      }
      
    }
    
  } else {
    
    # en caso contrario paramos la función con un mensaje de error
    stop("Error: solo se permiten como unidades de entrada/salida 'celsius', 'kelvin' o 'fahr'")
  }
  
  # devolvemos
  return(temp_out)
  
}

conversor_temp_total(x, unidades_entrada = "celsius",
                     unidades_salida = "celsius")
conversor_temp_total(y, unidades_entrada = "kelvin",
                     unidades_salida = "kelvin")
conversor_temp_total(z, unidades_entrada = "fahr",
                     unidades_salida = "fahr")

conversor_temp_total(x, unidades_entrada = "celsius",
                     unidades_salida = "kelvin")
conversor_temp_total(y, unidades_entrada = "kelvin",
                     unidades_salida = "celsius")
conversor_temp_total(x, unidades_entrada = "celsius",
                     unidades_salida = "fahr")
conversor_temp_total(z, unidades_entrada = "fahr",
                     unidades_salida = "celsius")

conversor_temp_total(z, unidades_entrada = "fahr",
                     unidades_salida = "celsius")
conversor_temp_total(conversor_temp_total(z, unidades_entrada = "fahr",
                                          unidades_salida = "kelvin"),
                     unidades_entrad = "kelvin",
                     unidades_salida = "celsius")