Empezando en R (VI). Creando funciones

Bueno, ha pasado un tiempo y se que os debía continuar con el curso de R ¡volvemos!

En la anterior entrada os conté como se podían crear rutinas en R para automatizar procesos que requieren de varias operaciones. Pues bien, una vez diseñadas esas rutinas, ¿no sería ideal poder llamarlas de forma sencilla para que se ejecutasen siempre que lo necesitásemos?

R lo hace a través de sus funciones que automatizan tareas de forma que tenemos fácil acceso a todas ellas. Pero ¿qué pasa si lo que queremos hacer no está ya en una función de R? ¿Si es una nueva tarea muy nuestra?

Hoy hablamos de cómo crear nuestras propias funciones en R

¿Me dejas que te cuente?

Funciones en R

Ya hablamos de funciones en la entrada sobre operadores. Allí las usábamos simplemente como eso, como merosm operadores pero, en realidad, en R encontramos funciones que pueden hacer cualquier cosa. Empecemos por definir lo que es una función

¿Qué es una función?

Una función es una sucesión de operaciones que se pueden llamar desde nuestro código de forma sencilla. Dicha llamada se realiza usando el nombre de la función seguido de unos paréntesis que encierran los argumentos que manejarán el comportamiento de la función. Estos argumentos son especialmente importantes ya que de ellos dependerá el resultado.

En particular, dentro de estos argumentos podemos encontrar objetos y parámetros. Los primeros serán – valga la redundancia– el objeto de la función sobre el que se realizarán las operaciones. Los parámetros por su parte, manejarán los detalles de las operaciones.

Saber cuáles son los argumentos que necesita una función es fundamental para usarla correctamente y conocerlos es relativamente sencillo usando la función help.

Por poner un ejemplo, si ejecutamos

> help(mean)

obtenemos donde podemos ver que hay tres argumentos principales para ejecutar la función.

  • El objeto sobre el que calcular la media, y
  • dos parámetros que nos indicarán
    • si queremos recortar la media (eliminar los valores más grandes o más pequeños antes de calcularla) e
    • ignorar o no, los valores faltantes.

A continuación vemos que aparecen tres puntos que indican que le podríamos pasar más parámetros que pueden estar relacionados con algún método interno. Hablaremos más de estos puntitos enseguida porque vamos a ver como podemos usarlos al generar nuestra propia función.

Una función propia

A la hora de definir una función la sintaxis es siempre la misma. Creamos un objeto cuyo nombre será aquel con el que llamaremos a la función. Es importante ser cautos con la asignación de nombres para evitar posible duplicidad sobre las funciones ya existentes en R.

Una vez decidido el nombre, lo convertiremos en un objeto de tipo función así:

> nombre <- function(arg1,arg2,...){expre}

Aquí expre es una expresión o grupo de expresiones (entre llaves), es decir, una rutina como las que ya hemos estudiado en la entrada anterior. Es importante también recordar que, como allí dijimos, el valor final de la rutina será la última expresión que se devuelva del grupo. Ese valor que puede ser un simple número, un vector, una gráfica, una lista o un mensaje impreso por pantalla, será lo que nos devuelva la función al ejecutarla.

Las ordenes que se ejecutan dentro de expre utilizan argumentos que aquí hemos nombrado como arg1,arg2,… y para llamarla lo haremos como con cualquier otra función de R:

> nombre(arg1 = valor_arg1 ,arg2 = valor_arg2, ...)

Veamos algunos ejemplos sencillos.

Mis primeras funciones

Empecemos por una función que suma dos objetos.

> sumaobjetos<-function(x,y){
+   (x+y)
+ }
> #puedo sumar números
> sumaobjetos(3,5)
## [1] 8
> # ¿Y si quiero sumar vectores? La función también es válida
> sumaobjetos(c(2,3),c(3,5))
## [1] 5 8

En el siguiente ejemplo vamos a crear nuestra propia función factorial (nota: el factorial de un número natural es el producto de todos los números entre 1 y ese valor)

> factorial<-function(x){ 
+   prod(1:x)
+ }
> factorial(8)
## [1] 40320

El último ejemplo que os presento aquí es una función que genera un vector que en cada posición tiene el valor más grande de los dos vectores que le pasamos.

> grande<-function(x,y){
+   y.g<-y>x #compara y con x y nos da un vector índice con TRUE y FALSE
+ x[y.g]<-y[y.g] 
+ #En las posiciones de x en las que y era mayor que x (y.g = TRUE) 
+ #insertamos el valor de y en esa posición
+ x
+ }
> grande(1:5,c(1,6,2,7,3))
## [1] 1 6 3 7 5

En esta última función podemos preguntarnos qué sucede si no ponemos la última sentencia.

