12

I have a dataframe with a set of objects df$data and a set of rules to be applied on every object df$rules.

df <- data.frame(
  data = c(1,2,3),
  rules = c("rule1", "rule1, rule2, rule3", "rule3, rule2"),
  stringsAsFactors = FALSE
)

The rules are

rule1 <- function(data) {
  data * 2
}

rule2 <- function(data) {
  data + 1
}

rule3 <- function(data) {
  data ^ 3
}

For every row in the dataframe I want to apply all the rules specified in the rules column. The rules should be applied in series.

What I figured out:

apply_rules <- function(data, rules) {
  for (i in 1:length(data)) {
    rules_now <- unlist(strsplit(rules[i], ", "))
    for (j in 1:length(rules_now)) {
      data[i] <- apply_rule(data[i], rules_now[j])
    }
  }
  return(data)
}

apply_rule <- function(data, rule) {
  return(sapply(data, rule))
}


apply_rules(df$data, df$rules)
# [1]   2 125  28

Although this works I'm pretty sure there must be more elegant solutions. On SO I could find lot's of stuff about the apply-functions and also one post about applying many functions to a vector and something about chaining functions. The Compose idea looks promising but I couldn't figure out how to make a call to Compose with my rules as string. (parse() didn't work..)

Any hints?

symbolrush
  • 7,123
  • 1
  • 39
  • 67
  • Try `mutate` function from `tidyverse` packages. https://dplyr.tidyverse.org/reference/mutate.html – Tito Sanz May 17 '18 at 12:12
  • @TitoSanz: thanks. I just don't see how this helps. How should I call `mutate` with the rules specified in `df$rules`? – symbolrush May 17 '18 at 12:38

3 Answers3

2

You can use mapply and Reduce together with mget in this case.

mapply(function(d,r) Reduce(function(lhs,rhs) rhs(lhs),
                            c(d,mget(strsplit(r,", ")[[1]],envir = globalenv())))
       ,df$data
       ,df$rules)

# [1] 2 125  28

You might have to adjust the envir argument of mget to your specific case. It would probably be more robust to explicitly pass the environment where your rules are defined to mget.

cryo111
  • 4,444
  • 1
  • 15
  • 37
2

Some good answers already but throw in another option - build a pipe chain as a string then evaluate it. For example - for row 1 - eval(parse(text = "1 %>% rule1")) gives 2

eval_chain <- function(df) {
    eval(parse(text = paste(c(df$data, unlist(strsplit(df$rules, ", "))), collapse=" %>% ")))
}

df$value <- sapply(1:nrow(df), function(i) df[i, ] %>% eval_chain)
  # data               rules value
# 1    1               rule1     2
# 2    2 rule1, rule2, rule3   125
# 3    3        rule3, rule2    28
CPak
  • 13,260
  • 3
  • 30
  • 48
1

I think you have to change the approach a little (expressions will only make things worse in this case):

df <- data.frame(
  data = c(1,2,3),
  rules = c("rule1", "rule1, rule2, rule3", "rule3, rule2"),
  stringsAsFactors = FALSE
)

# list of functions
fun_list <- list(
  rule1 = function(x) x*2,
  rule2 = function(x) x+1,
  rule3 = function(x) x^3
)

# function to call list of functions
call_funs <- function(x, fun_vec) {
  for (i in seq_along(fun_vec)) {
    x <- fun_list[[fun_vec[[i]]]](x)
  }
  x
}

(want <- unlist(Map(call_funs, df$data, strsplit(gsub(" ", "", df$rules), ","))))    
#   2 125  28
r.user.05apr
  • 5,356
  • 3
  • 22
  • 39