Mastering purrr: From Basic Maps to Functional Magic in R (2024)

[This article was first published on Numbers around us - Medium, and kindly contributed to R-bloggers]. (You can report issue about the content on this page here)

Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Mastering purrr: From Basic Maps to Functional Magic in R (1)

Welcome back to the world of purrr! Last time (about a year ago), we spun a metaphorical yarn about the wonders of purrr in R. Today, we're rolling up our sleeves and diving into a hands-on tutorial. We're going to explore how purrr makes working with lists and vectors a breeze, transforming and manipulating them like a datawizard.

With purrr, you can apply functions to each element of a list or vector, manipulate them, check conditions, and so much more. It's all about making your data dance to your commands with elegance and efficiency. Ready to unleash some functional magic?

Are map Functions Like apply Functions?

You might be wondering, “Aren’t map functions just fancy versions of apply functions?” It's a fair question! Both map and apply functions help you apply a function to elements in a data structure, but purrr takes it to a whole newlevel.

Here’s why purrr and its map functions are worth your attention:

  • Consistency: purrr functions have a consistent naming scheme, making them easier to learn and remember.
  • Type Safety: map functions in purrr return outputs of consistent types, reducing unexpected errors.
  • Integration: Seamlessly integrate with other tidyverse packages, making your data wrangling pipeline smoother.

Let’s see a quick comparison:

library(tidyverse)# Using lapply (base R)numbers <- list(1, 2, 3, 4, 5)squared_lapply <- lapply(numbers, function(x) x^2)# Using map (purrr)squared_map <- map(numbers, ~ .x^2)print(squared_lapply)[[1]][1] 1[[2]][1] 4[[3]][1] 9[[4]][1] 16[[5]][1] 25print(squared_map)[[1]][1] 1[[2]][1] 4[[3]][1] 9[[4]][1] 16[[5]][1] 25

Both do the same thing, but purrr’s map function is more readable and concise, especially when paired with the tidyverse syntax.

Here’s another example with a built-indataset:

# Using lapply with a built-in datasetiris_split <- split(iris, iris$Species)mean_sepal_length_lapply <- lapply(iris_split, function(df) mean(df$Sepal.Length))# Using map with a built-in datasetmean_sepal_length_map <- map(iris_split, ~ mean(.x$Sepal.Length))print(mean_sepal_length_lapply)$setosa[1] 5.006$versicolor[1] 5.936$virginica[1] 6.588print(mean_sepal_length_map)$setosa[1] 5.006$versicolor[1] 5.936$virginica[1] 6.588

Again, the purrr version is cleaner and easier to understand at aglance.

Convinced? Let’s move on to explore simple maps and their variants to see more of purrr’s magic.Ready?

Simple Maps and TheirVariants

Now that we know why purrr’s map functions are so cool, let’s dive into some practical examples. The map function family is like a Swiss Army knife for data transformation. It comes in different flavors depending on the type of output you want: logical, integer, character, ordouble.

Let’s start with the basic map function:

library(tidyverse)# Basic map examplenumbers <- list(1, 2, 3, 4, 5)squared_numbers <- map(numbers, ~ .x^2)squared_numbers

Easy, right? Yes, but we have one twist here. Result is returned as list, and we don’t always need list. So now, let’s look at the type-specific variants. These functions ensure that the output is of a specific type, which can help avoid unexpected surprises in your data processing pipeline.

  • Logical (map_lgl):
# Check if each number is evenis_even <- map_lgl(numbers, ~ .x %% 2 == 0)is_even[1] FALSE TRUE FALSE TRUE FALSE# it is not list anymore, it is logical vector
  • Integer (map_int):
# Double each number and return as integersdoubled_integers <- map_int(numbers, ~ .x * 2)doubled_integers[1] 2 4 6 8 10
  • Character (map_chr):
# Convert each number to a stringnumber_strings <- map_chr(numbers, ~ paste("Number", .x))number_strings[1] "Number 1" "Number 2" "Number 3" "Number 4" "Number 5"
  • Double (map_dbl):
# Half each number and return as doubleshalved_doubles <- map_dbl(numbers, ~ .x / 2)halved_doubles[1] 0.5 1.0 1.5 2.0 2.5

Let’s apply this to a built-in dataset to see it inaction:

