Making Multi-Argument Functions & Data Frames Purrr
March 26 2018 • 5 min read
Image by Aleksandar Cvetanović
Why purrr?
Ah, the purrr
package for R. Months after it had been released, I was still simply amused by all of the cat-related puns that this new package invoked, but I had no idea what it did. What did it mean to make your functions “purr”?
I started seeing post after post about why Hadley Wickham’s newest R package was a game-changer. But it was actually this Stack Overflow response that finally convinced me.
Essentially, for my purposes, I could substitute for()
loops and the *apply()
family of functions for purrr
. Since I consistently mess up the syntax of *apply()
functions and have a semi-irrational fear of never-ending for()
loops, I was so ready to jump on the purrr
bandwagon. I’ve only just started dipping my toe in the waters of this package, but there’s one use-case that I’ve found insanely helpful so far: iterating a function over several variables and combining the results into a new data frame.
Note: Many
purrr
functions result in lists. In much of my work I prefer to work in data frames, so this post will focus on usingpurrr
with data frames.
Here’s what I mean.
Functions with One Argument
Imagine you have a function like this:
myFunction <- function(arg1){
# Cool stuff in here that returns a data frame!
}
# Example
myFunction <- function(arg1){
col <- arg1 * 2
x <- as.data.frame(col)
}
If you wanted to run the function once, with arg1 = 5
, you could do:
myFunction(5)
But what if you’d like to run myFunction()
for several arg1
values and combine all of the results in a data frame?
You can do it with a for()
loop:
# Define the values you'd like to loop over
values <- c(1, 3, 5, 7, 9)
# Make a blank data frame with 1 columns that you can fill
df <- data.frame(col = vector())
# Write a for loop (assuming)
for (i in 1:length(values)) {
col <- myFunction(values[i]) # returns the result of myFunction
df[i,1] <- col # places the first result into row i, column 1
}
Or using the *apply()
family:
# Define the values you'd like to loop over
values <- c(1, 3, 5, 7, 9)
# Use the apply family of functions (assuming myFunction returns a data frame)
df <- do.call(rbind, (lapply(values, (myFunction))))
Or you can use the purrr
family of map*()
functions:
# Load purrr
library(purrr)
# Define the values you'd like to loop over
values <- c(1, 3, 5, 7, 9)
# Use purrr::map_df
df <- map_dfr(values, myFunction)
There are several map*()
functions in the purrr
package and I highly recommend checking out the documentation or the cheat sheet to become more familiar with them, but map_dfr()
runs myFunction()
for each value in values
and binds the results together rowwise. If you want to bind the results together as columns, you can use map_dfc()
.
In my opinion, using purrr::map_dfr
is the easiest way to solve this problem ☝🏻 and it gets even better if your function has more than one argument.
Before we move on a few things to keep in mind:
Warning: If you use
map_dfr()
on a function that does not return a data frame, you will get the following error: Error in bind_rows_(x, .id) : Argument 1 must have names.
Note: This also works if you would like to iterate along columns of a data frame. If you had a dataframe called
df
and you wanted to iterate along columnvalues
in functionmyFunction()
, you could call:
myData <- map_dfr(df$values, myFunction)
Ok, now we can continue.
Functions with Two Arguments
Imagine you have a function with two arguments:
myFunction <- function(arg1, arg2){
# Cool stuff in here that returns a data frame!
}
# Example
myFunction <- function(arg1, arg2){
col <- arg1 * arg2
x <- as.data.frame(col)
}
There’s a purrr
function for that! Use map2_dfr()
arg1 <- c(1, 3, 5, 7, 9)
arg2 <- c(2, 4, 6, 8, 10)
df <- map2_dfr(arg1, arg2, myFunction)
If you’re dealing with 2 or more arguments, make sure to read down to the Crossing Your Argument Vectors section.
Functions with 3 or More Arguments
And if your function has 3 or more arguments, make a list of your variable vectors and use pmap_dfr()
myComplexFunction <- function(arg1, arg2, arg3, arg4){
# Still cool stuff here!
}
# Example
myComplexFunction <- function(arg1, arg2, arg3, arg4){
col <- arg1 * arg2 * arg3 * arg4
x <- as.data.frame(col)
}
arg1 <- c(1, 3, 5, 7, 9)
arg2 <- c(2, 4, 6, 8, 10)
arg3 <- c(5, 10, 15, 20, 25)
arg4 <- c(10, 20, 30, 40, 50)
argList <- list(arg1, arg2, arg3, arg4)
myData <- pmap_dfr(argList, myComplexFunction)
Pretty simple, right?
Crossing Your Argument Vectors
There’s one more thing to keep in mind with map*()
functions. If your function has more than one argument, it iterates the values on each argument’s vector with matching indices at the same time.
In other words, if you run this:
arg1 <- c(1, 3, 5, 7, 9)
arg2 <- c(2, 4, 6, 8, 10)
df <- map2_dfr(arg1, arg2, myFunction)
You are essentially running
myFunction(1, 2)
myFunction(3, 4)
myFunction(5, 6)
myFunction(7, 8)
myFunction(9, 10)
If instead, you want every possible combination of the items on this list, like this:
myFunction(1, 2)
myFunction(1, 4)
myFunction(1, 6)
myFunction(1, 8)
myFunction(1, 10)
myFunction(3, 2)
myFunction(3, 4)
# All the way to...
myFunction(9, 8)
myFunction(9, 10)
you’ll need to incorporate the cross*()
series of functions from purrr
. Each of the functions cross()
, cross2()
, and cross3()
return a list item. If you’d instead prefer a dataframe, use cross_df()
like this:
arg1 <- c(1, 3, 5, 9)
arg2 <- c(2, 4, 6, 10)
argList <- list(x = arg1, y = arg2)
crossArg <- cross_df(argList)
myData <- map2_dfr(crossArg$x, crossArg$y, myFunction)
Correction: In the original version of this post, I had forgotten that
cross_df()
expects a list of (named) arguments. The code above is now fixed. Many thanks to sf99 for pointing out the error! 🎉
And that’s it! Again, purrr
has so many other great functions (ICYMI, I highly recommend checking out possibly
, safely
, and quietly
), but the combination of map*()
and cross*()
functions are my favorites so far.