> grande<-function(x,y){
+   y.g<-y>x #compara y con x y nos da un vector índice con TRUE y FALSE
+ x[y.g]<-y[y.g] 
+ #En las posiciones de x en las que y era mayor que x (y.g = TRUE) 
+ #insertamos el valor de y en esa posición
+ }
> grande(1:5,c(1,6,2,7,3))

Vemos que en este caso, no devuelve nada. esto sucede porque la rutina utilizada lo guarda todo en otros objetos que se quedan en un nivel interno y no van ni siquiera a nuestro environment. Volveremos sobre esto un poco más adelante. De momento solo decir que es importante que el final de nuestra rutina devuelva el valor que queremos recuperar.

Nota: esta devolución se puede realizar en cualquier momento de la rutina usando la función return(objeto) pero si lo hacemos solo devolverá lo que pongamos entre paréntesis y terminará la ejecución.

> grande<-function(x,y){
+   y.g<-y>x #compara y con x y nos da un vector índice con TRUE y FALSE
+   return(y.g)
+ x[y.g]<-y[y.g] 
+ #En las posiciones de x en las que y era mayor que x (y.g = TRUE) 
+ #insertamos el valor de y en esa posición
+ x
+ }
> grande(1:5,c(1,6,2,7,3))
## [1] FALSE  TRUE FALSE  TRUE FALSE

Volviendo a la definición original de la función grande, una cosa que merece la pena preguntarse es si el orden en el que pasamos los argumentos es importante

> grande<-function(x,y){
+   y.g<-y>x #compara y con x y nos da un vector índice con TRUE y FALSE
+ x[y.g]<-y[y.g] 
+ #En las posiciones de x en las que y era mayor que x (y.g = TRUE) 
+ #insertamos el valor de y en esa posición
+ x
+ }
> grande(1:5,c(1,6,2,7,3))
## [1] 1 6 3 7 5
> grande(c(1,6,2,7,3),1:5)
## [1] 1 6 3 7 5

Vemos que en este caso no hay ninguna diferencia, pero veamos que pasa con esta otra función.

> divide <- function(x,y){
+   x/y
+ }
> 
> divide(4,2)
## [1] 2
> divide(2,4)
## [1] 0.5

Vemos que en este caso el orden de los argumentos si influye y ha llegado el momento de hablar de ellos, de los argumentos.

Argumentos

Como ya hemos comentado, los argumentos son los objetos y parámetros que usará la función para realizar la tarea para la que la hemos diseñado. Cada uno de ellos recibe un nombre que será el que adoptará en el transcurso de la función. En ese sentido es muy importante que.

  • pasemos los argumentos en el orden en el que se han definido en la función o
  • los pasemos usando su nombre.

fijate:

> divide(4,2)
## [1] 2
> divide(y = 2, x = 4)
## [1] 2

ahora ambos resultados son iguales porque la función asigna el nombre x al 4 e y al 2 y hace el cálculo x/y

Además, da igual cual fuese el nombre original del objeto que le pasamos, a partir del momento en el que entran en la función, su nombre será el del correspondiente argumento.

> x <- 2
> y <- 4
> divide(x=y,y=x)
## [1] 2

La correcta definición de los argumentos es fundamental para el correcto funcionamiento de la rutina pero a veces pueden ser muchos y puede que algunos ellos queramos que tengan valores preasignados para no tener que definirlos cada vez.

Argumentos por defecto

Muchas veces las funciones que creamos queremos que funcionen de una forma concreta pero, de vez en cuando nos interesa cambiar su comportamiento. Por ejemplo, cuando hablabamos de la función mean, normalente queremos la media de todos los valores pero a veces nos interesa calcular la media recortada. Definir siempre el argumento trim sería un poco rollo así que lo podemos definir como 0 (no recortamos ningún dato) por defecto y solo especificarlo si lo necesitamos.

Y ¿cómo podemos hacer eso con nuestra función? Muy sencillo, mira:

> grande<-function(x,y=0*x){
+ #¿Qué pasa si ponemos y = 0?
+ y.g<-y>x
+ x[y.g]<-y[y.g]
+ x
+ }
> grande(c(-12:3))
##  [1] 0 0 0 0 0 0 0 0 0 0 0 0 0 1 2 3
> grande(c(1,2),2:3)
## [1] 2 3

Aquí, en la función grande hemos definido la y como un vector de 0s de la misma longitud que el argumento x. Así, si no especifico el valor de y, la función comparará cada posición de x con 0 y pondrá el valor más grande.

Dentro de la especificación de argumentos merece la pena comentar que, hasta ahora, las funciones que hemos utilizado solo llamaban a operadores sencillos. Sin embargo puede que nos interese utilizar otras funciones dentro de la nuestra, funciones que tienen sus propios argumentos. Pero, ¿cómo podemos especificar esos argumentos desde nuestra función sin tener que especificar una ristra enorme de elementos?