# Using map_dbl on the iris dataset to get the mean of each numeric columniris_means <- iris %>% select(-Species) %>% map_dbl(mean)iris_meansSepal.Length Sepal.Width Petal.Length Petal.Width 5.843333 3.057333 3.758000 1.199333 

Here, we’ve calculated the mean of each numeric column in the iris dataset, and the result is a named vector ofdoubles.

Pretty neat, huh? The map family makes it easy to ensure your data stays in the format youexpect.

Ready to see how purrr handles multiple vectors with map2 andpmap?

Not Only One Vector: map2 and pmap +Variants

So far, we’ve seen how map functions work with a single vector or list. But what if you have multiple vectors and want to apply a function to corresponding elements from each? Enter map2 andpmap.

  • map2: This function applies a function to corresponding elements of two vectors orlists.
  • pmap: This function applies a function to corresponding elements of multiplelists.

Let’s start withmap2:

library(tidyverse)# Two vectors to work withvec1 <- c(1, 2, 3)vec2 <- c(4, 5, 6)# Adding corresponding elements of two vectorssum_vecs <- map2(vec1, vec2, ~ .x + .y)sum_vecs[[1]][1] 5[[2]][1] 7[[3]][1] 9

Here, map2 takes elements from vec1 and vec2 and adds them together.

Now, let’s step it up withpmap:

# Creating a tibble for multiple listsdf <- tibble( a = 1:3, b = 4:6, c = 7:9)# Summing corresponding elements of multiple listssum_pmap <- pmap(df, ~ ..1 + ..2 + ..3)sum_pmap[[1]][1] 12[[2]][1] 15[[3]][1] 18

In this example, pmap takes elements from columns a, b, and c of the tibble and sums themup.

Look at syntax in those two examples. In map2, we give two vectors or lists, and then we are reffering to them as.x and.y. Further in pmap example we have data.frame, but it can be a list of lists, and we need to refer to them with numbers like..1,..2 and..3 (and more ifneeded).

Variants of map2 andpmap

Just like map, map2 and pmap have type-specific variants. Let’s see a couple of examples using data structures already definedabove:

  • map2_dbl:
# Multiplying corresponding elements of two vectors and returning doublesproduct_vecs <- map2_dbl(vec1, vec2, ~ .x * .y)product_vecs[1] 4 10 18
  • pmap_chr:
# Concatenating corresponding elements of multiple lists into stringsconcat_pmap <- pmap_chr(df, ~ paste(..1, ..2, ..3, sep = "-"))concat_pmap[1] "1-4-7" "2-5-8" "3-6-9"

These variants ensure that your results are of the expected type, just like the basic map variants.

With map2 and pmap, you can handle more complex data transformations involving multiple vectors or lists withease.

Ready to move on and see what lmap and imap can do foryou?

Using imap for Indexed Mapping and Conditional Maps with _if and_at

Let’s combine our exploration of imap with the conditional mapping functions map_if and map_at. These functions give you more control over how and when functions are applied to your data, making your code more precise and expressive.

imap: IndexedMapping

The imap function is a handy tool when you need to include the index or names of elements in your function calls. This is particularly useful for tasks where the position or name of an element influences the operation performed onit.

Here’s a practical example with a namedlist:

library(tidyverse)# A named list of scoresnamed_scores <- list(math = 90, science = 85, history = 78)# Create descriptive strings for each scorescore_descriptions <- imap(named_scores, ~ paste(.y, "score is", .x))score_descriptions$math[1] "math score is 90"$science[1] "science score is 85"$history[1] "history score is 78"

In thisexample:

  1. We have a named list named_scores with subjectscores.
  2. We use imap to create a descriptive string for each score that includes the subject name and thescore.

Conditional Maps with map_if andmap_at

Sometimes, you don’t want to apply a function to all elements of a list or vector — only to those that meet certain conditions. This is where map_if and map_at come intoplay.

map_if: Conditional Mapping

Use map_if to apply a function to elements that satisfy a specific condition (predicate).

# Mixed list of numbers and charactersmixed_list <- list(1, "a", 3, "b", 5)# Double only the numeric elementsdoubled_numbers <- map_if(mixed_list, is.numeric, ~ .x * 2)doubled_numbers[[1]][1] 2[[2]][1] "a"[[3]][1] 6[[4]][1] "b"[[5]][1] 10

In thisexample:

  1. We have a mixed list of numbers and characters.
  2. We use map_if to double only the numeric elements, leaving the characters unchanged.

map_at: Specific ElementMapping

