Capítulo 2 Funciones

2.1 Introducción al mundo de las funciones

En el capítulo anterior, exploramos los diferentes tipos de objetos que podemos usar para almacenar y organizar información en R. Aprendimos a crear variables, vectores, listas, matrices y arrays, y vimos cómo acceder a sus elementos y realizar operaciones con ellos.

Ahora, en este capítulo, daremos un paso más allá y nos adentraremos en el mundo de las funciones. Las funciones son uno de los pilares fundamentales de la programación en R, y nos permiten realizar tareas más complejas y automatizar nuestro trabajo.

2.1.1 ¿Qué son las funciones?

Imagina una máquina de hacer café. Tú le proporcionas los ingredientes (agua, café, azúcar), y la máquina realiza una serie de pasos para producir una taza de café. De manera similar, una función en R es un conjunto de instrucciones que recibe unos datos de entrada (los argumentos) y realiza una serie de operaciones para producir un resultado (el valor de retorno).

Las funciones nos permiten encapsular un conjunto de instrucciones en un solo bloque de código, lo que facilita la reutilización y la organización de nuestro código. En lugar de escribir las mismas instrucciones una y otra vez, podemos crear una función que las realice por nosotros.

2.1.2 ¿Por qué usar funciones?

Las funciones ofrecen varias ventajas:

  • Reutilización: Podemos usar la misma función en diferentes partes de nuestro código o en diferentes proyectos.
  • Organización: Las funciones nos ayudan a organizar nuestro código en bloques lógicos, lo que facilita su lectura y comprensión.
  • Legibilidad: Al usar funciones, nuestro código se vuelve más conciso y fácil de entender.
  • Abstracción: Las funciones nos permiten abstraer la complejidad de una tarea, lo que nos permite concentrarnos en la lógica del problema que queremos resolver.

2.1.3 Primeras funciones: explorando funciones básicas de R

R ya incluye una gran cantidad de funciones predefinidas que podemos utilizar para realizar diferentes tareas. Veamos algunos ejemplos:

  • sum(): Calcula la suma de los elementos de un vector.

    numeros <- c(1, 2, 3, 4, 5)
    sum(numeros)  # Output: 15
    #> [1] 15
  • mean(): Calcula la media aritmética de los elementos de un vector.

    temperaturas <- c(25, 28, 26, 29, 27)
    mean(temperaturas)  # Output: 27
    #> [1] 27
  • round(): Redondea un número a un número específico de decimales.

    pi  # Output: 3.141593
    #> [1] 3.141593
    round(pi, 2)  # Output: 3.14
    #> [1] 3.14
  • length(): Devuelve la longitud de un vector (el número de elementos que contiene).

    ciudades <- c("Nueva York", "Los Ángeles", "Chicago")
    length(ciudades)  # Output: 3
    #> [1] 3

Estas son solo algunas de las muchas funciones predefinidas que R ofrece. A medida que avancemos en el libro, iremos explorando más funciones y aprendiendo cómo utilizarlas para realizar análisis de datos más complejos.

2.2 Anatomía de una función

En la sección anterior, vimos qué son las funciones y por qué son tan útiles en la programación. Ahora, vamos a adentrarnos en la estructura de una función, para que puedas crear tus propias funciones y automatizar tareas en tu análisis de datos.

2.2.1 Argumentos: los ingredientes de la función

Para hacer una taza de café, necesitas ingredientes: agua, café, y quizás azúcar o leche. De manera similar, las funciones en R necesitan argumentos para realizar su trabajo. Los argumentos son los datos de entrada que la función utiliza para realizar sus operaciones.

Por ejemplo, la función sum() necesita un vector de números como argumento para calcular la suma de sus elementos.

numeros <- c(1, 2, 3, 4, 5)
sum(numeros)  # Output: 15
#> [1] 15

Los argumentos de una función se especifican entre paréntesis después del nombre de la función. Si una función requiere varios argumentos, se separan por comas.

Por ejemplo, imagina que queremos crear una función para calcular el costo total de un viaje en avión. Esta función podría necesitar los siguientes argumentos:

  • precio_boleto: El precio de un boleto de avión.
  • num_personas: El número de personas que viajan.
  • descuento: Un descuento opcional en el precio del boleto (por ejemplo, por ser estudiante o adulto mayor).

La función podría llamarse calcular_costo_viaje y se usaría de la siguiente manera:

calcular_costo_viaje(precio_boleto = 300, num_personas = 2, descuento = 0.1)

En este caso, estamos pasando tres argumentos a la función: precio_boleto con valor 300, num_personas con valor 2, y descuento con valor 0.1 (que representa un 10% de descuento).

2.2.2 Cuerpo: las instrucciones de la función

El cuerpo de una función es el conjunto de instrucciones que se ejecutan cuando se llama a la función. Estas instrucciones pueden ser cualquier código válido en R: asignaciones de variables, operaciones matemáticas, condicionales, bucles, llamadas a otras funciones, etc.

El cuerpo de una función se define entre llaves {}.

Por ejemplo, el cuerpo de la función calcular_costo_viaje podría ser:

calcular_costo_viaje <- function(precio_boleto, num_personas, descuento = 0) {
  costo_total <- precio_boleto * num_personas * (1 - descuento)
  return(costo_total)
}

En este cuerpo, primero se calcula el costo total del viaje multiplicando el precio del boleto por el número de personas y por (1 menos el descuento). Luego, se usa return() para devolver el costo_total.

Observa que en la definición de la función, el argumento descuento tiene un valor por defecto de 0. Esto significa que si no especificamos un valor para descuento al llamar a la función, se usará el valor 0.

Por ejemplo, si no especificamos un valor para descuento, la función utiliza el valor por defecto 0, y el costo total es 600:

# Llamar a la función sin especificar el descuento
calcular_costo_viaje(precio_boleto = 300, num_personas = 2)
#> [1] 600

Si queremos aplicar un descuento, podemos especificarlo al llamar a la función:

calcular_costo_viaje(precio_boleto = 300, num_personas = 2, descuento = 0.1)
#> [1] 540

En este caso, el costo total es 540, ya que se aplica un descuento del 10%.

2.2.3 Valor de retorno: el resultado de la función

El valor de retorno es el resultado que produce la función después de ejecutar sus instrucciones. Puede ser un valor simple (un número, un texto, un valor lógico) o un objeto más complejo (un vector, una lista, un data frame).

En R, el valor de retorno se especifica con la función return(). Si no se usa return(), la función devolverá el resultado de la última expresión evaluada en el cuerpo.

En el ejemplo de calcular_costo_viaje, el valor de retorno es el costo_total del viaje, que es un número.

2.2.4 Ejemplos: creando funciones simples paso a paso

Veamos un ejemplo de cómo crear una función simple que convierta grados Celsius a Fahrenheit:

celsius_a_fahrenheit <- function(celsius) {
  fahrenheit <- (celsius * 9 / 5) + 32
  return(fahrenheit)
}

En este ejemplo:

  • celsius_a_fahrenheit es el nombre de la función.
  • celsius es el argumento de la función (la temperatura en grados Celsius).
  • El cuerpo de la función calcula la temperatura en Fahrenheit usando la fórmula (celsius * 9 / 5) + 32 y la guarda en la variable fahrenheit.
  • La función return() devuelve el valor de la variable fahrenheit.

Ahora podemos usar nuestra función para convertir temperaturas:

celsius_a_fahrenheit(0)   # Output: 32
#> [1] 32
celsius_a_fahrenheit(100)  # Output: 212
#> [1] 212

¡Felicidades! Acabas de crear tu primera función en R. A medida que avancemos en el capítulo, aprenderás a crear funciones más complejas y a utilizarlas para resolver problemas del mundo real.

2.3 Dominando el uso de funciones

Ya hemos visto cómo crear funciones simples con argumentos básicos, incluyendo la posibilidad de asignar valores por defecto. Ahora, vamos a explorar técnicas aún más avanzadas para dominar el uso de funciones y escribir código más flexible y eficiente.

2.3.1 Funciones con un número variable de argumentos (...): Adaptándose a diferentes situaciones

En ocasiones, no sabemos de antemano cuántos argumentos recibirá una función. Para estos casos, R nos ofrece la posibilidad de definir funciones con un número variable de argumentos usando los tres puntos (...).

Por ejemplo, la función sum() puede recibir cualquier número de argumentos:

sum(1, 2, 3) 
#> [1] 6
sum(1, 2, 3, 4, 5)  # Output: 15
#> [1] 15

Podemos usar los tres puntos (...) para crear nuestras propias funciones que acepten un número variable de argumentos. Por ejemplo, una función que calcule el promedio de varios números:

calcular_promedio <- function(...) {
  numeros <- c(...)
  promedio <- mean(numeros)
  return(promedio)
}

calcular_promedio(1, 2, 3)
#> [1] 2
calcular_promedio(1, 2, 3, 4, 5)
#> [1] 3

En este ejemplo, los tres puntos (...) capturan todos los argumentos que se pasan a la función y los almacenan en el vector numeros. Luego, la función calcula el promedio de los números en el vector y lo devuelve como resultado.

Es importante destacar que al usar ..., perdemos la capacidad de nombrar los argumentos individualmente. Sin embargo, ganamos flexibilidad al poder pasar un número variable de argumentos a la función.

2.3.2 Ámbito de las variables: variables locales y globales

El ámbito de una variable se refiere a la parte del código donde la variable es accesible. En R, las variables que se definen dentro de una función tienen un ámbito local, lo que significa que solo son accesibles dentro de la función. Las variables que se definen fuera de cualquier función tienen un ámbito global, lo que significa que son accesibles desde cualquier parte del código.

Por ejemplo, en la función calcular_promedio, la variable numeros tiene un ámbito local:

calcular_promedio <- function(...) {
  numeros <- c(...)
  promedio <- mean(numeros)
  return(promedio)
}

Si intentamos acceder a la variable numeros fuera de la función, obtendremos un error:

numeros  # Error: object 'numeros' not found

Esto se debe a que numeros solo existe dentro de la función calcular_promedio. Cuando la función termina de ejecutarse, las variables locales que se definieron dentro de ella dejan de existir.

En cambio, si definimos una variable fuera de cualquier función, será una variable global:

tasa_conversion <- 0.621371  # Tasa de conversión de kilómetros a millas

Podemos acceder a la variable tasa_conversion desde cualquier parte del código, incluso dentro de una función:

kilometros_a_millas <- function(kilometros) {
  millas <- kilometros * tasa_conversion
  return(millas)
}

kilometros_a_millas(100)
#> [1] 62.1371

Es importante tener en cuenta el ámbito de las variables al escribir funciones, para evitar errores y confusiones. Si una variable no está definida en el ámbito actual (local), R buscará en el ámbito global. Si la variable no se encuentra en ningún ámbito, se producirá un error.

Por ejemplo, imagina que queremos calcular el costo total de un viaje, incluyendo el costo del boleto de avión, el alojamiento y otros gastos. Podemos crear una función que reciba estos gastos como argumentos y calcule el costo total:

calcular_costo_viaje <- function(boleto, alojamiento, otros_gastos) {
  costo_total <- boleto + alojamiento + otros_gastos
  return(costo_total)
}

Si llamamos a esta función con los valores de los gastos, obtendremos el costo total:

calcular_costo_viaje(boleto = 300, alojamiento = 500, otros_gastos = 100) 
#> [1] 900

Ahora, imagina que queremos aplicar un impuesto al costo total. Podríamos definir una variable global tasa_impuesto:

tasa_impuesto <- 0.16

Y luego modificar la función para que incluya el impuesto:

calcular_costo_viaje <- function(boleto, alojamiento, otros_gastos) {
  costo_total <- boleto + alojamiento + otros_gastos
  costo_total <- costo_total * (1 + tasa_impuesto)
  return(costo_total)
}

Al llamar a la función de nuevo, el costo total incluirá el impuesto:

calcular_costo_viaje(boleto = 300, alojamiento = 500, otros_gastos = 100)
#> [1] 1044

En este caso, la función calcular_costo_viaje puede acceder a la variable global tasa_impuesto porque no está definida localmente dentro de la función.

Si intentamos usar una variable que no está definida en ningún ámbito, obtendremos un error:

calcular_costo_viaje <- function(boleto, alojamiento, otros_gastos) {
  costo_total <- boleto + alojamiento + otros_gastos + propina
  return(costo_total)
}

calcular_costo_viaje(boleto = 300, alojamiento = 500, otros_gastos = 100)  # Error: object 'propina' not found

En este caso, la variable propina no está definida ni local ni globalmente, por lo que la función no puede acceder a ella.

Es importante entender el concepto de ámbito de las variables para escribir funciones que funcionen correctamente y evitar errores.

2.3.3 Ejemplos: funciones para calcular impuestos, descuentos, etc.

Las funciones son muy útiles para automatizar tareas repetitivas, como calcular impuestos, descuentos o convertir unidades. Veamos algunos ejemplos con diferentes niveles de dificultad:

Calcular el costo de envío de un paquete

calcular_costo_envio <- function(peso, destino) {
  if (destino == "local") {
    costo <- 5 + 0.1 * peso
  } else if (destino == "nacional") {
    costo <- 10 + 0.2 * peso
  } else {  # destino == "internacional"
    costo <- 20 + 0.5 * peso
  }
  return(costo)
}

# Ejemplo de uso
peso_paquete <- 2.5  # Peso en kilogramos
destino <- "nacional"
costo_envio <- calcular_costo_envio(peso_paquete, destino)

costo_envio
#> [1] 10.5

En este ejemplo, la función calcular_costo_envio() calcula el costo de envío de un paquete según su peso y destino. La función utiliza una estructura condicional (if-else if-else) para aplicar diferentes tarifas de envío según el destino.

Calculo del impuesto a la renta con tramos

calcular_impuesto_renta <- function(ingreso) {
  if (ingreso <= 10000) {
    tasa <- 0.10
  } else if (ingreso <= 20000) {
    tasa <- 0.15
  } else {
    tasa <- 0.20
  }
  impuesto <- ingreso * tasa
  return(impuesto)
}

# Ejemplo de uso
ingreso <- 15000
impuesto <- calcular_impuesto_renta(ingreso)

impuesto
#> [1] 2250

En este ejemplo, la función calcular_impuesto_renta() calcula el impuesto a la renta de una persona según su ingreso. La función utiliza una estructura condicional (if-else if-else) para aplicar diferentes tasas de impuesto según el tramo de ingreso.

Calculo del costo de un viaje con múltiples opciones

calcular_costo_viaje <- function(ciudad_origen, ciudad_destino, 
                                 tipo_transporte = "avion", 
                                 num_personas = 1, 
                                 hotel = NULL, 
                                 gastos_diarios = 100, 
                                 duracion_viaje = 7) {
  
  # Calcular costo de transporte
  if (tipo_transporte == "avion") {
    costo_transporte <- 300 * num_personas  # Precio base por persona
  } else if (tipo_transporte == "tren") {
    costo_transporte <- 150 * num_personas  # Precio base por persona
  } else {
    costo_transporte <- 0  # Asumiendo que el transporte es en auto propio
  }
  
  # Calcular costo de alojamiento
  if (!is.null(hotel)) {
    costo_alojamiento <- hotel$precio * duracion_viaje
  } else {
    costo_alojamiento <- 0  # Asumiendo que no se hospeda en un hotel
  }
  
  # Calcular otros gastos
  otros_gastos <- gastos_diarios * num_personas * duracion_viaje
  
  # Calcular costo total
  costo_total <- costo_transporte + costo_alojamiento + otros_gastos
  
  return(costo_total)
}

# Ejemplo de uso
costo_viaje_1 <- calcular_costo_viaje(ciudad_origen = "Lima", 
                                     ciudad_destino = "Nueva York", 
                                     tipo_transporte = "avion", 
                                     num_personas = 2)

costo_viaje_2 <- calcular_costo_viaje(ciudad_origen = "Lima", 
                                     ciudad_destino = "Los Ángeles", 
                                     tipo_transporte = "tren", 
                                     num_personas = 3, 
                                     hotel = list(precio = 150), 
                                     gastos_diarios = 120, 
                                     duracion_viaje = 10)

costo_viaje_1 
#> [1] 2000
costo_viaje_2 
#> [1] 5550

2.4 Funciones de orden superior

En las secciones anteriores, hemos explorado cómo crear y utilizar funciones en R. Ahora, vamos a adentrarnos en un concepto más avanzado: las funciones de orden superior.

¿Qué son las funciones de orden superior?