El argumento “…”

El argumento “…” (tres puntos) nos permite incluir argumentos que utilizarán funciones que estén dentro de la definición de la nuestra.

Veamos un par de ejemplos:

> # Función que calcula la media de cualquier número de vectores
> media.total<-function(...) { 
+   mean(c(...)) 
+   }
> media.total(1:4,-pi:pi)
## [1] 0.8189865

En este caso hemos usado los puntos para poder pasar cualquier grupo de vectores que después concatenaremos como uno único dentro de la función mean.

> # Función ejemplo del argumento ...
> ejem.fun <- function(x, y, label = "la x", ...){
+   plot(x, y, xlab = label, ...) 
+   }
> ejem.fun(1:5, 1:5)
> ejem.fun(1:5, 1:5, col = "red")

La función ejem.fun por su parte, usa los puntos para poder pasarle a la función gráfica de R: plot (a la que volveremos en próximos episodios) argumentos que originalmente no habíamos definido como, por ejemplo, el color que queremos para la gráfica.

Pero ¿qué pasa si los argumentos que pasamos a la función nos son correctos? Volvamos un momento la función sumaobjetos y vamos a intentar pasarle dos objetos de diferente tamaño

> sumaobjetos(c(2,3),c(3,5,8))
## Warning in x + y: longer object length is not a multiple of shorter object
## length
## [1]  5  8 10

Vemos que la última orden nos da un mensaje de advertencia (Warning). Este mensaje se obtiene porque el operador suma lo contempla cuando los objetos que se suman tienen diferentes tamaños. Resulta interesante pensar si merecería la pena que lo hubiésemos controlado dentro de la función, antes de hacer la suma, impidiendo continuar con el cálculo. Esta acción puede realizarse con lo que llamamos funciones de control y parada, de las que hablamos a continuación.

Funciones de control y parada

En ninguno de los ejemplos anteriores hemos tenido en cuenta comprobar si los argumentos son los apropiados, algo que podría habernos llevado a errores del sistema. R nos permite utilizar funciones para controlar y parar el funcionamiento de una función.

  • Si al hacer estas comprobaciones detectamos un error que no es grave, podemos llamar a la función warning(“mensaje”) que nos muestra el mensaje de advertencia pero la ejecución de la función continua.
  • Si por el contrario detectamos un error grave podemos usar la función stop(“mensaje”) que nos mostrará el mensaje de error y deja de evaluar la función.
  • Además, podemos hacer uso de la función missing(argumento) que nos ofrece un valor lógico (TRUE o FALSE) para indicarnos si falta un argumento o no.

Para usar todas estas funciones podemos recurrir a bucles del tipo if que nos permiten detectar el error y ejecutar el critero de parada o arventencia necesario. Veamos algunos ejemplos:

En el caso de la función media.total podemos comprobar si los valores de cada objeto son numéricos y dar un error en caso contrario. Dicha comprobación la realizaremos con la función is.numeric(objeto) que nos devolverá un TRUE o un FALSE.

> media.total<-function(...){
+ for (x in list(...)){
+ if (!is.numeric(x)) stop("No son numeros")
+ }
+ mean(c(...))
+ }
> media.total("a",3)

Si lo ejecutáis veréis que da un error y nos dice que “No son números”

En el caso de la función grande podemos advertir al usuario que no ha especificado el valor y y que, por tanto, va a usarse la opción por defecto de comparar con 0.

> grande<-function(x,y=0*x){
+ if (missing(y)) warning("Estamos comparando con 0")
+ y.g<-y>x
+ x[y.g]<-y[y.g]
+ x
+ }
> grande(-3:3)
## Warning in grande(-3:3): Estamos comparando con 0
## [1] 0 0 0 0 1 2 3

Sin embargo, aunque lo comprobemos todo, siempre puede haber salidas que no tengan sentido o errores que no sepamos de donde vienen. Esto pasa, sobre todo, cuando las funciones tienen muchas lineas de código y es dificil comprobarlo todo. En ese caso podemos llevar a cabo un proceso que se conoce en ingles como debug. No hablaremos mucho de ello aquí pero dejadme que os dé algunas pinceladas.

Debugging

Cuando se produce una salida fuera de lo que esperábamos en una función que hemos programado, lo mejor es investigar dónde y cuándo ha ocurrido el error. Para mello existen varias posibilidades:

  • La función traceback() nos informa de la secuencia de llamadas antes del fallo de nuestra función. Es muy útil cuando se producen mensajes de error incomprensibles.
  • Con la función browser podemos interrumpir la ejecución a partir de ese punto, lo que nos permite seguir la ejecución o examinar el entorno.
    • Con “n” vamos paso a paso,
    • con cualquier otra tecla se sigue la ejecución normal.
    • “Q” para salir.
  • debug es como poner un browser al principio de la función, con lo que conseguimos ejecutar la función paso a paso. Se sale con “Q”.