Use map_at to apply a function to specific elements of a list or vector, identified by their indices ornames.

# A named list of mixed typesspecific_list <- list(a = 1, b = "hello", c = 3, d = "world")# Convert only the character elements to uppercaseuppercase_chars <- map_at(specific_list, c("b", "d"), ~ toupper(.x))uppercase_chars$a[1] 1$b[1] "HELLO"$c[1] 3$d[1] "WORLD"

In thisexample:

  1. We have a named list with mixedtypes.
  2. We use map_at to convert only the specified character elements to uppercase.

Combining imap, map_if, and map_at allows you to handle complex data transformation tasks with precision and clarity. These functions make it easy to tailor your operations to the specific needs of yourdata.

Shall we move on to the next chapter to explore walk and its friends for side-effect operations?

Make Something Happen Outside of Data: walk and ItsFriends

Sometimes, you want to perform operations that have side effects, like printing, writing to a file, or plotting, rather than returning a transformed list or vector. This is where the walk family of functions comes in handy. These functions are designed to be used for their side effects, as they returnNULL.

walk

The basic walk function applies a function to each element of a list or vector and performs actions like printing or savingfiles.

library(tidyverse)# A list of numbersnumbers <- list(1, 2, 3, 4, 5)# Print each numberwalk(numbers, ~ print(.x))[1] 1[1] 2[1] 3[1] 4[1] 5

In this example, walk prints each element of the numberslist.

walk2

When you have two lists or vectors and you want to perform side-effect operations on their corresponding elements, walk2 is yourfriend.

# Two vectors to work withvec1 <- c("apple", "banana", "cherry")vec2 <- c("red", "yellow", "dark red")# Print each fruit with its colorwalk2(vec1, vec2, ~ cat(.x, "is", .y, "\n"))apple is red banana is yellow cherry is dark red 

Here, walk2 prints each fruit with its corresponding color.

iwalk

iwalk is the side-effect version of imap. It includes the index or names of the elements, which can be useful for logging or debugging.

# A named list of scoresnamed_scores <- list(math = 90, science = 85, history = 78)# Print each subject with its scoreiwalk(named_scores, ~ cat("The score for", .y, "is", .x, "\n"))The score for math is 90 The score for science is 85 The score for history is 78 

In this example, iwalk prints each subject name with its corresponding score.

Practical Example with Built-inData

Let’s use a built-in dataset and perform some side-effect operations. Suppose you want to save plots of each numeric column in the mtcars dataset to separatefiles.

# Directory to save plotsdir.create("plots")# Save histograms of each numeric column to fileswalk(names(mtcars), ~ { if (is.numeric(mtcars[[.x]])) { plot_path <- paste0("plots/", .x, "_histogram.png") png(plot_path) hist(mtcars[[.x]], main = paste("Histogram of", .x), xlab = .x) dev.off() }})
Mastering purrr: From Basic Maps to Functional Magic in R (2)

In thisexample:

  1. We create a directory called“plots”.
  2. We use walk to iterate over the names of the mtcarsdataset.
  3. For each numeric column, we save a histogram to a PNGfile.

This is a practical demonstration of how walk can be used for side-effect operations such as savingfiles.

Why Do We Need modifyThen?

Sometimes you need to tweak elements within a list or vector without completely transforming them. This is where modify functions come in handy. They allow you to make specific changes to elements while preserving the overall structure of yourdata.

modify

The modify function applies a transformation to each element of a list or vector and returns the modified list orvector.

library(tidyverse)# A list of numbersnumbers <- list(1, 2, 3, 4, 5)# Add 10 to each numbermodified_numbers <- modify(numbers, ~ .x + 10)modified_numbers[[1]][1] 11[[2]][1] 12[[3]][1] 13[[4]][1] 14[[5]][1] 15

In this example, modify adds 10 to each element of the numberslist.

modify_if

modify_if is used to conditionally modify elements that meet a specified condition (predicate).

# Modify only the even numbers by multiplying them by 2modified_if <- modify_if(numbers, ~ .x %% 2 == 0, ~ .x * 2)modified_if[[1]][1] 1[[2]][1] 4[[3]][1] 3[[4]][1] 8[[5]][1] 5

Here, modify_if multiplies only the even numbers by2.

modify_at

modify_at allows you to specify which elements to modify based on their indices ornames.

