-
Notifications
You must be signed in to change notification settings - Fork 2
/
Copy path07_funciones.qmd
482 lines (334 loc) · 17.7 KB
/
07_funciones.qmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
---
title: "Funciones"
---
## Objetivos de aprendizaje
- Convertir un bloque de código R en una función
- Utilizar funciones de otros paquetes en una función de su propio paquete
- Devolver un mensaje de error desde una función
## Trabajando con funciones y otro código
Hasta ahora tenemos un proyecto con una determinada estructura de carpetas que nos permite ordenar nuestro trabajo. Inicialmente escribir el código necesario para resolver un problema, leer un archivo o calcular medidas estadísticas es lo más razonable. Pero es posible que te encuentres repitiendo las mismas lineas de código o copiado y pegando código de un lado para el otro porque necesitás reutilizar algo que ya escribíste.
En estas situaciones es una buena idea comenzar a encapsular código en funciones. En esta sección veremos como escribir funciones, más adelante *empaquetaremos* esas funciones en un paquete.
Escribir funciones tiene cuatro grandes ventajas sobre copiar y pegar código:
* Podés nombrar tus funciones con un nombre descriptivo que facilite la comprensión del código.
* Si por alguna razón tenés que cambiar el código, sólo es necesitás hacerlo en un único lugar.
* Eliminás la posibilidad de cometer errores al copiar y pegar código.
* Podés reutilizar el código en las funciones en otros proyectos.
En las situaciones donde nuestro código no produce salidas como graficos o tablas, más bien son definiciones de funciones secundarias u otras herramientras, no tiene tanto sentido usar archivos .Rmd o .qmd. En estos casos podemos volver a los tradicionales scripts (.R).
## Esqueleto de una función
Cualquier función en R tendrá la siguiente pinta (se le llama firma de una función):
```r
nombre_de_funcion <- function(arg1, arg2, ...) {
# código que hace algo
}
```
Como habrás notado necesitamos la función `function()` para crear una función. Hay tres pasos clave para crear una nueva función:
* Tenés que elegir un nombre para la función.
* Enumeras los argumentos, los elementos de entrada que necesita el código para correr, en el ejemplo `arg1`, `arg2`, `...`.
* Colocas el código que has desarrollado en el cuerpo de la función, un bloque entre `{}` inmediatamente después de `function(...)`.
Imaginemos que tenemos este código:
```{r}
temperatura_ayer <- 53.6 # en Fahrenheit!
temperatura_ayer_c <- (temperatura_ayer - 32) * 5/9 # convierto a centigrados
```
De nuevo, este código no va a ser útil si querés usarlo en otro lugares, necesitamos generalizarlo en una función.
El primer paso es pensar un nombre, por ejemplo `fahrenheit_a_centigrados`. Luego tenemos que analizar el código e identificar cuales son los argumentos, la información que necesita la función, en este caso la temperatura en Fahrenheit, es es un argumento.
```{r}
fahrenheit_a_centigrados <- function(temperatura_ayer) {
(temperatura_ayer - 32) * 5/9
}
```
Está función está lista para usar, por ejemplo podemos convertir 100 Fahrenheit a centigrados:
```{r}
fahrenheit_a_centigrados(100)
```
Si bien podemos nombrar a los argumentos de las funciones de cualquier manera, `temperatura_ayer` no es razonable para una función general. Un mejor nombre podría ser `temperatura_fahrenheit`
```{r}
fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
(temperatura_fahrenheit - 32) * 5/9
}
```
Es posible que te encuentres con algo del estilo:
```{r}
fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
temperatura_centigrados <- (temperatura_fahrenheit - 32) * 5/9
return(temperatura_centigrados)
}
```
Usar la función `return()` no es necesario, R devuelve el último elemento con o sin el `return()` presente. Sin embargo, puede ayudar a la lectura del código cuando la función es más compleja.
::: importante
Cada vez que se ejecuta una función se crea un nuevo entorno, desde cero, propio de la función para contener su ejecución. Notá que si buscas la variable `temperatura_centigrados` en el _Environment_ no la vas a encontrar. Eso es así porque todo lo que ocurre adentro de la función, se queda adentro de la función. El código corre en ese "entorno" independiente del entorno general.
:::
Aquí podemos hacer un paréntesis para mencionar la necesidad de documentar apropiadamente cualquier función o código que generemos. Para una función deberíamos incluir:
- Qué hace o cual es su propósito.
- Qué argumentos requiere y de que tipo de datos son.
- Qué genera cómo resultado.
::: ejercicio
1. Creá un archivo .R con las siguientes funciones:
* Una función que convierta la temperatura de centigrados a fahrenheit.
* Una función utilizando el siguiente código `mean(is.na(x))`. ¿Qué resultado da este código? Probalo con `x <- c(0, 1, 2, NA, 4, NA)`.
* Una función utilizando el siguiente código `x / sum(x, na.rm = TRUE)`. ¿Qué resultado da este código? Probalo con `x <- c(1:5)`.
2. Guardá el archivo .R con un nombre informativo.
3. Al comienzo del archivo, en comentarios, antes de cada función, incluí que hace la función, que argumentos requiere y que genera.
:::
Ahora si quisieramos usar esa función en el análisis, podemos "cargarla" con `source("archivo.R")`.
La escritura de una función debería arrancar con un código que ya funciona. Es decir, en lugar de
empezar desde cero `fahrenheit_a_centigrados()` usamos el código que ya tenemos y que funciona para un ejemplo particular. Luego cambiando el nombre del argumento podemos generalizar el código.
## Testeá tu función
Es importante que pruebes tu función de distintas maneras. Primero, con algún valor para el que sabes el resultado, por ejemplo 32 fahrenheit es 0º centigrados.
```{r}
fahrenheit_a_centigrados(32)
```
También es importante testear la función con datos diferentes, por ejemplo en vez de 1 número entero, podemos usar un real o un vector de números.
```{r}
fahrenheit_a_centigrados(25.5)
```
```{r}
fahrenheit_a_centigrados(c(0, 32, 100))
```
Hay que chequear si estos resultados son correctos. ¿Esperabas obtener 3 valores en el segundo último ejemplo? ¿tiene sentido que haga esto?
Finalmente, tenemos que probar la función con otras cosas.
```{r}
#| error: true
fahrenheit_a_centigrados("100")
```
```{r}
#| error: true
fahrenheit_a_centigrados(TRUE)
```
El primero ejemplo da un error, poco informativo pero un error al fin. El segundo ejemplo devuelve un resultado, pero ¿no debería dar error?
Por estas situaciones es importante chequear que lo que ingresa a la función es lo esperado, antes de hacer ninguna otra operación.
### Revisá que los argumentos sean válidos
El primer acercamiento a esto es la función `stopifnot()`
```{r}
fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
stopifnot(is.numeric(temperatura_fahrenheit))
(temperatura_fahrenheit - 32) * 5/9
}
```
Ahora si volvemos a correr algunos de los ejemplos previos, obtendremos:
```{r}
#| error: true
fahrenheit_a_centigrados("100")
```
Pero de nuevo, esta función no devuelve un mensaje muy informativo.
::: importante
Para lo que sigue vamos a necesitar usar esquemas de flujo, `if`, `else`, etc.
Revisemos como es la sintaxis en R.
```
if (condición) {
# código que se ejecuta cuando la condición es TRUE
} else {
# código que se ejecuta cuando la condición es FALSE
}
```
En R la condición que evaluamos va entre `()` y usamos `{}` para separar cada *rama* de nuestro código, no es necesario indentar las líneas de código (aunque ayuda en la lectura!).
Además de `if` y `else`, podemos usar `else if` cuando queremos probar distintas condiciones.
:::
La siguiente solución implica encapsular la función `stop()` en una estructura `if` para poder incluir un mensaje de error apropiado:
```{r}
fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
if (!is.numeric(temperatura_fahrenheit)) {
stop("temperatura_centigrados debe ser numérico,\n",
"La variable ingresada es un ", class(temperatura_fahrenheit)[1])
}
(temperatura_centigrados - 32) * 5/9
}
```
```{r}
#| error: true
fahrenheit_a_centigrados("100")
```
Este mensaje nos da mucha más información:
* En que función ocurre el error
* La causa del error y como resolverlo.
Pero hay otras soluciones aún más superadoras, podemos escribir mensajes de error y _warnings_ (advertencias en inglés) usando el paquete `cli`:
```{r}
fahrenheit_a_centigrados <- function(temperatura_fahrenheit) {
if (!is.numeric(temperatura_fahrenheit)) {
cli::cli_abort(c(
"temperatura_fahrenheit debe ser numérico.",
"i" = "La variable ingresada es un {class(temperatura_fahrenheit)[1]}."
))
}
(temperatura_fahrenheit - 32) * 5/9
}
```
```{r}
#| error: true
fahrenheit_a_centigrados("100")
```
El mensaje de error se ve mejor y nos permite organizar la información. En este caso usamos `cli_abort()` pero hay toda una familia de funciones según la circusntancia, por ejemplo si queremos mostrar un _warning_, si la función corrió con exito, etc.
Además podemos mostrar distintos tipos de mensajes:
```{r}
cli::cli_bullets(c(
"noindent",
" " = "indent",
"*" = "bullet",
">" = "arrow",
"v" = "success",
"x" = "danger",
"!" = "warning",
"i" = "info"
))
```
::: ejercicio
Escribí una función para descargar y leer los datos de pingüinos.
1. La función debe aceptar un argumento, la ruta al archivo en tu computadora.
2. Revisá si el archivo ya existe en esa ruta, usá la función `file.exist()`.
* Si el archivo no está descargado, el código debe descargarlo y luego leerlo.
* Si el archivo ya está descargado, el código debe leerlo.
3. Agrega mensajes con `cli_inform()` para que el usuario sepa lo que la función hizo.
```{r eval=FALSE, include=FALSE}
descarga_pinguinos <- function(ruta_archivo) {
file <- here::here(ruta_archivo)
url <- "https://zenodo.org/records/12772944/files/pinguinos.csv?download=1"
download.file(url, file)
}
datos_pinguinos <- function(ruta_archivo) {
file <- here::here(ruta_archivo)
if (!file.exists(file)) {
descarga_pinguinos(ruta_archivo)
cli::cli_inform(c("v" = "Descargado el archivo en {ruta_archivo}"))
}
cli::cli_inform(c("v" = "Leyendo los datos"))
return(read_csv(file))
}
penguins <- datos_pinguinos("datos/datos_pinguinos.csv")
```
:::
## Escribí funciones para humanos y computadoras
Es importante recordar que las funciones no son sólo para que las entienda R, sino también para las personas que las van a usar. A R no le importa cómo se llama tu función, o qué comentarios contiene, pero éstos son importantes para que vos y otras personas entiendan lo que hace.
El nombre de una función es importante. Lo ideal es que el nombre de tu función sea corto, pero que describa claramente lo que hace la función. Eso es difícil. Pero es mejor que el nombre sea claro a que sea muy corto. También que ayude a RStudio a autocompletar.
Generalmente, los nombres de las funciones son verbos, y los argumentos sustantivos.Esto es porque las funciones **hacen** algo, tienen una acción asociada. Por supuesto, hay excepciones a la regla. Los sustantivos como nombre de funciones están bien si la función calcula un sustantivo muy conocido (por ejemplo, `promedio()` es mejor que `calcula_promedio()`), o accede a alguna propiedad de un objeto (por ejemplo, `coef()` es mejor que `extraer_coeficientes()`). Una buena señal de que un sustantivo puede ser una mejor opción es si se utiliza un verbo muy amplio como «obtener», «computar», «calcular» o «determinar». Usa tu mejor criterio y no tengas miedo de cambiar el nombre de una función si más adelante se te ocurre uno mejor.
**Muy corto**
* `f()`
**No es un verbo y no es descriptivo**
* `funcion_hermosa()`
**Nombre largo pero claro**
* `computa_faltantes()`
* `colapsa_anios()`
Si el nombre de tu función está compuesto por varias palabras, te recomendamos utlizar «snake_case», donde cada palabra en minúscula está separada por un guión bajo. camelCase es otra buena alternativa (es mas amigable con lectores de pantallas). Lo importante es ser coherente: elegí una y no cambies.
Para ayudar a RStudio a autocompletar el nombre de las funciones es mejor esto:
* `input_select()`
* `input_checkbox()`
* `input_text()`
que esto:
* `select_input()`
* `checkbox_input()`
* `text_input()`
### Construyendo un paquete de R paso a paso {#ex-funciones}
::: ejercicio
1. Crea una función para descagar y leer los datos de estaciones. Importante:
* Usá como base la función que creamos para leer los datos de pinguinos.
* La función deberá recibir **2 argumentos**, el id de la estación (por ejemplo "NH0437") y la ruta donde se guardará el archivo (por ejemplo "datos/NH0437.csv")
* Debe poder descargar y leer los datos de cualquier estación.
```{r eval=FALSE, include=FALSE, message=FALSE, warning=FALSE}
descarga_estacion <- function(id, ruta_archivo) {
file <- here::here(ruta_archivo)
url <- paste0("https://raw.githubusercontent.com/rse-r/intro-programacion/main/datos/", id, ".csv")
download.file(url, file)
}
datos_estacion <- function(id, ruta_archivo) {
file <- here::here(ruta_archivo)
if (!file.exists(file)) {
descarga_estacion(id, ruta_archivo)
cli::cli_inform(c("v" = "Descargado la estación {id} en {ruta_archivo}"))
}
cli::cli_inform(c("v" = "Leyendo los datos"))
return(readr::read_csv(file))
}
estacion_NH0437 <- datos_estacion("NH0437", "datos/NH0437.csv")
```
2. Crea una función que se llame `tabla_resumen_temperatura` y que devuelva una tabla de resumen de la `temperatura_abrigo_150cm` para una o más estaciones. Usá el código que generaste en el ejercicio 1 de tidyr.
```{r eval=FALSE, include=FALSE, message=FALSE, warning=FALSE}
library(dplyr)
library(tidyr)
estaciones_santafe <- c("NH0472", "NH0910", "NH0046", "NH0098", "NH0437")
santa_fe <- purrr::map_df(here::here(paste0("datos/", estaciones_santafe, ".csv")), readr::read_csv)
# A este nivel hardcodeamos la variable temperatura
tabla_resumen_temperatura <- function(datos, variable) {
# TODO: chequear que datos sea un data.frame/tibble y que tenga la variable temperatura_abrigo_150cm
datos |>
group_by(id) |>
summarise(media = mean(temperatura_abrigo_150cm, na.rm = TRUE),
desvio = sd(temperatura_abrigo_150cm, na.rm = TRUE)) |>
pivot_longer(cols = c("media", "desvio"), names_to = "variable", values_to = "valor") |>
pivot_wider(names_from = id, values_from = valor)
}
tabla_resumen_temperatura(santa_fe)
# Más adelate capaz llegamos a tidy evaluation para generalizar la función
# https://dplyr.tidyverse.org/articles/programming.html
tabla_resumen <- function(datos, variable) {
datos |>
group_by(id) |>
summarise(media = mean({{ variable }}, na.rm = TRUE),
desvio = sd({{ variable }}, na.rm = TRUE)) |>
pivot_longer(cols = c("media", "desvio"), names_to = "variable", values_to = "valor") |>
pivot_wider(names_from = id, values_from = valor)
}
tabla_resumen(santa_fe, temperatura_abrigo_150cm_minima)
```
3. Generá una función `grafico_temperatura_mensual` que devuelva un gráfico que muestre el promedio mensual de la temperatura de abrigo. usá el código que generaste para el ejercicio 2 de ggplot2. La función debe:
* Recibir el data.frame con los datos (que pueden ser de 1 o más estaciones).
* Tener un argumento para indicarle que colores debe usar para el gráfico.
* Tener un argumento para definir el título del gráfico. Por defecto, si no se define el título deberá aparecer "Temperatura"
Desafio extra (opcional): modificá la función para que si no le pasas los colores necesarios, elija colores de manera aleatoria. Pista: revisá `colors()`
```{r eval=FALSE, include=FALSE, message=FALSE, warning=FALSE}
library(ggplot2)
grafico_temperatura_mensual <- function(datos, colores, titulo = "Temperatura") {
datos |>
group_by(id, mes = lubridate::month(fecha)) |>
summarise(temperatura_media = mean(temperatura_abrigo_150cm, na.rm = TRUE)) |>
ggplot(aes(mes, temperatura_media)) +
geom_line(aes(color = id)) +
scale_color_manual(values = colores) +
labs(title = titulo,
x = "Mes",
y = "Temperatura media",
color = "Estación") +
theme_minimal()
}
grafico_temperatura_mensual(santa_fe,
colores = c("darkorange", "purple", "cyan4", "steelblue", "pink3"),
titulo = "Ciclo anual de temperatura")
# Mismo problema de tidy evaluation
grafico_mensual <- function(datos, variable, colores, titulo = "Temperatura") {
datos |>
group_by(id, mes = lubridate::month(fecha)) |>
summarise(promedio = mean({{ variable }}, na.rm = TRUE)) |>
ggplot(aes(mes, promedio)) +
geom_line(aes(color = id)) +
scale_color_manual(values = colores) +
labs(title = titulo,
x = "Mes",
y = "Temperatura media",
color = "Estación") +
theme_minimal()
}
grafico_mensual(santa_fe,
temperatura_abrigo_150cm_minima,
colores = c("darkorange", "purple", "cyan4", "steelblue", "pink3"),
titulo = "Ciclo anual de temperatura minima")
# Generalizando el uso de colores
grafico_mensual <- function(datos, variable, colores = NULL, titulo = "Temperatura") {
if (is.null(colores)) {
n <- nrow(distinct(datos, id))
colores <- sample(colors(), n)
}
datos |>
group_by(id, mes = lubridate::month(fecha)) |>
summarise(promedio = mean({{ variable }}, na.rm = TRUE)) |>
ggplot(aes(mes, promedio)) +
geom_line(aes(color = id)) +
scale_color_manual(values = colores) +
labs(title = titulo,
x = "Mes",
y = "Temperatura media",
color = "Estación") +
theme_minimal()
}
grafico_mensual(santa_fe,
temperatura_abrigo_150cm_minima,
titulo = "Ciclo anual de temperatura minima")
```
:::