[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.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 data wizard.
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 new level.
Here’s why purrr and its map functions are worth your attention:
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-in dataset:
```
``` Again, the purrr version is cleaner and easier to understand at a glance.
Convinced? Let’s move on to explore simple maps and their variants to see more of purrr’s magic. Ready?
Simple Maps and Their VariantsNow 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, or double.
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.
```
``` * Integer (map_int):
```
``` * Character (map_chr):
```
``` * Double (map_dbl):
```
``` Let’s apply this to a built-in dataset to see it in action:
```
``` Here, we’ve calculated the mean of each numeric column in the iris dataset, and the result is a named vector of doubles.
Pretty neat, huh? The map family makes it easy to ensure your data stays in the format you expect.
Ready to see how purrr handles multiple vectors with map2 and pmap?
Not Only One Vector: map2 and pmap + VariantsSo 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 and pmap.
Let’s start with map2:
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 with pmap:
```
``` In this example, pmap takes elements from columns a, b, and c of the tibble and sums them up.
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 if needed).
Variants of map2 and pmapJust like map, map2 and pmap have type-specific variants. Let’s see a couple of examples using data structures already defined above:
```
``` * pmap_chr:
```
``` 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 with ease.
Ready to move on and see what lmap and imap can do for you?
Using imap for Indexed Mapping and Conditional Maps with _if and _atLet’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: Indexed MappingThe 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 on it.
Here’s a practical example with a named list:
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 this example:
Conditional Maps with map_if and map_atSometimes, 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 into play.
map_if: Conditional Mapping
Use map_if to apply a function to elements that satisfy a specific condition (predicate).
```
``` In this example:
map_at: Specific Element Mapping
Use map_at to apply a function to specific elements of a list or vector, identified by their indices or names.
```
``` In this example:
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 your data.
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 Its FriendsSometimes, 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 return NULL.
walkThe basic walk function applies a function to each element of a list or vector and performs actions like printing or saving files.
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 numbers list.
walk2When you have two lists or vectors and you want to perform side-effect operations on their corresponding elements, walk2 is your friend.
```
``` Here, walk2 prints each fruit with its corresponding color.
iwalkiwalk is the side-effect version of imap. It includes the index or names of the elements, which can be useful for logging or debugging.
```
``` In this example, iwalk prints each subject name with its corresponding score.
Practical Example with Built-in DataLet’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 separate files.
```
``` In this example:
This is a practical demonstration of how walk can be used for side-effect operations such as saving files.
Why Do We Need modify Then?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 your data.
modifyThe modify function applies a transformation to each element of a list or vector and returns the modified list or vector.
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 numbers list.
modify_ifmodify_if is used to conditionally modify elements that meet a specified condition (predicate).
```
``` Here, modify_if multiplies only the even numbers by 2.
modify_atmodify_at allows you to specify which elements to modify based on their indices or names.
```
``` In this example, modify_at converts the specified character elements to uppercase.
modify with Built-in DatasetLet’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 its column.
```
``` In this example:
modify functions offer a powerful way to make targeted changes to your data, providing flexibility and control.
Predicates: Does Data Satisfy Our Assumptions? every, some, and noneWhen 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.
everyThe 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 returns FALSE.
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 are even.
someThe 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 returns FALSE.
```
``` Here, some checks if any element in the numbers list is greater than 5.
noneThe 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 returns FALSE.
```
``` In this example, none checks if no elements in the numbers list are odd.
Practical Example with Built-in DatasetLet’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 this dataset.
```
``` In this example:
These predicate functions provide a straightforward way to validate your data against specific conditions, making your analysis more robust.
What If Not: keep and discardWhen 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 your data.
keepThe keep function retains elements that satisfy a given predicate. If an element meets the condition, it is kept; otherwise, it is removed.
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 numbers list.
discardThe discard function removes elements that satisfy a given predicate. If an element meets the condition, it is discarded; otherwise, it is kept.
```
``` Here, discard removes the even numbers, leaving only the odd numbers in the numbers list.
Practical Example with Built-in DatasetLet’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 this example:
Combining keep and discard with mtcarsSimilarly, let’s fix the mtcars example:
```
``` In this combined example:
Do Things in Order of List/Vector: accumulate, reduceSometimes, 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 single value.
accumulateThe 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 simple example:
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
reduceThe reduce function applies a function iteratively to reduce the elements of a list or vector to a single value.
Here’s a basic example:
```
+)total_sum[1] 15``` Practical Example with Built-in DatasetLet’s use the mtcars dataset to demonstrate how accumulate and reduce can be applied in a practical scenario.
Using accumulate with mtcars
Suppose we want to calculate the cumulative sum of the miles per gallon (mpg) for each car.
```
+)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 mtcars dataset.
Using reduce with mtcars
Now, let’s say we want to find the product of all mpg values:
```
*)product_mpg[1] 1.264241e+41``` In this example, reduce calculates the product of all mpg values in the mtcars dataset.
Do It Another Way: compose and negateCreating 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.
composeThe 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 basic example:
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 this example:
Practical Example with Built-in DatasetLet’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.
```
``` In this example:
negateThe 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 simple example:
```
``` In this example:
Practical Example with Built-in DatasetLet’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 than 5.0.
```
``` In this example:
With compose and negate, you can create more flexible and powerful functions, allowing for more concise and readable code.
ConclusionCongratulations! 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 in R.
Gift for patient readersI decided to give you some useful, yet not trivial use cases of purrr functions.
Define list of function to apply on data
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 neat vector.
Let’s break it down:
Suppose that you want to apply 3 summary functions on vector of numbers. Here’s how you can do it:
number <- 1:48results <- apply\_funs(number, mean, median, sd)results[1] 24.5 24.5 14.0
Using pmap as equivalent of Python’s zipSometimes 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 reportsAssuming 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 click only.
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.
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.Continue reading: Mastering purrr: From Basic Maps to Functional Magic in R