# A named list of mixed typesnamed_list <- list(a = 1, b = "hello", c = 3, d = "world")# Convert only the specified elements to uppercasemodified_at <- modify_at(named_list, c("b", "d"), ~ toupper(.x))modified_at$a[1] 1$b[1] "HELLO"$c[1] 3$d[1] "WORLD"

In this example, modify_at converts the specified character elements to uppercase.

modify with Built-inDataset

Let’s use the iris dataset to demonstrate how modify functions can be applied in a practical scenario. Suppose we want to normalize numeric columns by dividing each value by the maximum value in itscolumn.

# Normalizing numeric columns in the iris datasetnormalized_iris <- iris %>% modify_at(vars(Sepal.Length, Sepal.Width, Petal.Length, Petal.Width), ~ .x / max(.x))head(normalized_iris) Sepal.Length Sepal.Width Petal.Length Petal.Width Species1 0.6455696 0.7954545 0.2028986 0.08 setosa2 0.6202532 0.6818182 0.2028986 0.08 setosa3 0.5949367 0.7272727 0.1884058 0.08 setosa4 0.5822785 0.7045455 0.2173913 0.08 setosa5 0.6329114 0.8181818 0.2028986 0.08 setosa6 0.6835443 0.8863636 0.2463768 0.16 setosahead(iris)1 5.1 3.5 1.4 0.2 setosa2 4.9 3.0 1.4 0.2 setosa3 4.7 3.2 1.3 0.2 setosa4 4.6 3.1 1.5 0.2 setosa5 5.0 3.6 1.4 0.2 setosa6 5.4 3.9 1.7 0.4 setosa

In thisexample:

  1. We use modify_at to specify the numeric columns of the irisdataset.
  2. Each value in these columns is divided by the maximum value in its respective column, normalizing thedata.

modify functions offer a powerful way to make targeted changes to your data, providing flexibility andcontrol.

Predicates: Does Data Satisfy Our Assumptions? every, some, andnone

When working with data, it’s often necessary to check if certain conditions hold across elements in a list or vector. This is where predicate functions like every, some, and none come in handy. These functions help you verify whether elements meet specified criteria, making your data validation tasks easier and more expressive.

every

The every function checks if all elements in a list or vector satisfy a given predicate. If all elements meet the condition, it returns TRUE; otherwise, it returnsFALSE.

library(tidyverse)# A list of numbersnumbers <- list(2, 4, 6, 8)# Check if all numbers are evenall_even <- every(numbers, ~ .x %% 2 == 0)all_even[1] TRUE

In this example, every checks if all elements in the numbers list areeven.

some

The some function checks if at least one element in a list or vector satisfies a given predicate. If any element meets the condition, it returns TRUE; otherwise, it returnsFALSE.

# Check if any number is greater than 5any_greater_than_five <- some(numbers, ~ .x > 5)any_greater_than_five[1] TRUE

Here, some checks if any element in the numbers list is greater than5.

none

The none function checks if no elements in a list or vector satisfy a given predicate. If no elements meet the condition, it returns TRUE; otherwise, it returnsFALSE.

# Check if no number is oddnone_odd <- none(numbers, ~ .x %% 2 != 0)none_odd[1] TRUE

In this example, none checks if no elements in the numbers list areodd.

Practical Example with Built-inDataset

Let’s use the mtcars dataset to demonstrate how these predicate functions can be applied in a practical scenario. Suppose we want to check various conditions on the columns of thisdataset.

# Check if all cars have more than 10 miles per gallon (mpg)all_mpg_above_10 <- mtcars %>% select(mpg) %>% map_lgl(~ every(.x, ~ .x > 10))all_mpg_above_10mpgTRUE# Check if some cars have more than 150 horsepower (hp)some_hp_above_150 <- mtcars %>% select(hp) %>% map_lgl(~ some(.x, ~ .x > 150))some_hp_above_150hpTRUE# Check if no car has more than 8 cylindersnone_cyl_above_8 <- mtcars %>% select(cyl) %>% map_lgl(~ none(.x, ~ .x > 8))none_cyl_above_8cylTRUE

In thisexample:

  1. We check if all cars in the mtcars dataset have more than 10 mpg usingevery.
  2. We check if some cars have more than 150 horsepower usingsome.
  3. We check if no car has more than 8 cylinders usingnone.

These predicate functions provide a straightforward way to validate your data against specific conditions, making your analysis morerobust.

What If Not: keep anddiscard