Si quieres probar puedes ejectura la siguiente orden

> debug(grande)
> grande(1:5, 5:1)

Veras que al ejecutar estas ordenes se abre un environment vacío que solo tiene los valores de las variables que se van definiendo dentro de la función y si vamos dando a enter vemos como se va ejecutando poco a poco. Al terminar o al introducir Q se cierra la función y desaparece el environment.

Esto es algo también muy interesante y es que sucede con aquellos valores auxiliares que se van definiendo dentro de la función, como sucede en este caso con y.g. ¿Dónde quedan esos valores? ¿Por qué no aparecen en nuestro environment? ¿Podemos usar objetos de nuestro environment dentro de una fución?

Asignaciones dentro de las funciones

Para empezar, es importante mencionar que cualquier asignación realizada dentro de una función es local y temporal. Esto quiere decir que se ejecuta en un espacio de memoria propio de la funcióny se pierde tras salir de esta.

Vamos a verlo con un ejemplo

> #definimos res=0
> res <- 0
> 
> sumav<-function(x,y){ 
+   res <- x+y
+   res 
+ }
> 
> sumav(3,4)
## [1] 7
> #nos da un res de 7
> res
## [1] 0
> #el res del environment sigue valiendo 0

Observamos que la asignación res <- x+y dentro de la función no afecta al valor del argumento de la función en que se utiliza. Sin embargo, a veces puede resultar conveniente realizar asignaciones globales y permanentes dentro de una función. Para ello, utilizaremos el operador de “superasignación”, <<-, o la función assign.

> #definimos res=0
> res <- 0
> 
> sumav<-function(x,y){ 
+   res <<- x+y
+   res 
+ }
> 
> sumav(3,4)
## [1] 7
> #nos da un res de 7
> res
## [1] 7
> #Ahora sí, el res ha cambiado.

Con esto tenemos solucionado el sacar valores de la función fuera de la misma pero, ¿qué pasa si algo ya estaba en el environment y queremos usarlo dentro de la función?

Ambito o alcance de los objetos

Dentro de un programa o un lenguaje de programación, el ámbito o el alcance son las reglas que se utilizan para encontrar un valor requerido. En el caso de R, el mecanismo que utiliza R se denomina lexical scoping. Básicamente lo que hace es buscar en el environment de la función y, si no lo encuentra, buscar en el general (el que nosotros vemos) los valores de las variables o parámetros requeridos.

Veámoslo con un ejemplo. Para ello empieza limpiando tu environment pulsando sobre la escoba

> #Definimos una función
> verfun<-function(x) {
+ y<-2*x
+ print(x) 
+ print(y) 
+ print(z) 
+ }
> #Definimos una función
> verfun<-function(x) {
+ y<-2*x
+ print(x) 
+ print(y) 
+ print(z) 
+ }
> # Llamamos a la función
> verfun(8)

Si ejecutáis esta orden veréis que os da un error porque no encuentra z. Podemos definir z en el environment general:

> # Repetimos
> z<-3
> # Llamamos a la función
> verfun(8)
## [1] 8
## [1] 16
## [1] 3

Ahora si funciona.

Pero creemos otra función: m

> # Definimos una nueva función ver2fun 
> ver2fun<-function(x){
+ z<-10
+ cat("z dentro ver2fun vale",z,"\n"); verfun(z)
+ }
> # Llamamos a la función
> ver2fun(6)
## z dentro ver2fun vale 10 
## [1] 10
## [1] 20
## [1] 3
> #Dentro de verfun2
> z
## [1] 3

¿Qué ha pasado?

Resulta que dentro de ver2fun z vale 10 porque así lo hemos definido, pero este valor no se guarda en el environment general, donde z sigue valiendo 3.

El siguiente paso de la función es llamar a verfun() usando el valor z que acaba de definir y que, ahí sí, vale 10. Sin embargo, verfun tiene un único argumento que se llama x por lo que, al entrar dentro de esa función z ya no es z si no x y lo que tenemos es que x vale 10. verfun se ejecuta sobre ese valor y llega un momento en que necesita una z que ella no tiene ¿dónde la busca? pues en el environment general donde esa z sigue valiendo 3… y de ahí el resultado.

Por supuesto, ahora toca practicar mucho y darse de bruces muchas veces contra la pared… pero estoy segura de que ¡tu puedes! Espero que te haya gustado y nos vemos en la próxima.

Tenéis un script con todo el código en este enlace

Nota: Para más información sobre el alcance podéis consultar:

Indice del curso 

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.

A %d blogueros les gusta esto: