ifelse()
ifelse(test, yes, no) The ifelse() function is R’s vectorized conditional operator. It tests each element of a vector and returns corresponding values from either the yes or no argument. This makes it essential for data transformation tasks where you need to apply conditional logic across entire vectors.
Syntax
ifelse(test, yes, no)
Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
| test | logical vector | , | Condition to evaluate for each element |
| yes | vector | , | Values returned where test is TRUE |
| no | vector | , | Values returned where test is FALSE |
Examples
Basic usage
x <- 1:5
ifelse(x > 2, "big", "small")
# [1] "small" "small" "big" "big" "big"
With strings
The ifelse() function works with character vectors just as naturally as with numeric ones. The yes and no arguments can be character strings, and the output inherits the type of those arguments. A typical grading workflow replaces numeric scores with categorical labels like "Pass" or "Fail" by testing each element against a threshold condition, producing a character vector of the same length as the input without any explicit loop or indexing.
scores <- c(85, 92, 78, 55, 90)
result <- ifelse(scores >= 60, "Pass", "Fail")
result
# [1] "Pass" "Pass" "Fail" "Fail" "Pass"
Preserving NA values
An important behaviour of ifelse() is how it handles missing values in the test vector. When test[i] is NA, the result at position i is also NA, regardless of what the yes and no arguments specify. This propagation respects the uncertainty of missing data: if you cannot determine whether the condition holds, you should not return a definite answer. This differs from some database CASE WHEN implementations that treat NULL as a falsy value and return the else branch.
x <- c(1, 2, NA, 4, 5)
ifelse(x > 3, "big", "small")
# [1] "small" "small" NA "big" "big"
Common patterns
Recoding values
Recoding categorical variables is a common data cleaning task that ifelse() handles well when paired with the %in% operator. Instead of writing multiple nested conditions for each category, you can test set membership to collapse several related values into a single group. This pattern appears frequently when standardising survey responses, merging sparse factor levels, or creating broader classification bins from detailed categorical data before building summary tables or visualisations.
colors <- c("red", "blue", "red", "green")
ifelse(colors %in% c("red", "blue"), "primary", "other")
# [1] "primary" "primary" "primary" "other"
Creating flags
Binary flag creation is one of the simplest and most common uses of ifelse(). The function returns a numeric 1 or 0 for each element based on a logical condition, which can then be summed to count occurrences or used as a grouping indicator in downstream aggregations. This pattern replaces explicit for loops that would manually populate a flag vector element by element, keeping the code short and declarative while still being immediately readable.
temperature <- c(15, 22, 18, 30, 25)
hot_day <- ifelse(temperature > 25, 1, 0)
hot_day
# [1] 0 0 0 1 0
Conditional transformation
When you need to transform only a subset of values while leaving others unchanged, ifelse() provides a concise vectorized alternative to manual subset assignment with bracket indexing. The example below applies a ten percent discount to prices above fifty while returning the original price for cheaper items. Note that both the yes and no expressions are evaluated for every element before selection occurs, so avoid expensive computations inside the arguments if performance matters for large datasets.
prices <- c(10, 50, 100, 200, 25)
discounted <- ifelse(prices > 50, prices * 0.9, prices)
discounted
# [1] 10 45 90 180 25
ifelse() in practice
ifelse() is a vectorized conditional: it evaluates test, and for each TRUE returns the corresponding element from yes, for each FALSE from no, and for each NA returns NA. Unlike if(), which works on a single logical value, ifelse() processes entire vectors in one call.
Both yes and no are evaluated fully before ifelse() runs the selection, only values from the chosen argument are used, but both arguments are computed. This matters for functions with side effects: ifelse(condition, expensive_function(x), cheap_value) calls expensive_function(x) for all rows, not just those where condition is TRUE. If conditional evaluation is needed, use dplyr::if_else() or a for loop.
Type coercion: ifelse() returns a vector of the type determined by the common type of yes and no, after coercion. Mixing a character and a numeric coerces both to character. This silent coercion is a source of bugs when yes and no have different types, use dplyr::if_else() which requires matching types and raises an error on mismatch.
For multiple conditions, nested ifelse() calls become hard to read. The dplyr::case_when() function handles multi-condition branching more legibly: case_when(x < 0 ~ "negative", x == 0 ~ "zero", TRUE ~ "positive").
ifelse() is a vectorized conditional that evaluates both yes and no for all elements, then selects based on test. This means it can produce unexpected results with side effects and does not short-circuit. The yes and no arguments must be the same type or coercible to a common type. For non-vectorized if-else, use if (condition) value_if_true else value_if_false. In tidyverse code, dplyr::if_else() is stricter — it requires true and false to be the same type.
Multiple conditions with nested ifelse
When two choices are not enough, nesting ifelse() calls handles three or more categories. Keep nesting to two or three levels at most — beyond that, dplyr::case_when() is clearer:
scores <- c(45, 72, 88, 95, 33, 60, 78)
grade <- ifelse(scores >= 90, "A",
ifelse(scores >= 80, "B",
ifelse(scores >= 70, "C",
ifelse(scores >= 60, "D", "F"))))
data.frame(scores, grade)
# scores grade
# 1 45 F
# 2 72 C
# 3 88 B
# 4 95 A
# 5 33 F
# 6 60 D
# 7 78 C
Each condition is checked in order from the outside in, so scores >= 90 is tested first and the remaining conditions receive only values that did not already match. The deeply nested parentheses are the main readability cost — a common convention is to align each ifelse( on a new indented line so the structure mirrors what case_when() would express more directly.
Date-aware binning with ifelse and weekdays()
dates <- as.Date(c("2026-01-05", "2026-01-10", "2026-01-11", "2026-01-12"))
day_type <- ifelse(weekdays(dates) %in% c("Saturday", "Sunday"),
"weekend", "weekday")
data.frame(date = dates, day_type)
# date day_type
# 1 2026-01-05 weekday
# 2 2026-01-10 weekend
# 3 2026-01-11 weekend
# 4 2026-01-12 weekday
weekdays() returns the day name as a character string, and %in% checks membership in the weekend set. This two-function pipeline replaces a multi-line for loop with a single vectorized expression that is easy to read and unit test. For locale-sensitive day names (e.g., “samedi” instead of “Saturday” on a French system), either set Sys.setlocale("LC_TIME", "C") before calling weekdays() or use lubridate::wday(dates, label = TRUE) which returns locale-independent factor labels.
See also
- which()
- intersect()
- head()
dplyr::case_match(x, val1 ~ result1, val2 ~ result2, .default = other)is a cleaner multi-way switch alternative to nestedifelse().A key limitation ofifelse()is that it evaluates bothyesandnoarguments for all elements before selecting — there is no short-circuit evaluation. This meansifelse(x > 0, log(x), 0)producesNaNwarnings for negativexeven though those positions use the0result.dplyr::if_else(x > 0, log(pmax(x, 0)), 0)avoids this by clamping the input before the log.data.table::fcase()is a fast multi-condition alternative similar to SQLCASE WHEN.