When you’re working with lists or vectors, you often need to filter elements based on certain conditions. The keep and discard functions from purrr are designed for this purpose. They allow you to retain or remove elements that meet specified criteria, making it easy to clean and subset yourdata.

keep

The keep function retains elements that satisfy a given predicate. If an element meets the condition, it is kept; otherwise, it isremoved.

library(tidyverse)# A list of mixed numbersnumbers <- list(1, 2, 3, 4, 5, 6, 7, 8, 9, 10)# Keep only the even numberseven_numbers <- keep(numbers, ~ .x %% 2 == 0)even_numbers[[1]][1] 2[[2]][1] 4[[3]][1] 6[[4]][1] 8[[5]][1] 10

In this example, keep retains only the even numbers from the numberslist.

discard

The discard function removes elements that satisfy a given predicate. If an element meets the condition, it is discarded; otherwise, it iskept.

# Discard the even numbersodd_numbers <- discard(numbers, ~ .x %% 2 == 0)odd_numbers[[1]][1] 1[[2]][1] 3[[3]][1] 5[[4]][1] 7[[5]][1] 9

Here, discard removes the even numbers, leaving only the odd numbers in the numberslist.

Practical Example with Built-inDataset

Let’s use the iris dataset to demonstrate how keep and discard can be applied in a practical scenario. Suppose we want to filter rows based on specific conditions for the Sepal.Length column.

library(tidyverse)# Keep rows where Sepal.Length is greater than 5.0iris_keep <- iris %>% split(1:nrow(.)) %>% keep(~ .x$Sepal.Length > 5.0) %>% bind_rows()head(iris_keep) Sepal.Length Sepal.Width Petal.Length Petal.Width Species1 5.1 3.5 1.4 0.2 setosa2 5.4 3.9 1.7 0.4 setosa3 5.4 3.7 1.5 0.2 setosa4 5.8 4.0 1.2 0.2 setosa5 5.7 4.4 1.5 0.4 setosa6 5.4 3.9 1.3 0.4 setosa# Discard rows where Sepal.Length is less than or equal to 5.0iris_discard <- iris %>% split(1:nrow(.)) %>% discard(~ .x$Sepal.Length <= 5.0) %>% bind_rows()head(iris_discard) Sepal.Length Sepal.Width Petal.Length Petal.Width Species1 5.1 3.5 1.4 0.2 setosa2 5.4 3.9 1.7 0.4 setosa3 5.4 3.7 1.5 0.2 setosa4 5.8 4.0 1.2 0.2 setosa5 5.7 4.4 1.5 0.4 setosa6 5.4 3.9 1.3 0.4 setosa

In thisexample:

  1. We split the iris dataset into a list ofrows.
  2. We apply keep to retain rows where Sepal.Length is greater than5.0.
  3. We apply discard to remove rows where Sepal.Length is less than or equal to5.0.
  4. Finally, we use bind_rows() to combine the list back into a dataframe.

Combining keep and discard withmtcars

Similarly, let’s fix the mtcarsexample:

# Keep cars with mpg greater than 20 and discard cars with hp less than 100filtered_cars <- mtcars %>% split(1:nrow(.)) %>% keep(~ .x$mpg > 20) %>% discard(~ .x$hp < 100) %>% bind_rows()filtered_cars mpg cyl disp hp drat wt qsec vs am gear carbMazda RX4 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4Mazda RX4 Wag 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4Hornet 4 Drive 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1Lotus Europa 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2Volvo 142E 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2

In this combinedexample:

  1. We split the mtcars dataset into a list ofrows.
  2. We use keep to retain cars with mpg greater than20.
  3. We use discard to remove cars with hp less than100.
  4. We combine the filtered list back into a data frame using bind_rows().

Do Things in Order of List/Vector: accumulate, reduce

Sometimes, you need to perform cumulative or sequential operations on your data. This is where accumulate and reduce come into play. These functions allow you to apply a function iteratively across elements of a list or vector, either accumulating results at each step or reducing the list to a singlevalue.

accumulate

The accumulate function applies a function iteratively to the elements of a list or vector and returns a list of intermediate results.

Let’s start with a simpleexample:

library(tidyverse)# A list of numbersnumbers <- list(1, 2, 3, 4, 5)# Cumulative sum of the numberscumulative_sum <- accumulate(numbers, `+`)cumulative_sum[1] 1 3 6 10 15

reduce

