Crear funciones en R

Introducción a R Conceptos básicos de R
Aprende a crear funciones propias en R

El lenguaje de programación R permite al usuario crear sus propias funciones. En este tutorial aprenderás cómo escribir funciones en R, cómo es la sintaxis, los argumentos, la salida, cómo utilizar la función return y cómo hacer un uso correcto de los parámetros opcionales, adicionales y los argumentos predeterminados.

¿Cómo crear funciones en R?

Las funciones base de R no siempre cubren todas nuestras necesidades. Para escribir una función en R, primero necesitamos saber cómo es la sintaxis del comando function. La sintaxis básica de una función de R es la siguiente.

nombre_funcion <- function(arg1, arg2, ... ) {
        # Código
}

En el bloque de código anterior tenemos las siguientes partes:

  • arg1, arg2, ... son los argumentos de entrada.
  • # Código representa el código a ser ejecutado dentro de la función para obtener la salida desada.

La salida de la función puede ser un número, una lista, un data.frame, un gráfico, un mensaje o cualquier objeto que queramos. También podemos asignarle a la salida alguna clase, pero hablaremos sobre ello en un artículo específico sobre las clases S3. Ésto último es especialmente interesante a la hora de escribir funciones para paquetes de R.

Creando una función en R

Para introducirnos en la creación de funciones de R, crearemos una función para trabajar con progresiones geométricas. Una progresión geométrica es una sucesión de números \(a_1, a_2, a_3\) de modo que cada uno de ellos (excepto el primero) es igual al último multiplicado por una constante r llamada ratio. Se puede verificar que,

\(a_2 = a_1 \cdot r; \qquad a_3 = a_2 \cdot r = a_1 \cdot r^2; \dots\)

Por lo tanto, generalizando este proceso, puede obtenerse el término general

\(a_n = a_1 \cdot r^{n-1}.\)

También se puede verificar que la suma de los n términos de la progresión es

\(S_n = a_1 + \dots + a_n = \frac{a_1(r^n - 1)}{r-1}.\)

Con esto en mente, puedes crear la siguiente función,

an <- function(a1, r, n){
    a1 * r ** (n - 1)
}

que calcula el término general \(a_n\) de una progresión geométrica pasando los parámetros \(a_1\), el ratio r y el valor n. En el siguiente bloque podemos ver algunos ejemplos, mostrando su salida como comentarios.

an(a1 = 1, r = 2, n = 5)  # 16
an(a1 = 4, r = -2, n = 6) # -128

Con la función anterior puedes obtener varios valores de la progresión pasando un vector al argumento n.

an(a1 = 1, r = 2, n = 1:5)   # a_1, ..., a_5
an(a1 = 1, r = 2, n = 10:15) # a_10,..., a_15

También puedes calcular los primeros n elementos de la progresión con la función sn, definida a continuación.

sn <- function(a1, r, n){
    a1 * (r ** n-1)/(r - 1)
}
sn(a1 = 1, r = 2, n = 5) # 31

# Equivalente
values <- an(a1 = 1, r = 2, n = 1:5)
values

sum(values) # 31

Argumentos de entrada

Los argumentos son valores de entrada de las funciones. Como ejemplo, en la función que creamos antes tenemos tres argumentos de entrada llamados a1, r y n. Hay varias consideraciones cuando se trata con este tipo de argumentos.

  • Si mantenemos el orden de entrada, no necesitamos llamar a los nombres de los argumentos. Como ejemplo, las siguientes llamadas a la función son equivalentes.
an(1, 2, 5) # Devuelve 16
an(a1 = 1, r = 2, n = 5) # Devuelve 16
  • Si ponemos el nombre de los argumentos, podemos usar cualquier oden.
an(r = 2, n = 5, a1 = 1) # Devuelve 16
an(n = 5, r = 2, a1 = 1) # Devuelve 16
  • Podemos usar la función args para conocer los argumentos de entrada de cualquier función que queramos usar.
args(an)
  • Si escribimos el nombre de la función, la consola devolverá el código de la función.

Ten en cuenta que a veces no podrás ver el código fuente de una función si ésta no está escrita en R.

Argumentos adicionales en las funciones de R

En ocasiones resulta muy interesante tener argumentos predeterminados en la función. En tal caso se utilizarán los valores predeterminados a menos que se incluyan otros al ejecutar la función. Al escribir una función, como la de nuestro ejemplo,

nombre_función <- function(arg1, arg2, arg3 ) {
        # Código
}

si queremos que arg2 y arg3 sean los valores a y b por defecto, podemos asignarlos en los argumentos de nuestra función.

nombre_función <- function(arg1, arg2 = a, arg3 = b) {
        # Código
}

Ilustraremos esto con un ejemplo muy simple. Considera una función que dibuja la función coseno.

coseno <- function(w = 1, min = -2 * pi, max = 2 * pi) {
    x <- seq(-2 * pi, 2 * pi, length = 200)
    plot(x, cos(w * x), type = "l")
}

Nótese que esta no es la mejor manera de usar una función para dibujar un gráfico. Revisa las clases S3 para ese propósito.

Si ejecutamos coseno(), la gráfica de cos(x) se trazará por defecto en el intervalo [-2 π , 2 π ]. Sin embargo, si queremos graficar la función cos(2x) en el mismo intervalo, necesitamos ejecutar coseno(w = 2). Veamos algunos ejemplos:

# Una fila, tres columnas
par(mfcol = c(1, 3))

coseno()
coseno(w = 2)
coseno(w = 3, min = -3 * pi)

Salida de una función de ejemplo en R

Argumentos adicionales en R

El argumento ... (punto punto punto) permite pasar argumentos libremente que se usarán en una subfunción dentro de la función principal. Como ejemplo, en la función,

coseno <- function(w = 1, min = -2 * pi, max = 2 * pi, ...) {
    x <- seq(-2 * pi, 2 * pi, length = 200)
    plot(x, cos(w * x), type = "l", ...)
}

los argumentos que se pasen dentro de ... se usarán por la función plot. Veamos un ejemplo completo:

par(mfcol = c(1, 2))

coseno(w = 2, col = "red", type = "l", lwd = 2)
coseno(w = 2, ylab = "")

Ejemplo de uso de los argumentos adicionales de las funciones de R

La función return

Por defecto, las funciones de R devolverán el último objeto evaluado dentro de ella. También podemos hacer uso de la función return, que es especialmente importante cuando se quiere devolver un objeto u otro dependiendo de ciertas condiciones o cuando se quiere ejecutar algún código después del objeto que queremos devolver.

Además, ten en cuenta que podemos devolver todos los tipos de objetos de R, pero solo uno de ellos. Por esa razón, es muy habitual devolver una lista de objetos. Veamos un ejemplo:

asn <- function(a1 = 1, r = 2, n = 5) {
    A  <- an(a1, r, n)
    S  <- sn(a1, r, n)
    ii <- 1:n
    AA <- an(a1, r, ii)
    SS <- sn(a1, r, ii)
    return(list(an = A, sn = S,
                salida = data.frame(valores = AA,
                                    sum = SS)))
}

Cuando ejecutes la función, obtendrás el siguiente resultado. Recuerda tener las funciones sn y an en el espacio de trabajo.

asn()
$`an`
[1] 16

$sn
[1] 31

$salida
  valores sum
1      1   1
2      2   3
3      4   7
4      8  15
5     16  31

Puede que te hayas dado cuenta de que en el caso anterior es equivalente usar la función return o no usarla. Sin embargo, considera el siguiente ejemplo, donde queremos comprobar si los parámetros que se pasan a los argumentos son números o no. Para ello, si alguno de los parámetros no es un número devolveremos un string, pero si son números seguiremos ejecutando el código.

asn <- function(a1 = 1, r = 2, n = 5) {
    if(!is.numeric(c(a1, r, n))) return("Todos los parámetros deben ser números")
    A  <- an(a1, r, n)
    S  <- sn(a1, r, n)
    ii <- 1:n
    AA <- an(a1, r, ii)
    SS <- sn(a1, r, ii)
    return(list(an = A, sn = S,
                salida = data.frame(valores = AA,
                                    sum = SS)))
}
asn("3")
"Todos los parámetros deben ser números"

Ahora bien, si en lugar de usar la función return hubiésemos usado la función print, si se pasa algún dato que no sea numérico se devolverá el texto pero también un error, ya que se ejecutará todo el código.

asn <- function(a1 = 1, r = 2, n = 5) {
    if(!is.numeric(c(a1, r, n))) print("Todos los parámetros deben ser números")
    A  <- an(a1, r, n)
    S  <- sn(a1, r, n)
    ii <- 1:n
    AA <- an(a1, r, ii)
    SS <- sn(a1, r, ii)
    return(list(an = A, sn = S,
                salida = data.frame(valores = AA,
                                    sum = SS)))
}
asn("3")
Todos los parámetros deben ser números
Error in a1 * r^(n - 1) : argumento no-numérico para operador binario

Variables locales y globales en R

En R no es necesario declarar las variables utilizadas dentro de una función. La regla llamada “ámbito lexicográfico” se usa para decidir si un objeto es local a una función o global. Considera el siguiente ejemplo:

fun <- function() {
    print(x)
}

x <- 1

fun() # 1

La variable x no se define dentro de fun, por lo que R buscará x dentro del ámbito “circundante” e imprimirá su valor. Si x se usa como el nombre de un objeto dentro de la función, el valor de x en el entorno global (fuera de la función) no cambia.

x <- 1
fun2 <- function() {
    x <- 2
    print(x)
}

fun2() # 2
x #1

Para cambiar el valor global de una variable dentro de una función, puedes usar el operador de doble asignación (<<-).

x <- 1
y <- 3
fun3 <- function() {
    x <- 2
    y <<- 5
    print(paste(x, y))
}

fun3() # 2 5
x # 1 (el valor no cambió)
y # 5 (el valor cambió)

Escribiendo funciones en R. Ejemplos

En esta sección se muestran diferentes ejemplos de funciones R para mejorar la comprensión de su creación y uso para el lector.

Función de ejemplo 1: Letra del DNI español

Vamos a calcular la letra del DNI a partir de su correspondiente número. El método utilizado para obtener la letra (L) del DNI consiste en dividir el número entre 23 y de acuerdo con el resto (R) obtenido otorgar la letra correspondiente de la siguiente tabla.

R L R L R L R L
0 T 7 F 14 Z 21 K
1 R 8 P 15 S 22 E
2 W 9 D 16 Q
3 A 10 X 17 V
4 G 11 B 18 H
5 M 12 N 19 L
6 Y 13 J 20 C

Nuestra función será como sigue:

DNI <- function(numero) {
    letras <- c("T", "R", "W", "A", "G", "M", "Y", "F", "P", "D", "X", "B",
                "N", "J", "Z", "S", "Q", "V", "H", "L", "C", "K", "E")
    letras <- letras[numero %% 23 + 1]
    return(letras)
}
DNI(50247828) # G

Función de ejemplo 2: Lanzando un dado

La siguiente función simula n (por defecto n = 100) lanzamientos de un dado. La función devuelve la tabla de frecuencias y el gráfico correspondiente.

dado <- function(n = 100){
    lanzamientos <- sample(1:6, n, rep = T)
    frecuencias <- table(lanzamientos)/n
    barplot(frecuencias, main = "")
    abline(h = 1/6, col = "red", lwd = 2)
    return(frecuencias)
}

Puedes ver los resultados de simulación ejecutando la función.

par(mfcol = c(1, 3))

dado(100)
dado(500)
dado(100000)
# 100
 1     2    3   4    5    6
0.17 0.11 0.20 0.16 0.25 0.11

# 500
  1     2     3    4      5     6
0.144 0.158 0.148 0.178 0.164 0.208

# 100000
    1      2       3       4       5      6
0.16612 0.16630 0.16569 0.16791 0.16697 0.16701

Ejemplo de cómo crear una función propia en R

Como puedes ver, a medida que aumentamos n estamos más cerca del valor teórico 1/6 = 0.1667.