Funciones
Última actualización: 2024-10-16 | Mejora esta página
Tiempo estimado: 60 minutos
Hoja de ruta
Preguntas
- ¿Cómo puedo escribir una nueva función en R?
Objetivos
- Definir una función que usa argumentos.
- Obtener un valor a partir de una función.
- Revisar argumentos con
stopifnot()
en las funciones. - Probar una función.
- Establecer valores por defecto para los argumentos de las funciones.
- Explicar por qué debemos dividir programas en funciones pequeñas y de propósitos únicos.
Palabras clave
Comando : Traducción
stopifnot
: para si no
NULL
: nulo
paste
: pegar
TRUE
: verdadero
FALSE
: falso
source
: fuente
Si tuviéramos un único conjunto de datos para analizar, probablemente
sería más rápido cargar el archivo en una hoja de cálculo y usarla para
graficar estadísticas simples. Sin embargo, los datos
gapminder
son actualizados periódicamente, y podríamos
querer volver a bajar esta información actualizada más adelante y
re-analizar los datos. También podríamos obtener datos similares de una
fuente distinta en el futuro.
En esta lección, aprenderás cómo escribir una función de forma que seamos capaces de repetir varias operaciones con un comando único.
¿Qué es una función?
Las funciones reúnen una secuencia de operaciones como un todo, almacenandola para su uso continuo. Las funciones proveen:
- un nombre que podemos recordar y usar para invocarla
- una solución para la necesidad de recordar operaciones individuales
- un conjunto definido de inputs y outputs esperados
- una mayor conexión con el ambiente de programación
Como el componente básico de la mayoría de los lenguajes de programación, las funciones definidas por el usuario constituyen la “programación” de cualquier abstracción que puedas hacer. Si has escrito una función, eres ya todo un programador.
Definiendo una función
Empecemos abriendo un nuevo script de R en el
directorio functions/
y nombrémosle functions-lesson.R.
R
my_sum <- function(a, b) {
the_sum <- a + b
return(the_sum)
}
Definamos una función fahr_a_kelvin()
que convierta
temperaturas de Fahrenheit a Kelvin:
R
fahr_to_kelvin <- function(temp) {
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
Definimos fahr_a_kelvin()
asignándola al
output de function
. La lista de los
nombres de los argumentos se encuentran entre paréntesis. Luego, el cuerpo de la función–los
comandos que son ejecutados cuando se corre–se encuentran entre
paréntesis curvos ({}
). Los comandos en el cuerpo se
indentan con dos espacios. Esto hace que el código sea legible sin
afectar su funcionalidad.
Cuando utilizamos la función, los valores que definimos como argumentos se asignan a esas variables para que podamos usarlos dentro de la función. Dentro de la función, usamos un return statement para devolver un resultado a quien lo solicitó.
Sugerencia
Una característica única de R es que el return statement no es necesario. R automáticamente devuelve cualquier variable que esté en la última línea del cuerpo de la función. Pero por claridad, nosotros explícitamente definiremos el return statement.
Tratemos de correr nuestra función. Llamamos nuestra propia función de la misma manera que llamamos cualquier otra:
R
# Punto de congelación del agua
fahr_to_kelvin(32)
SALIDA
[1] 273.15
R
# Punto de ebullición del agua
fahr_to_kelvin(212)
SALIDA
[1] 373.15
Desafío 1
Escribe una función llamada kelvin_a_celsius()
que toma
la temperatura en grados Kelvin y devuelve la temperatura en
Celsius.
Pista: Para convertir de Kelvin a Celsius se debe restar 273.15
Escribe una función llamada kelvin_a_celsius()
que toma
la temperatura en grados Kelvin y devuelve la temperatura en
Celsius.
R
kelvin_to_celsius <- function(temp) {
celsius <- temp - 273.15
return(celsius)
}
Combinando funciones
El poder real de las funciones proviene de mezclarlas y combinarlas en pedazos de código aún mas grandes para lograr el resultado que buscamos.
Definamos dos funciones que convertirán la temperatura de Fahrenheit a Kelvin, y de Kelvin a Celsius:
R
fahr_to_kelvin <- function(temp) {
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
kelvin_to_celsius <- function(temp) {
celsius <- temp - 273.15
return(celsius)
}
Desafío 2
Define la función para convertir directamente de Fahrenheit a Celsius, reutilizando las dos funciones de arriba (o utilizando tus propias funciones si lo prefieres).
Define la función para convertir directamente de Fahrenheit a Celsius, reutilizando las dos funciones de arriba.
R
fahr_to_celsius <- function(temp) {
temp_k <- fahr_to_kelvin(temp)
result <- kelvin_to_celsius(temp_k)
return(result)
}
Interludio: Programación defensiva
Ahora que hemos empezado a apreciar cómo las funciones proporcionan
una manera eficiente de hacer que el código R sea reutilizable y
modular, debemos tener en cuenta que es importante garantizar que las
funciones solo funcionen en los casos de uso previstos. Revisar los
parámetros de las funciones está relacionado con el concepto de
programación defensiva. La programación defensiva nos alienta a
probar las condiciones frecuentemente y arrojar un error si algo está
mal. Estas pruebas se conocen como assertion statements
porque queremos asegurarnos de que una determinada condición es
TRUE
antes de proceder. Esto facilita la depuración porque
nos dan una mejor idea de dónde se originan los errores.
Probando condiciones con stopifnot()
Empecemos por re-examinar fahr_a_kelvin()
, nuestra
función para convertir temperaturas de Fahrenheit a Kelvin. Estaba
definida de la siguiente manera:
R
fahr_to_kelvin <- function(temp) {
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
Para que esta función trabaje como se desea, el argumento
temp
debe ser un valor numeric
; de lo
contrario, el procedimiento matemático para convertir entre las dos
escalas de temperatura no funcionará. Para crear un error, podemos usar
la función stop()
. Por ejemplo, dado que el argumento
temp
debe ser un vector numeric
, podríamos
probarlo con un condicional if
y devolver un error si la
condición no se cumple. Podríamos agregar esto a nuestra función de la
siguiente manera:
R
fahr_to_kelvin <- function(temp) {
if (!is.numeric(temp)) {
stop("temp must be a numeric vector.")
}
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
Si tuviéramos muchas condiciones o argumentos para revisar, podría
llevar muchas líneas de código probarlas todas. Afortunadamente R provee
la función de conveniencia stopifnot()
. Podemos listar
todos los requerimientos que deben ser evaluados como TRUE
;
stopifnot()
arroja un error si encuentra uno que sea
FALSE
. Listar estas condiciones tiene como objetivo
secundario el generar documentación extra para la función.
Hagamos la programación defensiva con stopifnot()
agregando aseveraciones para probar el input a nuestra
función fahr_a_kelvin()
.
Queremos asegurar lo siguiente: temp
es un vector
numérico. Lo podemos hacer de la siguiente manera:
R
fahr_to_kelvin <- function(temp) {
stopifnot(is.numeric(temp))
kelvin <- ((temp - 32) * (5 / 9)) + 273.15
return(kelvin)
}
Aún funciona si se le da un input adecuado.
R
# Punto de congelación del agua
fahr_to_kelvin(temp = 32)
SALIDA
[1] 273.15
Pero falla instantáneamente si se le da un input inapropiado.
R
# La métrica es un factor en lugar de numeric
fahr_to_kelvin(temp = as.factor(32))
ERROR
Error in fahr_to_kelvin(temp = as.factor(32)): is.numeric(temp) is not TRUE
Desafío 3
Usar programación defensiva para asegurar que nuestra función
fahr_a_celsius()
arroja inmediatamente un error si el
argumento temp
se especifica inadecuadamente.
Extender la definición previa de nuestra función agregándole una
llamada explícita a stopifnot()
. Dado que
fahr_a_celsius()
es una composición de otras dos funciones,
hacer pruebas a la función hace redundante el agregar pruebas a cada una
de las dos funciones que la componen.
R
fahr_to_celsius <- function(temp) {
stopifnot(!is.numeric(temp))
temp_k <- fahr_to_kelvin(temp)
result <- kelvin_to_celsius(temp_k)
return(result)
}
Más sobre combinar funciones
Ahora vamos a definir una función que calcula el Producto Interno Bruto (“GDP” en la base de datos, por sus siglas en inglés Gross Domestic Product) de un país a partir de los datos disponibles en nuestro conjunto de datos:
R
# Toma un dataset y multiplica la columna de población
# por la columna de GDP per capita
calcGDP <- function(dat) {
gdp <- dat$pop * dat$gdpPercap
return(gdp)
}
Definimos calcGDP()
asignándola al
output de function
. La lista de los
nombres de los argumentos se encuentran entre paréntesis. Luego, el
cuerpo de la función–las instrucciones que se ejecutan cuando se llama a
la función– se encuentran entre llaves ({}
).
Hemos indentado los comandos en el cuerpo con dos espacios. Esto hace que el código sea mas fácil de leer sin afectar su funcionamiento.
Cuando utilizamos la función, los valores que le pasamos se asignan como argumentos, que se convierten en variables dentro del cuerpo de la función.
Dentro de la función, usamos la función return()
para
obtener el resultado.
Esta función return()
es opcional: R automáticamente
devolverá el resultado de cualquier comando que se ejecute en la última
línea de la función.
R
calcGDP(head(gapminder))
SALIDA
[1] 6567086330 7585448670 8758855797 9648014150 9678553274 11697659231
Eso no es muy informativo. Agreguemos algunos argumentos más para poder extraer por año y país.
R
# Toma un dataset y multiplica la columna de población
# por la columna de GDP per capita
calcGDP <- function(dat, year=NULL, country=NULL) {
if(!is.null(year)) {
dat <- dat[dat$year %in% year, ]
}
if (!is.null(country)) {
dat <- dat[dat$country %in% country,]
}
gdp <- dat$pop * dat$gdpPercap
new <- cbind(dat, gdp=gdp)
return(new)
}
Si has estado escribiendo estas funciones en un
script de R aparte (¡una buena idea!), puedes cargar
las funciones en nuestra sesión de R usando la función
source()
:
R
source("functions/functions-lesson.R")
Ok, entonces están pasando muchas cosas en esta función ahora. En pocas palabras, ahora la función filtra un subconjunto de datos por año si el argumento año no está vacío, luego filtra un subconjunto de los resultados por país si el argumento país no está vacío. Luego calcula el GDP de los datos filtrados resultado de los dos pasos anteriores. La función luego agrega el valor calculado de GDP como una nueva columna en los datos filtrados y devuelve esto como el resultado final. Puedes ver que el output es mucho más informativo que un vector numérico.
Veamos qué sucede cuando especificamos el año:
R
head(calcGDP(gapminder, year=2007))
SALIDA
country year pop continent lifeExp gdpPercap gdp
12 Afghanistan 2007 31889923 Asia 43.828 974.5803 31079291949
24 Albania 2007 3600523 Europe 76.423 5937.0295 21376411360
36 Algeria 2007 33333216 Africa 72.301 6223.3675 207444851958
48 Angola 2007 12420476 Africa 42.731 4797.2313 59583895818
60 Argentina 2007 40301927 Americas 75.320 12779.3796 515033625357
72 Australia 2007 20434176 Oceania 81.235 34435.3674 703658358894
O para un país específico:
R
calcGDP(gapminder, country="Australia")
SALIDA
country year pop continent lifeExp gdpPercap gdp
61 Australia 1952 8691212 Oceania 69.120 10039.60 87256254102
62 Australia 1957 9712569 Oceania 70.330 10949.65 106349227169
63 Australia 1962 10794968 Oceania 70.930 12217.23 131884573002
64 Australia 1967 11872264 Oceania 71.100 14526.12 172457986742
65 Australia 1972 13177000 Oceania 71.930 16788.63 221223770658
66 Australia 1977 14074100 Oceania 73.490 18334.20 258037329175
67 Australia 1982 15184200 Oceania 74.740 19477.01 295742804309
68 Australia 1987 16257249 Oceania 76.320 21888.89 355853119294
69 Australia 1992 17481977 Oceania 77.560 23424.77 409511234952
70 Australia 1997 18565243 Oceania 78.830 26997.94 501223252921
71 Australia 2002 19546792 Oceania 80.370 30687.75 599847158654
72 Australia 2007 20434176 Oceania 81.235 34435.37 703658358894
O ambos:
R
calcGDP(gapminder, year=2007, country="Australia")
SALIDA
country year pop continent lifeExp gdpPercap gdp
72 Australia 2007 20434176 Oceania 81.235 34435.37 703658358894
Veamos paso a paso el cuerpo de la función:
Aquí hemos agregado dos argumentos, año y país. Hemos establecido argumentos predeterminados para ambos como NULL usando el operador = en la definición de la función. Esto significa que esos argumentos tomarán esos valores a menos que el usuario especifique lo contrario.
R
if(!is.null(year)) {
dat <- dat[dat$year %in% year, ]
}
if (!is.null(country)) {
dat <- dat[dat$country %in% country,]
}
Aquí verificamos si cada argumento adicional se define como
null
, y cuando no sea null
se sobreescriben
los datos almacenados en dat
por un subconjunto de datos
determinados por el argumento not-null
.
Hacemos esto para que nuestra función sea más flexible para más adelante. Podemos pedirle que calcule el GDP para:
- El dataset completo;
- Un solo año;
- Un solo país;
- Una combinación única de año y país.
Al utilizar el operador %in%
, también podemos asignarle
múltiples años o países a estos argumentos.
Sugerencia: Pasar por valor
Las funciones en R casi siempre hacen copias de los datos para operar
dentro del cuerpo de una función. Cuando modificamos dat
dentro de la función estamos modificando la copia del
dataset gapminder almacenado en dat
, y no
la variable original que asignamos como el primer argumento.
Eso se llama ****pasar por valor**** y hace la escritura del código mucho más segura: puedes estar seguro que cualquier cambio que hagas dentro del cuerpo de la función, se mantendrá dentro de la función.
Sugerencia: Alcance de la función
Otro concepto importante es el alcance: las variables (¡o funciones!)
que creas o modificas dentro del cuerpo de una función sólo existen
durante el tiempo de ejecución de la función. Cuando llamamos
calcGDP()
, las variables dat
, gdp
y new
sólo existen dentro del cuerpo de la función.
Incluso, si tenemos variables con el mismo nombre en nuestra sesión
interactiva de R, éstas no son modificadas en ninguna manera cuando se
ejecuta la función.
Finalmente, calculamos GDP en nuestro nuevo subconjunto de datos, y creamos una nueva dataframe con esta columna agregada. Esto significa que cuando llamamos a la función, en el resultado podemos ver el contexto de los valores GDP obtenidos, lo que es mucho mejor que nuestro primer intento cuando habíamos obtenido un vector de números.
Desafío 3
Probar tu función GDP calculando el GDP para Nueva Zelandia (“New Zealand”) en 1987. ¿Cómo difiere del GDP de Nueva Zelandia en 1952?
R
calcGDP(gapminder, year = c(1952, 1987), country = "New Zealand")
GDP para Nueva Zelandia en 1987: 65050008703
GDP para Nueva Zelandia en 1952: 21058193787
Desafío 4
La función paste()
puede ser usada para combinar texto,
ej.:
R
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
paste(best_practice, collapse=" ")
SALIDA
[1] "Write programs for people not computers"
Escribir una función fence()
que tome dos vectores como
argumentos, llamados text
y wrapper
, y muestra
el texto flanqueado del wrapper
:
R
fence(text=best_practice, wrapper="***")
Nota: la función paste()
tiene un argumento
llamado sep
, que especifica el separador de texto. Por
defecto es un espacio: ” “. El valor por defecto de la función
paste0()
es sin espacio”“.
Escribir una función fence()
que toma dos vectores como
argumentos, llamados text
y wrapper
, e imprime
el texto flanqueado del wrapper
:
R
fence <- function(text, wrapper){
text <- c(wrapper, text, wrapper)
result <- paste(text, collapse = " ")
return(result)
}
best_practice <- c("Write", "programs", "for", "people", "not", "computers")
fence(text=best_practice, wrapper="***")
SALIDA
[1] "*** Write programs for people not computers ***"
Sugerencia
R tiene algunos aspectos únicos que pueden ser explotados cuando se realizan operaciones más complicadas. No escribiremos nada que requiera el conocimiento de estos conceptos más avanzados. En el futuro, cuando te sientas cómodo escribiendo funciones en R, puedes aprender más leyendo el Manual de lenguaje de R o este capítulo de Advanced R Programming de Hadley Wickham.
Sugerencia: Probar y documentar
Es importante probar las funciones así como documentarlas: la
documentación ayuda, tanto a tí como a otros, a entender cuál es el
propósito de la función y cómo usarla; además es importante
asegurarse que la función realmente haga lo que tú piensas que hace.
Cuando recién comiences, tu flujo de trabajo probablemente luzca algo así:
- Escribir una función
- Comentar partes de la función para documentar su comportamiento
- Cargar el archivo source
- Experimentar con ella en la consola para asegurarte que se comporta tal como tu esperas.
- Hacer los arreglos necesarios
- Volver a probar y repetir.
La documentación formal para las funciones, escritas en archivos
.Rd
aparte, se transforman en la documentación que ves en
los archivos de ayuda. El paquete roxygen2
le permite a los programadores de R escribir la documentación junto con
el código, y luego procesarlo para generar los archivos .Rd
apropiados. Quizás quieras cambiarte a este método más formal de
escribir la documentación cuando empieces a escribir proyectos de R más
complicados.
Pruebas automatizadas formales se pueden escribir usando el paquete testthat.
Puntos Clave
- Usar
function
para definir una nueva función en R. - Usar parámetros para ingresar valores dentro de las funciones.
- Usar
stopifnot()
para revisar los argumentos de una función en R de manera flexible. - Cargar funciones dentro de programas empleando
source()
.