Given the following code:
a <- 3
colors <- ifelse(a == 3,
c("#004B93", "#389DC3", "#6DBE99"),
ifelse(a == 2, c("#004B93", "#389DC3"), c("#000000", "#000000", "#000000")))
My expectation ist to get something like
> colors
[1] "#004B93" "#389DC3" "#6DBE99"
But what I get is
> colors
[1] "#004B93"
What am I doing wrong?
You can also use if else statements for check the conditions inside R.
For that you can do the same logic as I checked for you.
a <- 3
colors <- if(a == 3) {
c("#004B93", "#389DC3", "#6DBE99")
} else if (a == 2) {
c("#004B93", "#389DC3")
} else {
c("#000000", "#000000", "#000000")
}
print(colors)
Output Result :
[1] "#004B93" "#389DC3" "#6DBE99"
Here's how I would write this:
possible_colors = list(
c("#000000", "#000000", "#000000"),
c("#004B93", "#389DC3"),
c("#004B93", "#389DC3", "#6DBE99")
)
colors = if (a < 1L || a > 3L) list[[1L]] else list[[a]]
This assumes that a only has integer values. Adjust the condition of the if accordingly if that is not the case.
If you know that a will never be any value except 1, 2 or 3, you can omit the if completely and directly write colors = possible_colors[[a]].
ifelse is a command that is usually better to avoid.
Why? Because
ifelse(TRUE, 1:3, 4:6)
returns 1 and not 1:3.
In fact, ifelse wants the output to be the same length as the test. Above TRUE is a vector of length 1, while the candidate output1:3 is of length 3. However, ifelse does not raise any warning, it just cuts the vector 1:3.
There is rationale behind this, and this example should explain it:
ifelse(c(TRUE, FALSE, TRUE), yes = 1:3, no = 4:6)
[1] 1 5 3
True values are taken from the yes argument and false values are
taken from the no argument. If you really want this sophisticate behaviour, ifelse can be extremely powerful, but this is rarely what you want.
The good news is that R is a functional language, thus everything is a function, including the if/else construct. Hence, to get the intended result from
ret <- ifelse(TRUE, 1:3, 4:6)
rewrite it like so:
ret <- if(TRUE) 1:3 else 4:6
and ret is 1:3
Do you need nested conditions? No problem.
a <- 1
ret <- if(a == 1) 1:3 else (if(a == 2) 4:6 else 7:9)
The line above emphasises the functional approach, where I replace the expression 4:6 with the bracketed one, but it can be shortened to:
ret <- if(a == 1) 1:3 else if(a == 2) 4:6 else 7:9
However, let me note that for this type of tasks, there is a dedicated R function: switch.
ret <- switch(a, 1:3, 4:6, 7:9)
Counting after a, the first, second, etc. argument is chosen depending on the value of a, thus:
a <- 2
switch(a, 1:3, 4:6, 7:9)
# 4:6
As for your case:
a <- 3
switch(a,
c("#000000", "#000000", "#000000"),
c("#004B93", "#389DC3"),
c("#004B93", "#389DC3", "#6DBE99"))
# [1] "#004B93" "#389DC3" "#6DBE99"
Related
I'm trying to write a recursive function that builds a nested ifelse call. I do realize there are much better approaches than nested ifelse, e.g., dplyr::case_when and data.table::fcase, but I'm trying to learn how to approach such problems with metaprogramming.
The following code builds out the nested ifelse, but I'm struggling to substitute data with the actual supplied value, in this case my_df.
If I replace quote(data) with substitute(data), it only works for the first ifelse, but after entering the next iteration, it turns into data.
I think something like pryr::modify_lang could solve this after the fact, but I think there's probably a base R solution someone knows.
my_df <- data.frame(group = letters[1:3],
value = 1:3)
build_ifelse <- function(data, by, values, iter=1){
x <- call("ifelse",
call("==",
call("[[", quote(data), by),
values[iter]),
1,
if(iter != length(values)) build_ifelse(data, by, values, iter = iter + 1) else NA)
return(x)
}
build_ifelse(data = my_df, by = "group", values = letters[1:3])
# ifelse(data[["group"]] == "a", 1, ifelse(data[["group"]] == "b",
# 1, ifelse(data[["group"]] == "c", 1, NA)))
Thanks for any input!
Edit:
I found this question/answer: https://stackoverflow.com/a/59242109/9244371
Based on that, I found a solution that seems to work pretty well:
build_ifelse <- function(data, by, values, iter=1){
x <- call("ifelse",
call("==",
call("[[", quote(data), by),
values[iter]),
1,
if(iter != length(values)) build_ifelse(data, by, values, iter = iter + 1) else NA)
x <- do.call(what = "substitute",
args = list(x,
list(data = substitute(data))))
return(x)
}
build_ifelse(data = my_df, by = "group", values = letters[1:3])
# ifelse(my_df[["group"]] == "a", 1, ifelse(my_df[["group"]] ==
# "b", 1, ifelse(my_df[["group"]] == "c", 1, NA)))
eval(build_ifelse(data = my_df, by = "group", values = letters[1:3]))
# [1] 1 1 1
There is a base function, switch, that can deliver sequential testing and results similar to dplyr::case_when, at least when used with a loop wrapper. It's not well documented. It is really two different functions, one that expects a numeric input for it classification variable and another that expects character values. I can never remember it's name, and so typically I need to remind myself that it is referenced in the ?Control page. Since you're using character values, here goes. (I changed the outputs so you can see that some degree of substitution is occurring and that there is an "otherwise" option
sapply( my_df$group, switch, a=4, b=5, d=6, NA)
a b c
4 5 NA
I want to find the P(X<Y<Z) in r. For each value of z_i, I want to check whether it satisfies the conditions or not. I demonstrated the problem below. Here I used the ifelse function in r. I don't how to put multiple statements within ifelse. When I type ifelse(z[i]>y>x, 1, 0) I get errors. I want to know how to include this.
x = c(1,1)
y = c(2,2)
z = c(3,3)
value = NULL
n1 = length(x)
n2 = length(y)
n3 = length(z)
for(i in 1: length(z)){
value[i] = sum (ifelse(z[i]>y & z[i]> x & y > x, 1, 0))
}
value
The desired output should be 4 4. But the above code gives 2 2. Thanks in advance.
I have the following three parameters:
a <- c("brown", "red", "purple", "yellow", "blue", "green")
b <- c("unknown", "medium", "low", "high")
c <- c("group1", "group2")
I want to loop through each combination of parameters in order to obtain a list of names of those very parameters. The result should look like the following:
$plot_group1_unknown_brown
[1] "plot_group1_unknown_brown"
$plot_group2_unknown_brown
[1] "plot_group2_unknown_brown"
$plot_group1_medium_brown
[1] "plot_group1_medium_brown"
$plot_group2_medium_brown
[1] "plot_group2_medium_brown"
$plot_group1_low_brown
[1] "plot_group1_low_brown"
.
.
.
Now I want achieve this using any of the apply family of functions or really any approach other than a for loop. I can accomplish this with a for loop like so:
for (x in a) {
for (a in b) {
for (i in c) {
plot_name <- paste( 'plot', i, a, x, sep = '_' )
plot_list[[plot_name]] <- plot_name
}
}
}
I would also like to note that this is just an example. In my actual use case, plot_name (on the RHS) would be a function that takes the same parameters as the ones used inside the paste() function above it. Something along the lines of plot_list[[plot_name]] <- some_plotting_function(x, i, a) and the result would be a list of ggplot2 objects.
We can use expand.grid on the vectors and paste
v1 <- paste0('plot_', do.call(paste, c(expand.grid(c, b, a), sep="_")))
lst1 <- setNames(as.list(v1), v1)
head(lst1)
#$plot_group1_unknown_brown
#[1] "plot_group1_unknown_brown"
#$plot_group2_unknown_brown
#[1] "plot_group2_unknown_brown"
#$plot_group1_medium_brown
#[1] "plot_group1_medium_brown"
#$plot_group2_medium_brown
#[1] "plot_group2_medium_brown"
#$plot_group1_low_brown
#[1] "plot_group1_low_brown"
#$plot_group2_low_brown
#[1] "plot_group2_low_brown"
You could use expand.grid to get all possible combinations and then use apply row-wise to pass the parameters to the function.
complete_list <- expand.grid(c, b, a)
#Can also use cross_df from purrr
#complete_list <- purrr::cross_df(list(c = c, b = b, a = a))
apply(complete_list, 1, function(x) some_plotting_function(x[1], x[2], x[3]))
Assume I have a value x which is of some (unknown) type (especially: scalar, vector or list). I would like to get the R expression representing this value. If x == 1 then this function should simply return expression(1). For x == c(1,2)) this function should return expression(c(1,2)). The enquote function is quite near to that what I want, but not exactly.
By some playing around I found the following "solution" to my problem:
get_expr <- function(val) {
tmp_expr <- enquote(val)
tmp_expr[1] <- quote(expression())
return(eval(tmp_expr))
}
get_expr(1) # returns expression(1)
get_expr(c(1, 2)) # returns expression(c(1, 2))
get_expr(list(x = 1)) # returns expression(list(x = 1))
But I think my get_expr function is some kind of hack. Logically, the evaluation should not be necessary.
Is there some more elegant way to do this? As far as I see, substitute does not really work for me, because the parameter of my get_expr function may be the result of an evaluation (and substitute(eval(expr)) does not do the evaluation).
I found another way via parse(text = deparse(val)), but this is even more a bad hack...
as.expression(list(...)) seems to do it:
> get_expr <- function(val) as.expression(list(val))
> str(get_expr(1))
expression(1)
> str(get_expr(c(1, 2)))
expression(c(1, 2))
> str(get_expr(list(x=1)))
expression(list(x = 1))
> val <- list(x=1, y=2)
> str(get_expr(val))
expression(list(x = 1, y = 2))
You can use substitute(), and just need to call it a bit differently:
express <- function(e) substitute(expression(x), env = list(x=e))
v1 <- c(1, 2)
express(v1)
# expression(c(1, 2))
v2 <- list(a = 1, b = 2)
express(v2)
# expression(list(a = 1, b = 2))
I have a dataframe with million of rows and ten columns.
My code seems to work but never finish cause of the for loop and if statement I think.
I want to write it differently but I'm stuck.
df <- data.frame(x = 1:5,
y = c("a", "a", "b", "b", "c"),
z = sample(5))
for (i in seq_along(df$x)){
if (df$y[i] == df$y[i+1] & df$y[i] == "a"){
df$status[i] <- 1
} else {
df$status[i] <- "ok"
}
}
In fact, you can replace the whole loop by a vectorised ifelse:
df$status = ifelse(df$y == df$y[-1] & df$y == 'a', 1, 'ok')
This code will give you a warning, unlike the for loop. However, the warning is actually correct and also concerns your code: you are reading past the last element of df$y when doing df$y[i + 1].
You can make this warning go away (and make the code arguably clearer) by borrowing the lead function from dplyr (simplified):
lead = function (x, n = 1, default = NA) {
if (n == 0)
return(x)
`attributes<-`(c(x[-seq_len(n)], rep(default, n)), attributes(x))
}
With this, you can rewrite the code ever so slightly and get rid of the warning:
df$status = ifelse(df$y == lead(df$y) & df$y == 'a', 1, 'ok')
It’s a shame that this function doesn’t seem to exist in base R.