Dplyr/Rlang: Parse_Expr with Multiple Expressions

dplyr/rlang: parse_expr with multiple expressions

We can use the triple-bang operator with the plural form parse_exprs and a modified e2 expression to parse multiple expressions (see ?parse_quosures):

Explanation:

  1. Multiple expressions in e2 need to be separated either by ; or by new lines.
  2. From ?quasiquotation: The !!! operator unquotes and splices its argument. The argument should represents a list or a vector.

e2 = "vs + am ; am +vs";
mtcars %>% mutate(!!!parse_exprs(e2))
# mpg cyl disp hp drat wt qsec vs am gear carb vs + am am + vs
#1 21.0 6 160.0 110 3.90 2.620 16.46 0 1 4 4 1 1
#2 21.0 6 160.0 110 3.90 2.875 17.02 0 1 4 4 1 1
#3 22.8 4 108.0 93 3.85 2.320 18.61 1 1 4 1 2 2
#4 21.4 6 258.0 110 3.08 3.215 19.44 1 0 3 1 1 1
#5 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2 0 0
#6 18.1 6 225.0 105 2.76 3.460 20.22 1 0 3 1 1 1
#7 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4 0 0
#8 24.4 4 146.7 62 3.69 3.190 20.00 1 0 4 2 1 1
#9 22.8 4 140.8 95 3.92 3.150 22.90 1 0 4 2 1 1
#10 19.2 6 167.6 123 3.92 3.440 18.30 1 0 4 4 1 1
#11 17.8 6 167.6 123 3.92 3.440 18.90 1 0 4 4 1 1
#12 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3 0 0
#13 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3 0 0
#14 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3 0 0
#15 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4 0 0
#16 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4 0 0
#17 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4 0 0
#18 32.4 4 78.7 66 4.08 2.200 19.47 1 1 4 1 2 2
#19 30.4 4 75.7 52 4.93 1.615 18.52 1 1 4 2 2 2
#20 33.9 4 71.1 65 4.22 1.835 19.90 1 1 4 1 2 2
#21 21.5 4 120.1 97 3.70 2.465 20.01 1 0 3 1 1 1
#22 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2 0 0
#23 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2 0 0
#24 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4 0 0
#25 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2 0 0
#26 27.3 4 79.0 66 4.08 1.935 18.90 1 1 4 1 2 2
#27 26.0 4 120.3 91 4.43 2.140 16.70 0 1 5 2 1 1
#28 30.4 4 95.1 113 3.77 1.513 16.90 1 1 5 2 2 2
#29 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4 1 1
#30 19.7 6 145.0 175 3.62 2.770 15.50 0 1 5 6 1 1
#31 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8 1 1
#32 21.4 4 121.0 109 4.11 2.780 18.60 1 1 4 2 2 2

Using rlang Package to Parse Quoted Argument

Use rlang::parse_expr to convert strings to expressions and eval to evaluate them. eval() allows you to provide context for the expression in its second argument, where we supply the person data frame to it. In case of filter, the context is already understood to be the dataframe on the left-hand side of the %>% pipe.

Another difference in how we handle the two expressions is that filter() has an additional internal layer of quasiquoation. Since you already have an expression, you don't need it to be quoted again, so you would use !! to unquote it.

different_age_friends <- function(condition, p = person, f = friends) 
{
stmts <- str_split(condition, " ~ ")[[1]] %>% map( rlang::parse_expr )

if( eval(stmts[[1]], p) ) # Effectively: eval(age == 20, person)
f %>% filter(!!stmts[[2]]) # Effectively: friends %>% filter(age == 48)
else
f
}

different_age_friends(condition = "age == 20 ~ age == 48")
# # A tibble: 2 x 2
# id age
# <dbl> <dbl>
# 1 2 48
# 2 5 48

Minor note:

  • You didn't provide a value for different_age_friends when the condition is false. I made an assumption that in this case, the entire friends list is to be returned.

How to use an expression in dplyr::mutate in R

Any of these work. The second is similar to the first but does not require that rlang be on the search path. The third and fourth also work if the d= part is not present in expr in which case default names are used. The last one uses only base R and is also the shortest.

data %>% mutate(within(., !!parse_expr(expr)))

data %>% mutate(within(., !!parse(text = expr)))

data %>% mutate(data, !!parse_expr(sprintf("tibble(%s)", expr)))

data %>% { eval_tidy(parse_expr(sprintf("mutate(., %s)", expr))) }

within(data, eval(parse(text = expr))) # base R

Note

Assume this premable:

library(dplyr)
library(rlang)

# input
data <- tibble(a = c(1, 2), b = c(3, 4))
expr <- "d = a + b"

How to pass an expression to the filter() verb the tidy way?

We can select the columns of interest and extract the components with ..1, ..2 (or .x, .y - if there are only 2 columns)

library(dplyr)
library(tibble)
library(purrr)
tibble(pick_these = get_these %>%
map(rlang::parse_expr)) %>%
mutate(wat = names(get_these),
goods = list(diamonds)) %>%
mutate(goods = pmap(select(., goods, pick_these), ~ {
..1 %>%
filter(rlang::eval_tidy( ..2))

})) %>%
dplyr::select(goods, wat)

# A tibble: 2 x 2
# goods wat
# <list> <chr>
#1 <tibble [3,903 × 10]> Ideal E
#2 <tibble [307 × 10]> Good J

Functions dplyr with rlang::last_error() in purrr::map loop in r

The specific error is caused because intersect_list has empty items in the list, which cannot be joined because they are empty, and hence have no columns to join by. If you modified the map function to only use non-empty items of intersect_list you would not get that error.

As you noted in the comments, removing the empty list entries with keep(intersect_list, ~ !is.null(.)) before mapping left_join onto the list items will fix the error.

However, I don't think this is the most elegant way to solve this problem. I might misunderstand what the goal is, but if it's to produce a raster from the total length of lines within each grid cell, I think a simpler approach without using purrr might work.

This is not the exact same as your product, but I'm keeping it simpler rn to illustrate an alternate approach. Here is a sum of the lengths in each cell as a stars object (similar to raster but plays better with the tidyverse and sf).

I'm starting off from your objects one_df and grid:

# Turn multiple lines into single MULTILINESTRING:
one_df %>%
st_union() ->
union_df

# Intersection of each grid cell with the MULTILINESTRING geometry:
grid %>%
st_intersection(union_df) ->
grid_lines

# Get lengths:
grid_lines %>%
mutate(length = st_length(x)) %>%
st_drop_geometry() ->
grid_lengths

# Join the calculated lengths back with the spatial grid,
# most of which will have NA for length
grid %>%
left_join(grid_lengths, by = "cell") ->
grid_with_lengths

# Rasterize the length field of the grid
grid_with_lengths %>%
dplyr::select(length) %>%
stars::st_rasterize() ->
length_stars

length_stars %>% mapview::mapview()

stars object viewed with mapview

rlang Expressions in List as Args to Function

After parse_exprs(), you end up with assignment expressions stored in an unnamed list:

# [[1]]
# base = 10
#
# [[2]]
# offset = 0

while, to get the effect you want with !!!, they need to be values in a named list:

# $base
# [1] 10
#
# $offset
# [1] 0

With that said, I suggest "forwarding the dots" instead, because it leads to a simpler and more flexible implementation:

add_step <- function(x, step, selector, ...){
f <- parse_expr(paste0("step_", step))
vars <- sym(selector)
step_expr <- call2(f, vars, ...) # <---- forwarding the dots here

expr(!!x %>% !!step_expr)
}

The user can now simply provide the desired arguments directly to add_step():

rcp %>% add_step(user_step, user_selector, base=10, offset=0)

# recipe(mpg ~ cyl + hp + wt + disp, data = mtcars) %>%
# step_log(disp, base = 10, offset = 0)

OR store them in a list and use !!! on your function:

user_args <- list(base = 10, offset=0)
rcp %>% add_step(user_step, user_selector, !!!user_args)

R dplyr pass expression as argument to function

Any of these seems to work with parsing expression: parse_expr() and parse_quo()

library(rlang)
library(dplyr)

test_func1 <- function(data, myexpr){
data %>%
filter(!!parse_expr(myexpr))
}

test_func2 <- function(data, myexpr){
data %>%
filter(!!parse_quo(myexpr, env = global_env()))
}

mtcars %>%
test_func1(myexpr = "cyl > 6")
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
#> Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
#> Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
#> Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
#> Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
#> Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
#> Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
#> Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
#> Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
#> AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
#> Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
#> Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
#> Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4
#> Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8

mtcars %>%
test_func2(myexpr = "cyl > 6")
#> mpg cyl disp hp drat wt qsec vs am gear carb
#> Hornet Sportabout 18.7 8 360.0 175 3.15 3.440 17.02 0 0 3 2
#> Duster 360 14.3 8 360.0 245 3.21 3.570 15.84 0 0 3 4
#> Merc 450SE 16.4 8 275.8 180 3.07 4.070 17.40 0 0 3 3
#> Merc 450SL 17.3 8 275.8 180 3.07 3.730 17.60 0 0 3 3
#> Merc 450SLC 15.2 8 275.8 180 3.07 3.780 18.00 0 0 3 3
#> Cadillac Fleetwood 10.4 8 472.0 205 2.93 5.250 17.98 0 0 3 4
#> Lincoln Continental 10.4 8 460.0 215 3.00 5.424 17.82 0 0 3 4
#> Chrysler Imperial 14.7 8 440.0 230 3.23 5.345 17.42 0 0 3 4
#> Dodge Challenger 15.5 8 318.0 150 2.76 3.520 16.87 0 0 3 2
#> AMC Javelin 15.2 8 304.0 150 3.15 3.435 17.30 0 0 3 2
#> Camaro Z28 13.3 8 350.0 245 3.73 3.840 15.41 0 0 3 4
#> Pontiac Firebird 19.2 8 400.0 175 3.08 3.845 17.05 0 0 3 2
#> Ford Pantera L 15.8 8 351.0 264 4.22 3.170 14.50 0 1 5 4
#> Maserati Bora 15.0 8 301.0 335 3.54 3.570 14.60 0 1 5 8

Created on 2020-06-16 by the reprex package (v0.3.0)

How to use dynamic tidy-select expressions in dplyr::select()?

You can't just pass in a string. A string is not the same as an expression. One way is to use purrr and rlang to build the expression and then inject that into the select

library(purrr)
library(rlang)
query <- map2(
map(x, ~expr(contains(!!.x))),
map(names(x), ~expr(contains(!!.x))),
~expr((!!.x & !!.y))) %>%
reduce(~expr(!!.x | !!.y))
dplyr::select(df, !!query)

Though if you really wanted to build the code as a string, then you would just need to parse that string into an expression first using rlang::parse_expr. You just need to add some quotes to your string so it exactly matches the code you used before

s <- paste0("(", paste0("contains(\"", names(x),"\") & contains(\"", x, "\")"), ")", collapse = " | ")
dplyr::select(df, !!rlang::parse_expr(s))


Related Topics



Leave a reply



Submit