Las funciones de orden superior son funciones que pueden:

  • Recibir otras funciones como argumentos.
  • Retornar una función como resultado.

Este tipo de funciones nos permite escribir código más flexible y expresivo, y son una herramienta poderosa para el análisis de datos.

2.4.1 lapply() y sapply(): aplicando una función a cada elemento

Imagina que tienes una lista con información sobre varias ciudades de Estados Unidos, y quieres calcular la densidad de población de cada ciudad. Podrías escribir un bucle for para recorrer la lista y calcular la densidad de cada ciudad por separado. Sin embargo, R nos ofrece una forma más eficiente y elegante de hacerlo: la función lapply().

lapply() (que significa “aplicar a una lista”) recibe dos argumentos:

  • Una lista (o un vector).
  • Una función que se aplicará a cada elemento de la lista.

lapply() aplica la función a cada elemento de la lista y devuelve una nueva lista con los resultados.

# Crear una lista con información sobre ciudades
ciudades <- list(
  Nueva_York = list(poblacion = 8.4e6, area = 783.8),
  Los_Angeles = list(poblacion = 3.9e6, area = 1302.0),
  Chicago = list(poblacion = 2.7e6, area = 606.1)
)

# Función para calcular la densidad de población
calcular_densidad <- function(ciudad) {
  densidad <- ciudad$poblacion / ciudad$area
  return(densidad)
}

# Calcular la densidad de población de cada ciudad
densidades <- lapply(ciudades, calcular_densidad)

densidades
#> $Nueva_York
#> [1] 10717.02
#> 
#> $Los_Angeles
#> [1] 2995.392
#> 
#> $Chicago
#> [1] 4454.71

En este ejemplo, lapply() aplica la función calcular_densidad a cada elemento de la lista ciudades y devuelve una nueva lista densidades con la densidad de población de cada ciudad.

La función sapply() es similar a lapply(), pero intenta simplificar el resultado. Si el resultado es una lista de vectores del mismo tipo y longitud, sapply() devuelve un vector o una matriz.

# Calcular la densidad de población de cada ciudad con sapply()
densidades <- sapply(ciudades, calcular_densidad)

densidades
#>  Nueva_York Los_Angeles     Chicago 
#>   10717.020    2995.392    4454.710

En este caso, sapply() devuelve un vector con las densidades de población.

2.4.2 apply(): aplicando una función a filas o columnas

La función apply() nos permite aplicar una función a las filas o columnas de una matriz o array. Es como si tuviéramos una herramienta que nos permite recorrer cada fila o columna de nuestra tabla de datos y realizar un cálculo específico en cada una.

Por ejemplo, si tenemos una matriz con las temperaturas máximas y mínimas de diferentes ciudades, podemos usar apply() para calcular la temperatura promedio de cada ciudad.

# Crear una matriz con temperaturas
temperaturas <- matrix(c(25, 18, 30, 22, 35, 28), nrow = 3, ncol = 2,
                       dimnames = list(c("Nueva York", "Los Ángeles", "Chicago"),
                                       c("Máxima", "Mínima")))

# Calcular la temperatura promedio de cada ciudad
temperaturas_promedio <- apply(temperaturas, 1, mean)

temperaturas_promedio
#>  Nueva York Los Ángeles     Chicago 
#>        23.5        26.5        29.0

En este ejemplo, apply() aplica la función mean() a cada fila de la matriz temperaturas (el argumento 1 indica que se debe aplicar la función a las filas) y devuelve un vector con las temperaturas promedio de cada ciudad.

Si quisiéramos calcular la temperatura máxima o mínima de entre todas las ciudades, podríamos usar apply() con la función max() o min(), respectivamente, y aplicarlo a las columnas (usando el argumento 2).

# Calcular la temperatura máxima de entre todas las ciudades
temperatura_maxima <- apply(temperaturas, 2, max)

temperatura_maxima
#> Máxima Mínima 
#>     30     35

2.4.3 mapply(): aplicando una función a múltiples argumentos

La función mapply() nos permite aplicar una función a múltiples argumentos en paralelo. Es como si tuviéramos una herramienta que nos permite tomar varios conjuntos de datos y aplicar la misma operación a cada conjunto correspondiente.

Por ejemplo, imagina que tenemos dos vectores: uno con los nombres de diferentes ciudades de Estados Unidos y otro con sus respectivas poblaciones. Queremos crear un nuevo vector que contenga la frase “La ciudad de [nombre de la ciudad] tiene una población de [población] habitantes”. Podríamos usar mapply() para aplicar una función que combine el nombre de la ciudad y su población a cada par de elementos de los vectores.

# Crear vectores con nombres de ciudades y poblaciones
ciudades <- c("Nueva York", "Los Ángeles", "Chicago")
poblaciones <- c(8.4e6, 3.9e6, 2.7e6)

# Función para crear la frase
crear_frase <- function(ciudad, poblacion) {
  frase <- paste("La ciudad de", ciudad, "tiene una población de", poblacion, "habitantes.")
  return(frase)
}

# Crear el vector con las frases
frases_ciudades <- mapply(crear_frase, ciudades, poblaciones)

frases_ciudades
#>                                                            Nueva York 
#>  "La ciudad de Nueva York tiene una población de 8400000 habitantes." 
#>                                                           Los Ángeles 
#> "La ciudad de Los Ángeles tiene una población de 3900000 habitantes." 
#>                                                               Chicago 
#>     "La ciudad de Chicago tiene una población de 2700000 habitantes."

En este ejemplo, mapply() aplica la función crear_frase a los vectores ciudades y poblaciones en paralelo, tomando un elemento de cada vector a la vez, y devuelve un vector con las frases resultantes.