The reduce function applies a function iteratively to reduce the elements of a list or vector to a singlevalue.

Here’s a basicexample:

# Sum of the numberstotal_sum <- reduce(numbers, `+`)total_sum[1] 15

Practical Example with Built-inDataset

Let’s use the mtcars dataset to demonstrate how accumulate and reduce can be applied in a practical scenario.

Using accumulate withmtcars

Suppose we want to calculate the cumulative sum of the miles per gallon (mpg) for eachcar.

# Cumulative sum of mpg valuescumulative_mpg <- mtcars %>% pull(mpg) %>% accumulate(`+`)cumulative_mpg[1] 21.0 42.0 64.8 86.2 104.9 123.0 137.3 161.7 184.5 203.7 221.5 237.9 255.2 270.4 280.8 291.2 305.9 338.3 368.7[20] 402.6 424.1 439.6 454.8 468.1 487.3 514.6 540.6 571.0 586.8 606.5 621.5 642.9

In this example, accumulate gives us a cumulative sum of the mpg values for the cars in the mtcarsdataset.

Using reduce withmtcars

Now, let’s say we want to find the product of all mpgvalues:

# Product of mpg valuesproduct_mpg <- mtcars %>% pull(mpg) %>% reduce(`*`)product_mpg[1] 1.264241e+41

In this example, reduce calculates the product of all mpg values in the mtcarsdataset.

Do It Another Way: compose andnegate

Creating flexible and reusable functions is a hallmark of efficient programming. purrr provides tools like compose and negate to help you build and manipulate functions more effectively. These tools allow you to combine multiple functions into one or invert the logic of a predicate function.

compose

The compose function combines multiple functions into a single function that applies them sequentially. This can be incredibly useful for creating pipelines of operations.

Here’s a basicexample:

library(tidyverse)# Define some simple functionsadd1 <- function(x) x + 1square <- function(x) x * x# Compose them into a single functionadd1_and_square <- compose(square, add1)# Apply the composed functionresult <- add1_and_square(2) # (2 + 1)^2 = 9result[1] 9

In thisexample:

  1. We define two simple functions: add1 andsquare.
  2. We use compose to create a new function, add1_and_square, which first adds 1 to its input and then squares theresult.
  3. We apply the composed function to the number 2, yielding9.

Practical Example with Built-inDataset

Let’s use compose with a more practical example involving the mtcars dataset. Suppose we want to create a function that first scales the horsepower (hp) by 10 and then calculates the logarithm.

# Define scaling and log functionsscale_by_10 <- function(x) x * 10safe_log <- safely(log, otherwise = NA)# Compose them into a single functionscale_and_log <- compose(safe_log, scale_by_10)# Apply the composed function to the hp columnmtcars <- mtcars %>% mutate(log_scaled_hp = map_dbl(hp, ~ scale_and_log(.x)$result))head(mtcars) mpg cyl disp hp drat wt qsec vs am gear carb log_scaled_hpMazda RX4 21.0 6 160 110 3.90 2.620 16.46 0 1 4 4 7.003065Mazda RX4 Wag 21.0 6 160 110 3.90 2.875 17.02 0 1 4 4 7.003065Datsun 710 22.8 4 108 93 3.85 2.320 18.61 1 1 4 1 6.835185Hornet 4 Drive 21.4 6 258 110 3.08 3.215 19.44 1 0 3 1 7.003065Hornet Sportabout 18.7 8 360 175 3.15 3.440 17.02 0 0 3 2 7.467371Valiant 18.1 6 225 105 2.76 3.460 20.22 1 0 3 1 6.956545

In thisexample:

  1. We define two functions: scale_by_10 and safe_log.
  2. We compose these functions into scale_and_log.
  3. We apply the composed function to the hp column of the mtcars dataset and add the results as a newcolumn.

negate

The negate function creates a new function that returns the logical negation of a predicate function. This is useful when you want to invert the logic of a condition.

Here’s a simpleexample:

# Define a simple predicate functionis_even <- function(x) x %% 2 == 0# Negate the predicate functionis_odd <- negate(is_even)# Apply the negated functionresults <- map_lgl(1:10, is_odd)results [1] TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE TRUE FALSE

In thisexample:

  1. We define a predicate function is_even to check if a number iseven.
  2. We use negate to create a new function is_odd that returns the oppositeresult.
  3. We apply is_odd to the numbers 1 through10.

Practical Example with Built-inDataset

Let’s use negate in a practical scenario with the iris dataset. Suppose we want to filter out rows where the Sepal.Length is not greater than5.0.

# Define a predicate functionis_long_sepal <- function(x) x > 5.0# Negate the predicate functionis_not_long_sepal <- negate(is_long_sepal)# Filter out rows where Sepal.Length is not greater than 5.0iris_filtered <- iris %>% split(1:nrow(.)) %>% discard(~ is_not_long_sepal(.x$Sepal.Length)) %>% bind_rows()head(iris_filtered) Sepal.Length Sepal.Width Petal.Length Petal.Width Species1 5.1 3.5 1.4 0.2 setosa2 5.4 3.9 1.7 0.4 setosa3 5.4 3.7 1.5 0.2 setosa4 5.8 4.0 1.2 0.2 setosa5 5.7 4.4 1.5 0.4 setosa6 5.4 3.9 1.3 0.4 setosa

In thisexample:

  1. We define a predicate function is_long_sepal to check if Sepal.Length is greater than5.0.
  2. We use negate to create a new function is_not_long_sepal that returns the oppositeresult.
  3. We use discard to remove rows where Sepal.Length is not greater than 5.0, then combine the filtered list back into a dataframe.

With compose and negate, you can create more flexible and powerful functions, allowing for more concise and readablecode.

Conclusion

Congratulations! You’ve journeyed through the world of purrr, mastering a wide array of functions and techniques to manipulate and transform your data. From basic mapping to creating powerful function compositions, purrr equips you with tools to make your data wrangling tasks more efficient and expressive.

Whether you’re applying functions conditionally, dealing with side effects, or validating your data, purrr has you covered. Keep exploring and experimenting with these functions to unlock the full potential of functional programming inR.

Gift for patientreaders

I decided to give you some useful, yet not trivial use cases of purrr functions.

Define list of function to apply ondata

apply_funs <- function(x, ...) purrr::map_dbl(list(...), ~ .x(x))

Want to apply multiple functions to a single vector and get a tidy result? Meet apply_funs, your new best friend! This nifty little function takes a value and a bunch of functions, then maps each function to the vector, returning the results as a neatvector.

Let’s break itdown:

  • x: The value you want to transform.
  • ...: A bunch of functions you want to apply tox.
  • purrr::map_dbl: Maps each function in the list to x and returns the results as a vector ofdoubles.

Suppose that you want to apply 3 summary functions on vector of numbers. Here’s how you can doit:

number <- 1:48results <- apply_funs(number, mean, median, sd)results[1] 24.5 24.5 14.0

Using pmap as equivalent of Python’szip

Sometimes you need to zip two tables or columns together. In Python there is zip function for it, but we do not have twin function in R, unless you use pmap. I will not make it longer, so check it out in one of my previous articles.

Rendering parameterized RMarkdown reports

Assuming that you have kind of report you use for each salesperson, there is possibility, that you are changing parameters manually to generate report for person X, for date range Y, for product Z. Why not prepare lists of people, time range, and list of products, and then based on them generate series of reports by one clickonly.

Mastering purrr: From Basic Maps to Functional Magic in R (3)

Mastering purrr: From Basic Maps to Functional Magic in R was originally published in Numbers around us on Medium, where people are continuing the conversation by highlighting and responding to this story.

Related

To leave a comment for the author, please follow the link and comment on their blog: Numbers around us - Medium.

R-bloggers.com offers daily e-mail updates about R news and tutorials about learning R and many other topics. Click here if you're looking to post or find an R/data-science job.

Want to share your content on R-bloggers? click here if you have a blog, or here if you don't.

Mastering purrr: From Basic Maps to Functional Magic in R (2024)
Top Articles
Latest Posts
Article information

Author: Greg O'Connell

Last Updated:

Views: 5629

Rating: 4.1 / 5 (42 voted)

Reviews: 89% of readers found this page helpful

Author information

Name: Greg O'Connell

Birthday: 1992-01-10

Address: Suite 517 2436 Jefferey Pass, Shanitaside, UT 27519

Phone: +2614651609714

Job: Education Developer

Hobby: Cooking, Gambling, Pottery, Shooting, Baseball, Singing, Snowboarding

Introduction: My name is Greg O'Connell, I am a delightful, colorful, talented, kind, lively, modern, tender person who loves writing and wants to share my knowledge and understanding with you.