Purrr-fecting your R workflow

An introduction to functional programming
R Day Colombia

Who am I?

Leading the translation of R4DS to Portuguese

Inspired by the Spanish version: https://es.r4ds.hadley.nz/

https://cienciadedatos.github.io/pt-r4ds/

Plan for today

  • What is functional programming?

  • What is the purrr package?

  • What are my favorite purrr functions?

  • Example of how purrr helps in my workflow!

Let’s go!

Functional programming in R

  • Functional programming is a programming paradigm

  • Functions are really important in this context!

Let’s remember: what is a function in R?

  • A function is a set of instructions that takes an input (arguments) and returns an output.

  • You can encapsulate and reuse code.

Example

Instead of writing this command in the terminal:

date
Fri Nov 24 12:05:56 -03 2023

You can use this function in R:

Sys.time()
[1] "2023-11-24 12:05:56 -03"

We use functions all the time!

Structure of a function

A function has:

  • a name

  • arguments

  • a body

  • a return value (it is the last line of the function)

name_of_function <- function(argument1, argument2, ...) {
  body
  return(value)
}

Simple example of how to create a function

convert_reais_to_us_dollars <- function(reais) {
  us_dollar <- reais / 4.87
  scales::dollar(us_dollar)
}
  • Minimum wage in Brazil per month in US Dollars:
convert_reais_to_us_dollars(1320)
[1] "$271.05"

Example of a more useful function

  • In Brazil, we use the date format dd/mm/yyyy: 21/01/2021.

  • We can use the function as.Date() to convert the date format, but we need to specify the format of the date.

as.Date("24/11/2023", format = "%d/%m/%Y")
[1] "2023-11-24"
  • We can create a function to do this for us!
convert_date_br <- function(date){
  as.Date(date, format = "%d/%m/%Y")
}
convert_date_br("14/10/2023")
[1] "2023-10-14"

Let’s purrr!

The purrr package

  • A toolbox for functional programming

  • Many of the functions in purrr are alternatives to for loops

  • Code with purrr is shorter and clearer than for loops

  • Knowing how to use purrr allows you to use the furrr package: it has the same syntax, but allows you to run code in parallel

Package version

  • Check if the version installed is >= 1.0.0.
packageVersion("purrr")
[1] '1.0.2'
  • If not, install the latest version:
install.packages("purrr")

My favorite functions

Let’s start!

library(tidyverse)

purrr::map()

  • purrr::map() is the most important function in purrr

  • Given an object (can be a vector or a list) we want to apply a function to each element of that object.

  • It returns a list

# Structure
map(vector_or_list, name_of_the_function)
  • There are many functions that start with map_*() and have similar behavior

Simple example of purrr::map()

# Vector that we want to apply the function
countries_mercosul <- c("Brazil", "Argentina",
                        "Uruguay", "Paraguay")
map(countries_mercosul, str_to_upper)
[[1]]
[1] "BRAZIL"

[[2]]
[1] "ARGENTINA"

[[3]]
[1] "URUGUAY"

[[4]]
[1] "PARAGUAY"

What if I want a vector to be returned?

We can use purrr::map_vec()!

  • map_vec() is a variant of map() that always returns a vector!

  • It is useful when you want to apply a function to each element of a vector and return a vector.

map_vec(countries_mercosul, str_to_upper) 
[1] "BRAZIL"    "ARGENTINA" "URUGUAY"   "PARAGUAY" 

We also can use purrr::list_c()!

  • list_c() is a function that takes a list and returns a vector

  • It is useful to use after map(), and you want to convert the list into a vector

map(countries_mercosul, str_to_upper) |> list_c()
[1] "BRAZIL"    "ARGENTINA" "URUGUAY"   "PARAGUAY" 

purrr::pluck()

  • pluck() is a function that extracts a single element from a list

  • pluck() can be used to extract elements from nested lists. That’s where it shines!

  • We will use a simple list for the examples:

map(countries_mercosul, str_to_upper)
[[1]]
[1] "BRAZIL"

[[2]]
[1] "ARGENTINA"

[[3]]
[1] "URUGUAY"

[[4]]
[1] "PARAGUAY"

Example of purrr::pluck()

map(countries_mercosul, str_to_upper) |> pluck(1)
[1] "BRAZIL"
map(countries_mercosul, str_to_upper) |> pluck(2)
[1] "ARGENTINA"
map(countries_mercosul, str_to_upper) |> pluck(3)
[1] "URUGUAY"
map(countries_mercosul, str_to_upper) |> pluck(4)
[1] "PARAGUAY"

Why we don’t use [[ ]] instead?

  • With pluck():
map(countries_mercosul, str_to_upper) |> pluck(1)
[1] "BRAZIL"
  • With [[ ]]:
map(countries_mercosul, str_to_upper)[[1]]
[1] "BRAZIL"

Pluck is pipeable! |>

  • With pluck():
map(countries_mercosul, str_to_upper) |>
  pluck(1)
[1] "BRAZIL"
  • With [[ ]]:
map(countries_mercosul, str_to_upper) |> 
  .[[1]]
Error: function '[[' not supported in RHS call of a pipe

Pluck is safer!

  • If the element does not exist, it returns NULL instead of an error.

  • With pluck():

map(countries_mercosul, str_to_upper) |> pluck(5)
NULL
  • With [[ ]]:
map(countries_mercosul, str_to_upper)[[5]]
Error in map(countries_mercosul, str_to_upper)[[5]]: subscript out of bounds

list_rbind(), list_cbind()

  • list_rbind() and list_cbind() are functions that bind lists that contains dataframes (by row or column) and returns a tibble.

  • Let’s see a real life example!

Example on my daily tasks

  • I had to pick one to fit in the time we have today!

Example

“I have a folder with multiple files that I want to import. How can I do that?”

  • Remember: to use map(), we need:

    • a vector or a list of the elements that we want to apply the function;

    • a function.

Example

If we want to import one file, we can use readxl::read_excel():

library(readxl)
recent_chla <- read_excel("data/ex-1/RelatorioQualidadeAguasSuperficiais_clorofila-a_2018-2022.xlsx")
glimpse(recent_chla)
Rows: 63
Columns: 26
$ `Período DE`              <chr> "01/01/2018", "01/01/2018", "01/01/2018", "0…
$ `Período ATE`             <chr> "16/10/2022", "16/10/2022", "16/10/2022", "1…
$ Cod_Interaguas            <chr> "1386", "1386", "1386", "1386", "1386", "138…
$ `Tipo Rede`               <chr> "Rede Básica", "Rede Básica", "Rede Básica",…
$ UGRHI                     <chr> "06 - ALTO TIÊTE", "06 - ALTO TIÊTE", "06 - …
$ `Código Ponto`            <chr> "BILL02030", "BILL02030", "BILL02030", "BILL…
$ `Status Ponto`            <chr> "Ativo", "Ativo", "Ativo", "Ativo", "Ativo",…
$ `Data Coleta`             <chr> "11/01/2018", "22/05/2018", "26/07/2018", "2…
$ `Hora Coleta`             <chr> "13:44", "13:33", "16:29", "14:12", "10:00",…
$ Parametro                 <chr> "Clorofila-a", "Clorofila-a", "Clorofila-a",…
$ Sinal                     <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ Valor                     <chr> "368,87000000", "47,22000000", "53,46000000"…
$ Unidade                   <chr> "µg/L", "µg/L", "µg/L", "µg/L", "µg/L", "µg/…
$ `Tipo Parâmetro`          <chr> "5- Hidrobiológicos", "5- Hidrobiológicos", …
$ `Sistema Hídrico`         <chr> "Reservatório Billings - BILL", "Reservatóri…
$ `Tipo de Sistema Hídrico` <chr> "Reservatório (Lêntico)", "Reservatório (Lên…
$ CLASSE                    <chr> "Classe 2", "Classe 2", "Classe 2", "Classe …
$ Município                 <chr> "SÃO PAULO", "SÃO PAULO", "SÃO PAULO", "SÃO …
$ UF                        <chr> "SP", "SP", "SP", "SP", "SP", "SP", "SP", "S…
$ `Inicio Operação`         <chr> "01/01/2007", "01/01/2007", "01/01/2007", "0…
$ `Fim Operação`            <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ Latitude                  <chr> "23 43 04", "23 43 04", "23 43 04", "23 43 0…
$ Longitude                 <chr> "46 39 51", "46 39 51", "46 39 51", "46 39 5…
$ Altitude                  <chr> "743", "743", "743", "743", "743", "743", "7…
$ Localização               <chr> "No meio do corpo central, cerca de 1,5 km d…
$ Captação                  <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N",…

Example

  • But we want to import multiple files!

  • The function is the same (readxl::read_excel()), but we need to apply it to multiple files.

  • We need to create a vector with all the file paths!

Example

  • Let’s start creating the vector of files that we want to import:
files_to_import <-
  list.files(path = "data/ex-1",
             pattern = "*.xlsx",
             full.names = TRUE)
files_to_import
[1] "data/ex-1/RelatorioQualidadeAguasSuperficiais_clorofila-a_2008-2012.xlsx"
[2] "data/ex-1/RelatorioQualidadeAguasSuperficiais_clorofila-a_2013-2017.xlsx"
[3] "data/ex-1/RelatorioQualidadeAguasSuperficiais_clorofila-a_2018-2022.xlsx"
[4] "data/ex-1/RelatorioQualidadeAguasSuperficiais_PT_2008-2012.xlsx"         
[5] "data/ex-1/RelatorioQualidadeAguasSuperficiais_PT_2013-2017.xlsx"         
[6] "data/ex-1/RelatorioQualidadeAguasSuperficiais_PT_2018-2022.xlsx"         