Observa que la función crear_frase recibe dos argumentos: ciudad y poblacion. mapply() se encarga de tomar un elemento de cada vector y pasarlos como argumentos a la función. En la primera iteración, pasa “Nueva York” como ciudad y 8.4e6 como poblacion. En la segunda iteración, pasa “Los Ángeles” y 3.9e6, y así sucesivamente.

Otro ejemplo del uso de mapply() sería si tenemos dos vectores con las temperaturas máximas y mínimas de diferentes ciudades, y queremos calcular la diferencia de temperatura entre la máxima y la mínima para cada ciudad.

# Crear vectores con temperaturas máximas y mínimas
maximas <- c(25, 30, 35)
minimas <- c(18, 22, 28)

# Calcular la diferencia de temperatura para cada ciudad
diferencia_temperaturas <- mapply(function(max, min) max - min, maximas, minimas)

diferencia_temperaturas
#> [1] 7 8 7

En este ejemplo, mapply() aplica la función anónima function(max, min) max - min a los vectores maximas y minimas en paralelo, tomando el primer elemento de maximas y el primer elemento de minimas, luego el segundo elemento de cada vector, y así sucesivamente. Para cada par de elementos, la función anónima calcula la diferencia y devuelve un vector con los resultados.

2.4.4 Ejemplos: análisis de datos con funciones de orden superior

Las funciones de orden superior son una herramienta poderosa para el análisis de datos. Nos permiten realizar operaciones complejas de forma concisa y eficiente. Imagina que tienes una matriz con información sobre diferentes estados, donde cada fila representa un estado y cada columna una variable numérica, como la población o el ingreso per cápita. Podrías usar apply() para calcular la media de cada columna.

# Crear una matriz con información sobre estados
estados <- matrix(c(39.2e6, 29.0e6, 21.4e6, 64500, 56100, 50800), nrow = 3, ncol = 2,
                 dimnames = list(c("California", "Texas", "Florida"),
                                 c("poblacion", "ingreso_per_capita")))

# Calcular la media de cada columna
medias <- apply(estados, 2, mean)

medias
#>          poblacion ingreso_per_capita 
#>        29866666.67           57133.33

En este ejemplo, apply() aplica la función mean() a cada columna de la matriz estados y devuelve un vector con las medias.

Otro sería si tenemos una lista con los precios de diferentes hoteles en varias ciudades de Estados Unidos. Podrías usar sapply() para aplicar una función que calcule el impuesto de cada precio, o lapply() para convertir los precios de dólares a euros.

También podrías usar apply() para calcular el promedio de los precios de los hoteles en cada ciudad, o para encontrar el hotel más caro y el más barato en cada ciudad.

A medida que avancemos en el libro, veremos más ejemplos de cómo utilizar las funciones de orden superior para resolver problemas del mundo real.

Las posibilidades son infinitas, y las funciones de orden superior te brindan una gran flexibilidad para manipular y analizar tus datos.

2.5 Closures: funciones con memoria

Hasta ahora, hemos visto que las funciones en R reciben argumentos, ejecutan un conjunto de instrucciones y devuelven un resultado. Sin embargo, las funciones también pueden tener “memoria”, es decir, pueden recordar información entre llamadas. Esto es posible gracias a un concepto llamado closures.

2.5.1 Concepto: funciones que “recuerdan”

Un closure es una función que “recuerda” el entorno en el que fue creada. Esto significa que la función tiene acceso a las variables que estaban definidas en el momento de su creación, incluso si esas variables ya no están en el ámbito actual.

Para entender mejor este concepto, veamos un ejemplo. Imagina que queremos crear una función que cuente cuántas veces se ha llamado. Podemos hacerlo usando un closure:

crear_contador <- function() {
  contador <- 0  # Inicializar el contador

  # Definir la función que incrementa el contador
  incrementar_contador <- function() {
    contador <<- contador + 1
    return(contador)
  }

  return(incrementar_contador)  # Devolver la función
}

# Crear un contador
mi_contador <- crear_contador()

# Llamar al contador varias veces
mi_contador()  
#> [1] 1
mi_contador()  
#> [1] 2
mi_contador()  
#> [1] 3

En este ejemplo, la función crear_contador() crea una variable contador y una función incrementar_contador(). La función incrementar_contador() tiene acceso a la variable contador y la incrementa en 1 cada vez que se llama. La función crear_contador() devuelve la función incrementar_contador().

Cuando llamamos a mi_contador(), estamos llamando a la función incrementar_contador() que fue creada dentro de crear_contador(). Esta función “recuerda” el valor de la variable contador y lo incrementa en cada llamada.

Es importante destacar que la variable contador no es una variable global. Solo es accesible dentro de la función incrementar_contador(). Esto se debe a que contador fue definida dentro de la función crear_contador(), por lo que su ámbito es local a esa función.

Sin embargo, la función incrementar_contador() “captura” la variable contador en su entorno, lo que le permite acceder a ella incluso después de que la función crear_contador() haya terminado de ejecutarse.

2.5.2 Aplicaciones: creando contadores, funciones con estado interno

Los closures tienen muchas aplicaciones en la programación. Algunas de las más comunes son:

  • Crear contadores: Como vimos en el ejemplo anterior, los closures nos permiten crear funciones que mantienen un estado interno entre llamadas.
  • Crear funciones con parámetros configurables: Podemos usar closures para crear funciones que “recuerden” parámetros específicos. Por ejemplo, podríamos crear una función que genere funciones para convertir temperaturas de Celsius a Fahrenheit, donde la función generada “recuerde” la escala de temperatura a la que debe convertir.
  • Encapsular datos: Los closures nos permiten ocultar datos dentro de una función, lo que puede ser útil para proteger información sensible o para evitar conflictos de nombres. Por ejemplo, podríamos crear una función que genere identificadores únicos, donde la función generada “recuerde” el último identificador generado.

2.5.3 Ejemplos: simulando un juego, creando un historial de operaciones

Veamos algunos ejemplos más concretos del uso de closures:

  • Simulando un juego: Podemos usar un closure para simular un juego donde el jugador tiene que adivinar un número secreto. El closure puede “recordar” el número secreto y llevar la cuenta de los intentos del jugador.

  • Creando un historial de operaciones: Podemos usar un closure para crear una función que registre las operaciones que se realizan en una variable. El closure puede “recordar” el historial de operaciones y mostrarlo cuando se le solicite.

Los closures son una herramienta poderosa que nos permite escribir código más flexible y expresivo. A medida que te familiarices con ellos, descubrirás nuevas formas de aplicarlos en tus análisis de datos.

2.6 Depuración y manejo de errores: resolviendo los misterios de tu código

Hasta ahora, hemos explorado el fascinante mundo de las funciones en R. Hemos aprendido a crearlas, utilizarlas y combinarlas para realizar tareas complejas. Sin embargo, en el camino de la programación, es inevitable encontrarse con errores. A veces, nuestro código no funciona como esperamos, y nos encontramos con mensajes de error crípticos que nos dejan perplejos.

En esta sección, aprenderemos a identificar, entender y solucionar errores en nuestro código R. También veremos cómo manejar errores de forma elegante, para que nuestro código sea más robusto y confiable.

2.6.1 Identificando errores: mensajes de error comunes en R

Cuando nuestro código contiene un error, R nos mostrará un mensaje de error en la consola. Estos mensajes pueden parecer intimidantes al principio, pero con un poco de práctica, aprenderemos a interpretarlos y a utilizarlos para encontrar la causa del error.

Algunos mensajes de error comunes en R son:

  • Error: object 'nombre_objeto' not found: Este error ocurre cuando intentamos usar una variable o función que no existe. Puede ser que hayamos escrito mal el nombre, o que la variable o función no esté definida en el ámbito actual.
  • Error in nombre_funcion(argumentos): argumento no válido: Este error ocurre cuando pasamos un argumento inválido a una función. Por ejemplo, si pasamos un vector de texto a una función que espera un vector numérico.
  • Error in if (condicion) { ... }: argumento tiene longitud cero: Este error ocurre cuando la condición en una estructura if tiene longitud cero. Esto puede suceder si la condición se evalúa como NULL o como un vector vacío.
  • Error in for (variable in secuencia) { ... }: argumento no válido para 'en': Este error ocurre cuando la secuencia en un bucle for no es válida. Por ejemplo, si la secuencia es NULL o un vector de longitud cero.

Es importante leer cuidadosamente los mensajes de error y tratar de entender qué nos están diciendo. A menudo, el mensaje de error nos dará una pista sobre la causa del problema.

2.6.2 Herramientas de depuración: debug(), traceback()

R nos ofrece varias herramientas para depurar nuestro código y encontrar la causa de los errores. Dos de las herramientas más útiles son debug() y traceback().

  • debug(): Esta función nos permite ejecutar una función paso a paso, lo que nos permite inspeccionar el valor de las variables en cada paso y entender cómo se está ejecutando el código. Para usar debug(), simplemente llamamos a la función con el nombre de la función que queremos depurar como argumento.

    debug(mi_funcion)

    Luego, cuando llamemos a mi_funcion(), R entrará en modo de depuración y nos permitirá ejecutar el código línea por línea.

  • traceback(): Esta función nos muestra la secuencia de llamadas a funciones que llevaron al error. Esto puede ser útil para entender cómo se llegó al error y qué funciones están involucradas. Para usar traceback(), simplemente llamamos a la función después de que se haya producido un error.

    traceback()

    R mostrará una lista de las funciones que se llamaron, empezando por la función donde se produjo el error y terminando con la función que inició la ejecución del código.

2.6.3 Manejo de errores: tryCatch()

A veces, queremos que nuestro código continúe ejecutándose incluso si se produce un error. Para esto, podemos usar la función tryCatch().

tryCatch() nos permite especificar un bloque de código que se ejecutará si se produce un error. También podemos especificar un bloque de código que se ejecutará si no se produce ningún error.

tryCatch(
  {
    # Código que puede producir un error
  },
  error = function(e) {
    # Código que se ejecutará si se produce un error
  },
  finally = {
    # Código que se ejecutará siempre,  haya o no error
  }
)

Por ejemplo, si estamos leyendo datos de un archivo y el archivo no existe, podemos usar tryCatch() para mostrar un mensaje de error y continuar con la ejecución del códig

tryCatch(
  {
    datos <- read.csv("mi_archivo.csv")
  },
  error = function(e) {
    print("Error al leer el archivo.  Por favor,  verifica que el archivo existe.")
  }
)

2.6.4 Ejemplos: depurando funciones con errores, manejando excepciones

