Skip to content

How to use ifelse without invoking warnings

2 messages · Leonard Mada

#
Dear Ravi,


I wrote a small replacement for ifelse() which avoids such unnecessary 
evaluations (it bothered me a few times as well - so I decided to try a 
small replacement).


### Example:
x = 1:10
FUN = list();
FUN[[1]] = function(x, y) x*y;
FUN[[2]] = function(x, y) x^2;
FUN[[3]] = function(x, y) x;
# lets run multiple conditions
# eval.by.formula(conditions, FUN.list, ... (arguments for FUN) );
eval.by.formula((x > 5 & x %% 2) ~ (x <= 5) ~ ., FUN, x, x-1)
# Example 2
eval.by.formula((x > 5 & x %% 2) ~ (x <= 5) ~ ., FUN, 2, x)


### Disclaimer:
- NOT properly tested;


The code for the function is below. Maybe someone can experiment with 
the code and improve it further. There are a few issues / open 
questions, like:

1.) Best Name: eval.by.formula, ifelse.formula, ...?

2.) Named arguments: not yet;

3.) Fixed values inside FUN.list

4.) Format of expression for conditions:

expression(cond1, cond2, cond3) vs cond1 ~ cond2 ~ cond3 ???

5.) Code efficiency

- some tests on large data sets & optimizations are warranted;


Sincerely,


Leonard

=======

The latest code is on Github:

https://github.com/discoleo/R/blob/master/Stat/Tools.Formulas.R


eval.by.formula = function(e, FUN.list, ..., default=NA) {
 ?? ?tok = split.formula(e);
 ?? ?if(length(tok) == 0) return();
 ?? ?FUN = FUN.list;
 ?? ?# Argument List
 ?? ?clst = substitute(as.list(...))[-1];
 ?? ?len? = length(clst);
 ?? ?clst.all = lapply(clst, eval);
 ?? ?eval.f = function(idCond) {
 ?? ???? sapply(seq(length(isEval)), function(id) {
 ?? ???? ??? if(isEval[[id]] == FALSE) return(default);
 ?? ???? ??? args.l = lapply(clst.all, function(a) if(length(a) == 1) a 
else a[[id]]);
 ?? ???? ??? do.call(FUN[[idCond]], args.l);
 ?? ???? });
 ?? ?}
 ?? ?# eval 1st condition:
 ?? ?isEval = eval(tok[[1]]);
 ?? ?rez = eval.f(1);
 ?? ?if(length(tok) == 1) return(rez);
 ?? ?# eval remaining conditions
 ?? ?isEvalAll = isEval;
 ?? ?for(id in seq(2, length(tok))) {
 ?? ???? if(tok[[id]] == ".") {
 ?? ???? ??? # Remaining conditions: tok == ".";
 ?? ???? ??? # makes sens only on the last position
 ?? ???? ??? if(id < length(tok)) warning("\".\" is not last!");
 ?? ???? ??? isEval = ! isEvalAll;
 ?? ???? ??? rez[isEval] = eval.f(id)[isEval];
 ?? ???? ??? next;
 ?? ???? }
 ?? ???? isEval = rep(FALSE, length(isEval));
 ?? ???? isEval[ ! isEvalAll] = eval(tok[[id]])[ ! isEvalAll];
 ?? ???? isEvalAll[isEval] = isEval[isEval];
 ?? ???? rez[isEval] = eval.f(id)[isEval];
 ?? ?}
 ?? ?return(rez);
}


# current code uses the formula format:
# cond1 ~ cond 2 ~ cond3

# tokenizes a formula in its parts delimited by "~"
# Note:
# - tokenization is automatic for ",";
# - but call MUST then use FUN(expression(_conditions_), other_args, ...);
split.formula = function(e) {
 ?? ?tok = list();
 ?? ?while(length(e) > 0) {
 ?? ???? if(e[[1]] == "~") {
 ?? ???? ??? if(length(e) == 2) { tok = c(NA, e[[2]], tok); break; }
 ?? ???? ??? tok = c(e[[3]], tok);
 ?? ???? ??? e = e[[2]];
 ?? ???? } else {
 ?? ???? ??? tok = c(e, tok); break;
 ?? ???? }
 ?? ?}
 ?? ?return(tok);
}
#
Dear Ravi,


I have uploaded on GitHub a version which handles also constant values 
instead of functions.


Regarding named arguments: this is actually handled automatically as well:

eval.by.formula((x > 5 & x %% 2) ~ (x <= 5) ~ ., FUN, y=2, x)
# [1]? 1? 4? 9 16 25? 6 14? 8 18 10
eval.by.formula((x > 5 & x %% 2) ~ (x <= 5) ~ ., FUN, x=2, x)
# [1]? 4? 4? 4? 4? 4? 2 14? 2 18? 2
eval.by.formula((x > 5 & x %% 2) ~ (x <= 5) ~ ., list(FUN[[1]], 0, 1), 
y=2, x)
 ?# [1]? 0? 0? 0? 0? 0? 1 14? 1 18? 1


But it still needs proper testing and maybe optimization: it is possible 
to run sapply on the filtered sequence (but I did not want to break 
anything now).


Sincerely,


Leonard
On 10/9/2021 9:26 PM, Leonard Mada wrote: