21 FUNCIONES DEFINIDAS POR EL USUARIO

En adición a las funciones que R trae incorporadas en los paquetes que se cargan al inicio o en cualquier otro paquete, el usuario puede crear sus propias funciones para realizar de manera ágil y personalizada labores que ejecute consuetudinariamente.

Esta es una de las características que marcan una mayor diferencia entre R y otras aplicaciones para análisis estadístico. Al ser R un lenguaje de programación, sus posibilidades son prácticamente ilimitadas, no siendo necesario que R ‘tenga’ una función destinada a un objetivo particular, puesto que el usuario puede crearla, acorde con sus requerimientos y necesidades.

El formato general para crear una función personalizada es:

nombre.función <- function (arg1, arg2, ... , argk, ...)
{
  instrucciones
}

Una vez cargada en memoria, la función podrá usarse, invocándola por su nombre, haciendo uso de los argumentos necesarios, con lo cual se ejecutarán todas las instrucciones especificadas entre llaves en la definición de la función.

nombre.función(arg1, arg2, ..., argk, ...)

Hay dos maneras de cargar una función personalizada en el ambiente de trabajo. La primera es abriendo el script que la contiene y ejecutándolo en su totalidad, desde la línea en la que se define la función hasta la línea que contiene la llave de cierre. La otra forma es invocándola, sin necesidad de abrir el script, mediante el comando source. Así, para invocar, por ejemplo, una función guardada en un archivo llamado statistics.R (aunque el script puede tener el mismo nombre de la función, no es obligatorio que sea así), se usaría la siguiente instrucción:

source("statistics.R")

Cualquiera que sea el método seguido, podrá verificarse si la función sí fue cargada, revisando el Global Environment (usualmente en la ventana superior derecha) de RStudio. Tras cargar una función, esta deberá aparecer allí, en el apartado Functions.

Considérese una función que tome como argumento un vector numérico, le calcule una serie de estadísticos básicos, los cuales reporte con cuatro cifras decimales y con su correspondiente leyenda en español, además de generar un gráfico de caja y bigotes.

statistics <- function (x)
{
  cat("Media               =", round(mean(x), 4), "\n")
  cat("Desviación estándar =", round(sd(x), 4), "\n")
  q <- quantile(x)
  cat("Mínimo              =", round(q[1], 4), "\n")
  cat("Cuartil 1 (25%)     =", round(q[2], 4), "\n")
  cat("Cuartil 2 (50%)     =", round(q[3], 4), "\n")
  cat("Cuartil 3 (75%)     =", round(q[4], 4), "\n")
  cat("Máximo              =", round(q[5], 4), "\n")
  if (!require(agricolae))
    install.packages("agricolae", dependencies = T)
  library(agricolae)  # Para cálculo de asimetría y curtosis
  s <- skewness(x)
  k <- kurtosis(x)
  cat("Coef. asimetría     =", round(s, 4), "\n")
  cat("Coef. curtosis      =", round(k, 4), "\n")
  if (!require(lattice))
    install.packages("lattice", dependencies = T)
  library(lattice)  # Para el diagrama de caja y bigotes
  bwplot(x)
}

Supóngase que se quiere usar la función creada anteriormente sobre un vector de 100 observaciones seudoaleatorias de la distribución normal estándar, usando 47 como semilla (con el fin de que los resultados sean reproducibles). Tras cargar la función en memoria, por cualquiera de los dos métodos descritos anteriormente, esta puede invocarse así:

set.seed(47)
statistics(rnorm(100))
#> Media               = 0.0515 
#> Desviación estándar = 0.9826 
#> Mínimo              = -2.3224 
#> Cuartil 1 (25%)     = -0.6958 
#> Cuartil 2 (50%)     = 0.039 
#> Cuartil 3 (75%)     = 0.8857 
#> Máximo              = 2.1879
#> Coef. asimetría     = -0.138 
#> Coef. curtosis      = -0.4123

La escritura de funciones definidas por el usuario es un arte que va de la mano con la programación. El texto de Santana y Mateos (2014) constituye una muy buena referencia en español sobre este tema, manteniéndose en un nivel relativamente básico. Si se desea profundizar, pueden consultarse textos más especializados como The R Inferno de Burns (2011), quien mediante un sentido del humor muy particular establece un paralelo con la Divina Comedia de Dante (¡a quien le agradece por sus útiles comentarios!), presentado un mapa para moverse en el ‘infierno’ que R puede representar para muchos usuarios. También es recomendable consultar la definición del lenguaje R, la cual es escrita y actualizada por el equipo nuclear de R (2022).

Un excelente recurso para captar la lógica y el estilo de las funciones consiste en revisar funciones existentes en R. Para ver el contenido de una función, en muchas ocasiones basta con escribir el nombre de dicha función y presionar Enter.

Veamos cómo obtener, por ejemplo, el código de la función factor:

factor
#> function (x = character(), levels, labels = levels, exclude = NA, 
#>     ordered = is.ordered(x), nmax = NA) 
#> {
#>     if (is.null(x)) 
#>         x <- character()
#>     nx <- names(x)
#>     if (missing(levels)) {
#>         y <- unique(x, nmax = nmax)
#>         ind <- order(y)
#>         levels <- unique(as.character(y)[ind])
#>     }
#>     force(ordered)
#>     if (!is.character(x)) 
#>         x <- as.character(x)
#>     levels <- levels[is.na(match(levels, exclude))]
#>     f <- match(x, levels)
#>     if (!is.null(nx)) 
#>         names(f) <- nx
#>     if (missing(labels)) {
#>         levels(f) <- as.character(levels)
#>     }
#>     else {
#>         nlab <- length(labels)
#>         if (nlab == length(levels)) {
#>             nlevs <- unique(xlevs <- as.character(labels))
#>             at <- attributes(f)
#>             at$levels <- nlevs
#>             f <- match(xlevs, nlevs)[f]
#>             attributes(f) <- at
#>         }
#>         else if (nlab == 1L) 
#>             levels(f) <- paste0(labels, seq_along(levels))
#>         else stop(gettextf("invalid 'labels'; length %d should be 1 or %d", 
#>             nlab, length(levels)), domain = NA)
#>     }
#>     class(f) <- c(if (ordered) "ordered", "factor")
#>     f
#> }
#> <bytecode: 0x000001ddeedbb150>
#> <environment: namespace:base>

En otros casos resulta un tanto más complejo obtener el código de una función determinada.

t.test
#> function (x, ...) 
#> UseMethod("t.test")
#> <bytecode: 0x000001ddf4d516d0>
#> <environment: namespace:stats>

Este resultado indica que la función de interés es genérica y que usa métodos o subfunciones diferenciadas, dependiendo la clase del primer argumento. En tales casos, casi siempre hay un método por defecto (default), cuyo código es el que se quiere visualizar.

Para ello se usa la siguiente instrucción:

getAnywhere(t.test.default) 

En otros casos, no existe un método por defecto, al que pueda accederse agregando el sufijo default al nombre de la función:

TukeyHSD
#> function (x, which, ordered = FALSE, conf.level = 0.95, ...) 
#> UseMethod("TukeyHSD")
#> <bytecode: 0x000001ddf4d69008>
#> <environment: namespace:stats>

Este resultado indica que para buscar los métodos disponibles, debe usarse la función methods.

methods(TukeyHSD)
#> [1] TukeyHSD.aov*
#> see '?methods' for accessing help and source code

Una vez ubicado el método de interés, se obtiene su código mediante la función getAnywhere:

getAnywhere(TukeyHSD.aov)

21.1 Funciones sin argumentos

Cuando todos los argumentos de una función tienen valores por defecto, es posible correrla sin especificar ningún argumento, usando únicamente los paréntesis. Así, por ejemplo, cuando se ejecuta la instrucción box(), se crea una caja, en la que se usan todos los valores por defecto (línea continua, negra, de ancho 1), como las que se muestran en el capítulo 19. Si se deseara otro tipo de caja, podría especificarse, mediante los correspondientes argumentos.

box(col = "red", lty = 3, lwd = 2)

En otros casos, la función se corre siempre sin argumentos.

search()

21.2 El argumento especial triple punto “…”

Este argumento tiene dos usos, dependiendo de si ocupa la primera posición o la última. Cuando se ubica en la primera posición, indica que la función tiene un número indefinido de argumentos de la misma índole. Tal es el caso de la función cat, que concatena un número cualquiera de argumentos.

args(cat)
#> function (..., file = "", sep = " ", fill = FALSE, labels = NULL, 
#>     append = FALSE) 
#> NULL

En estas situaciones, puesto que los argumentos adicionales no tienen una posición definida, es necesario nombrarlos siempre que vayan a usarse, no siendo posible asociarlos con la posición (cf. sección 2.4). Así, por ejemplo, si se quisiera usar un separador diferente al que viene por defecto (un espacio en blanco), sería necesario presentarlo explícitamente, mediante el argumento sep.

Asimismo, puede ubicarse este argumento al final de la función, cuando se desea dejar abierta la posibilidad de que el usuario incorpore argumentos adicionales para uso en las funciones internas que forman parte de la función principal. En la función que se presenta en el capítulo 23, se ilustra este aspecto.