Veamos algunos ejemplos de cómo usar las herramientas de depuración y manejo de errores en R:

  • Depurando una función con debug():

    Imagina que creamos una función para calcular el índice de masa corporal (IMC) de una persona, pero al usarla, obtenemos un error. Podemos usar debug() para analizar qué está sucediendo dentro de la función.

    calcular_imc <- function(peso, altura) {
      imc <- peso / (altura ^ 2) 
      return(imc)
    }
    
    debug(calcular_imc)
    calcular_imc(peso = 70, altura = 1.75)  # Llamamos a la función para iniciar la depuración

    Al ejecutar este código, R entrará en modo de depuración. En la consola, veremos un nuevo prompt Browse[1]>. Podemos usar comandos como n (next) para ejecutar la siguiente línea de código, c (continue) para continuar la ejecución normal, o Q para salir del modo de depuración. También podemos imprimir el valor de las variables usando su nombre (ej. peso, altura, imc).

  • Manejando una excepción con tryCatch():

    Supongamos que estamos creando una función para calcular la tasa de crecimiento anual de la población de una ciudad. Si la población inicial es 0, la división producirá un error. Podemos usar tryCatch() para manejar esta situación:

    calcular_tasa_crecimiento <- function(poblacion_inicial, poblacion_final, años) {
      tryCatch(
        {
          tasa <- ((poblacion_final / poblacion_inicial)^(1 / años) - 1) * 100
          return(tasa)
        },
        error = function(e) {
          message("Error: La población inicial no puede ser cero.")
          return(NA)
        }
      )
    }
    
    calcular_tasa_crecimiento(10000, 12000, 5)  # Output: 3.7137...
    #> [1] 3.713729
    calcular_tasa_crecimiento(0, 12000, 5)  # Output: "Error: La población inicial no puede ser cero." 
    #> [1] Inf

    En este ejemplo, si la poblacion_inicial es 0, tryCatch() captura el error y muestra un mensaje. Luego, devuelve NA para indicar que el cálculo no se pudo realizar.

Con la práctica, aprenderás a utilizar estas herramientas para depurar tu código, manejar errores y escribir programas más robustos y confiables.

2.7 Ejercicios

¡Es hora de poner a prueba tus habilidades con las funciones! A continuación, encontrarás una serie de ejercicios con diferentes niveles de dificultad.

  1. Crea una función llamada millas_a_kilometros() que convierta millas a kilómetros. La función debe recibir un argumento millas y devolver el equivalente en kilómetros. (Recuerda que 1 milla equivale a 1.60934 kilómetros).
Solución
millas_a_kilometros <- function(millas) {
  kilometros <- millas * 1.60934
  return(kilometros)
}
  1. Crea una función llamada area_triangulo() que calcule el área de un triángulo. La función debe recibir dos argumentos: base y altura, y devolver el área del triángulo. (Recuerda que el área de un triángulo es igual a (base * altura) / 2).
Solución
area_triangulo <- function(base, altura) {
  area <- (base * altura) / 2
  return(area)
    }
  1. Crea una función llamada precio_con_iva() que calcule el precio de un producto con IVA. La función debe recibir dos argumentos: precio_sin_iva y tasa_iva (por defecto, 0.16), y devolver el precio con IVA.
Solución
precio_con_iva <- function(precio_sin_iva, tasa_iva = 0.16) {
  precio_con_iva <- precio_sin_iva * (1 + tasa_iva)
  return(precio_con_iva)
}
  1. Crea una función llamada es_par() que determine si un número es par. La función debe recibir un argumento numero y devolver TRUE si el número es par y FALSE si no lo es. (Pista: usa el operador módulo %%).
Solución
    es_par <- function(numero) {
      return(numero %% 2 == 0)
    }
  1. Crea una función llamada factorial() que calcule el factorial de un número. El factorial de un número entero positivo n, denotado por n!, es el producto de todos los enteros positivos menores o iguales que n. Por ejemplo, 5! = 5 * 4 * 3 * 2 * 1 = 120. (Pista: usa una función recursiva).
Solución
factorial <- function(n) {
  if (n == 0) {
    return(1)
  } else {
    return(n * factorial(n - 1))
  }
}
  1. Crea una función llamada fibonacci() que genere una secuencia de Fibonacci de una longitud dada. La secuencia de Fibonacci es una serie de números en la que cada número es la suma de los dos anteriores. La secuencia comienza típicamente con 0 y 1. Por ejemplo, una secuencia de Fibonacci de longitud 10 sería: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34.
Solución
fibonacci <- function(n) {
  if (n <= 0) {
    return(numeric(0))
  } else if (n == 1) {
    return(0)
  } else if (n == 2) {
    return(c(0, 1))
  } else {
    fib_seq <- numeric(n)
    fib_seq[1] <- 0
    fib_seq[2] <- 1
    for (i in 3:n) {
      fib_seq[i] <- fib_seq[i - 1] + fib_seq[i - 2]
    }
    return(fib_seq)
  }
}

fibonacci(10)
#>  [1]  0  1  1  2  3  5  8 13 21 34
  1. Crea una función llamada mcd() que calcule el máximo común divisor (MCD) de dos números. El MCD de dos o más números enteros no nulos es el mayor entero positivo que los divide sin dejar residuo. Por ejemplo, el MCD de 12 y 18 es 6. (Pista: usa el algoritmo de Euclides).
Solución
mcd <- function(a, b) {
  while (b != 0) {
    temp <- b
    b <- a %% b
    a <- temp
  }
  return(a)
}
  1. Crea una función llamada validar_contrasena() que valide una contraseña. La función debe recibir un argumento contrasena y devolver TRUE si la contraseña cumple con las siguientes condiciones, y FALSE en caso contrario:

    • Tiene al menos 8 caracteres.
    • Contiene al menos una letra mayúscula.
    • Contiene al menos una letra minúscula.
    • Contiene al menos un número.
    • Contiene al menos un carácter especial (!@#$%^&*).
Solución
validar_contrasena <- function(contrasena) {
  if (nchar(contrasena) < 8) {
    return(FALSE)
  }
  if (!grepl("[A-Z]", contrasena)) {
    return(FALSE)
  }
  if (!grepl("[a-z]", contrasena)) {
    return(FALSE)
  }
  if (!grepl("[0-9]", contrasena)) {
    return(FALSE)
  }
  if (!grepl("[!@#$%^&*]", contrasena)) {
    return(FALSE)
  }
  return(TRUE)
}
  1. Crea una función llamada aplicar_descuento() que reciba una función de cálculo de precio y un descuento como argumentos. La función aplicar_descuento() debe devolver una nueva función que calcule el precio con el descuento aplicado.
Solución
aplicar_descuento <- function(funcion_precio, descuento) {
  function(precio_original) {
    precio_con_descuento <- funcion_precio(precio_original) * (1 - descuento)
    return(precio_con_descuento)
  }
}
  1. Crea una función llamada crear_conversor_temperatura() que reciba una escala de temperatura como argumento (“Celsius”, “Fahrenheit” o “Kelvin”). La función debe devolver una función que convierta temperaturas a la escala especificada.
Solución
crear_conversor_temperatura <- function(escala) {
  if (escala == "Celsius") {
    return(function(temp) (temp - 32) * 5 / 9)  # Fahrenheit a Celsius
  } else if (escala == "Fahrenheit") {
    return(function(temp) (temp * 9 / 5) + 32)  # Celsius a Fahrenheit
  } else if (escala == "Kelvin") {
    return(function(temp) temp + 273.15)  # Celsius a Kelvin
  } else {
    stop("Escala de temperatura inválida.")
  }
}
  1. Crea una función llamada adivinar_numero() que simule un juego de adivinar el número. La función debe generar un número aleatorio entre 1 y 100 y pedir al usuario que lo adivine. La función debe dar pistas al usuario (mayor o menor) y contar el número de intentos. (Pista: usa un closure para almacenar el número secreto y el número de intentos).
Solución
adivinar_numero <- function() {
  numero_secreto <- sample(1:100, 1)
  intentos <- 0

  adivinar <- function() {
    intentos <<- intentos + 1
    cat("Intento", intentos, ": ")
    numero <- as.numeric(readline())
    if (is.na(numero)) {
      cat("Por favor, ingresa un número válido.\n")
    } else if (numero < numero_secreto) {
      cat("El número secreto es mayor.\n")
    } else if (numero > numero_secreto) {
      cat("El número secreto es menor.\n")
    } else {
      cat("¡Adivinaste! El número secreto era", numero_secreto, "\n")
      cat("Te tomó", intentos, "intentos.\n")
    }
  }

  return(adivinar)
}

juego <- adivinar_numero()

juego()
#> Intento 1 : 
#> Por favor, ingresa un número válido.
  1. Crea una función que, dado un vector de números enteros, encuentre la subsecuencia contigua con la suma máxima. Por ejemplo, para el vector c(-2, 1, -3, 4, -1, 2, 1, -5, 4), la subsecuencia contigua con la suma máxima es c(4, -1, 2, 1), con una suma de 6.
Solución
max_subsecuencia <- function(x) {
  max_actual <- 0
  max_global <- 0
  inicio <- 1
  fin <- 1
  inicio_temp <- 1

  for (i in 1:length(x)) {
    max_actual <- max_actual + x[i]
    if (max_actual > max_global) {
      max_global <- max_actual
      inicio <- inicio_temp
      fin <- i
    }
    if (max_actual < 0) {
      max_actual <- 0
      inicio_temp <- i + 1
    }
  }
  return(list(subsecuencia = x[inicio:fin], suma = max_global))
}

test <- c(-2, 1, -3, 4, -1, 2, 1, -5, 4)
max_subsecuencia(test)
#> $subsecuencia
#> [1]  4 -1  2  1
#> 
#> $suma
#> [1] 6
  1. Crea una función que, dado un vector de caracteres, determine si es posible obtener un palíndromo reordenando sus letras. Un palíndromo es una palabra o frase que se lee igual de izquierda a derecha que de derecha a izquierda (ej. “anilina”, “Anita lava la tina”).
Solución
es_palindromo_posible <- function(texto) {
  letras <- strsplit(tolower(texto), "")[[1]]
  frecuencias <- table(letras)
  impares <- sum(frecuencias %% 2)
  return(impares <= 1)
}

test <- c("anilina", "Anita lava la tina", "danieleinad")
result <- sapply(test, es_palindromo_posible)
result
#>            anilina Anita lava la tina        danieleinad 
#>               TRUE              FALSE               TRUE
  1. Crea una función que, dado un número entero positivo, determine si es un número primo. Un número primo es un número natural mayor que 1 que no tiene más divisores que 1 y sí mismo.
Solución
es_primo <- function(n) {
  if (n <= 1) {
    return(FALSE)
  }
  if (n <= 3) {
    return(TRUE)
  }
  if (n %% 2 == 0 || n %% 3 == 0) {
    return(FALSE)
  }
  i <- 5
  while (i * i <= n) {
    if (n %% i == 0 || n %% (i + 2) == 0) {
      return(FALSE)
    }
    i <- i + 6
  }
  return(TRUE)
}
La condición i * i <= n en el bucle while limita las iteraciones a la raíz cuadrada de n. Esto optimiza el algoritmo, ya que no es necesario verificar divisores mayores que la raíz cuadrada de n. El incremento i <- i + 6 se basa en la observación de que todos los números primos mayores que 3 se pueden expresar en la forma 6k ± 1. Por lo tanto, solo es necesario verificar los números de la forma 6k ± 1 como posibles divisores.