Example

result_import <- files_to_import |> 
  map(read_excel)

result_import
[[1]]
# A tibble: 102 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 92 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <lgl>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

[[2]]
# A tibble: 160 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2013   31/12/2017    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 150 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <lgl>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

[[3]]
# A tibble: 63 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 53 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <lgl>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

[[4]]
# A tibble: 120 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2008   31/12/2012    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 110 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <chr>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

[[5]]
# A tibble: 141 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2013   31/01/2017    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 131 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <chr>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

[[6]]
# A tibble: 78 × 26
   `Período DE` `Período ATE` Cod_Interaguas `Tipo Rede` UGRHI    `Código Ponto`
   <chr>        <chr>         <chr>          <chr>       <chr>    <chr>         
 1 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 2 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 3 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 4 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 5 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 6 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 7 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 8 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
 9 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
10 01/01/2018   16/10/2022    1386           Rede Básica 06 - AL… BILL02030     
# ℹ 68 more rows
# ℹ 20 more variables: `Status Ponto` <chr>, `Data Coleta` <chr>,
#   `Hora Coleta` <chr>, Parametro <chr>, Sinal <chr>, Valor <chr>,
#   Unidade <chr>, `Tipo Parâmetro` <chr>, `Sistema Hídrico` <chr>,
#   `Tipo de Sistema Hídrico` <chr>, CLASSE <chr>, Município <chr>, UF <chr>,
#   `Inicio Operação` <chr>, `Fim Operação` <lgl>, Latitude <chr>,
#   Longitude <chr>, Altitude <chr>, Localização <chr>, Captação <chr>

Example

  • Remember: map() returns a list!
class(result_import)
[1] "list"
  • But we want to bind the dataframes by row!

Example

  • We can use list_rbind():
result_import_df <- files_to_import |> 
  map(read_excel) |> 
  list_rbind()

glimpse(result_import_df)
Rows: 664
Columns: 26
$ `Período DE`              <chr> "01/01/2008", "01/01/2008", "01/01/2008", "0…
$ `Período ATE`             <chr> "31/12/2012", "31/12/2012", "31/12/2012", "3…
$ Cod_Interaguas            <chr> "1386", "1386", "1386", "1386", "1386", "138…
$ `Tipo Rede`               <chr> "Rede Básica", "Rede Básica", "Rede Básica",…
$ UGRHI                     <chr> "06 - ALTO TIÊTE", "06 - ALTO TIÊTE", "06 - …
$ `Código Ponto`            <chr> "BILL02030", "BILL02030", "BILL02030", "BILL…
$ `Status Ponto`            <chr> "Ativo", "Ativo", "Ativo", "Ativo", "Ativo",…
$ `Data Coleta`             <chr> "22/01/2008", "26/03/2008", "14/05/2008", "1…
$ `Hora Coleta`             <chr> "11:20", "11:22", "12:30", "11:50", "11:20",…
$ Parametro                 <chr> "Clorofila-a", "Clorofila-a", "Clorofila-a",…
$ Sinal                     <chr> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ Valor                     <chr> "18,71000000", "28,87000000", "31,19000000",…
$ Unidade                   <chr> "µg/L", "µg/L", "µg/L", "µg/L", "µg/L", "µg/…
$ `Tipo Parâmetro`          <chr> "5- Hidrobiológicos", "5- Hidrobiológicos", …
$ `Sistema Hídrico`         <chr> "Reservatório Billings - BILL", "Reservatóri…
$ `Tipo de Sistema Hídrico` <chr> "Reservatório (Lêntico)", "Reservatório (Lên…
$ CLASSE                    <chr> "Classe 2", "Classe 2", "Classe 2", "Classe …
$ Município                 <chr> "SÃO PAULO", "SÃO PAULO", "SÃO PAULO", "SÃO …
$ UF                        <chr> "SP", "SP", "SP", "SP", "SP", "SP", "SP", "S…
$ `Inicio Operação`         <chr> "01/01/2007", "01/01/2007", "01/01/2007", "0…
$ `Fim Operação`            <lgl> NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, NA, …
$ Latitude                  <chr> "23 43 04", "23 43 04", "23 43 04", "23 43 0…
$ Longitude                 <chr> "46 39 51", "46 39 51", "46 39 51", "46 39 5…
$ Altitude                  <chr> "743", "743", "743", "743", "743", "743", "7…
$ Localização               <chr> "No meio do corpo central, cerca de 1,5 km d…
$ Captação                  <chr> "N", "N", "N", "N", "N", "N", "N", "N", "N",…

Final thoughts

  • purrr is a very powerful package!

  • I use it a lot in my daily tasks!

  • Today, I focused on helping you to understand the basics to start using purrr. But there is other functions that you can learn!

Learn more