R PROGRAMMING • SYNTAX AND CORE TYPES

ifelse() — Use ifelse() for vectorized conditional assignment (intro)

Apply conditional logic element-wise across entire vectors without writing a single loop.

Historical Context & Motivation

R was designed from the ground up as a vectorized language, meaning that its core operations apply to entire vectors at once rather than requiring element-by-element iteration. This design philosophy traces back to the S language created at Bell Labs in the 1970s, where statisticians needed a concise way to manipulate large datasets without the boilerplate of explicit loops. When Ross Ihaka and Robert Gentleman reimplemented S as R in the early 1990s at the University of Auckland, they preserved and extended this vectorized paradigm, recognizing that most data analysis tasks involve applying the same transformation or classification rule uniformly across collections of observations.

The scalar if...else control-flow statement handles branching for single logical values, but applying it across a vector of, say, ten thousand test scores requires a for loop—verbose, slow in interpreted R, and antithetical to R's vectorized idiom. The ifelse() function was introduced precisely to fill this gap: a single call that evaluates a logical condition element-wise and returns a vector of results, choosing values from one of two supplied alternatives at each position. This function embodies R's philosophy that operations on data should read like concise mathematical expressions rather than procedural recipes.

1976
S Language at Bell Labs
John Chambers and colleagues create S, establishing the vectorized semantics and functional style that would later influence R's design, including element-wise conditional operations.
1993
R Language Born
Ross Ihaka and Robert Gentleman release R, inheriting S's vectorized ifelse() function as part of the base language and extending it with R's scoping rules and object system.
2000
R 1.0.0 Released
The first stable release of R formalizes ifelse() in base R. Its documentation explicitly notes the function returns a value of the same shape as test, cementing its vectorized semantics.
2016+
Tidyverse Alternatives Emerge
The dplyr package introduces if_else() and case_when() as stricter-typed alternatives, but base R's ifelse() remains foundational and universally available.

The central question that ifelse() answers is deceptively simple: How do you apply a conditional rule to every element of a vector in a single, readable expression, without sacrificing performance or clarity? Understanding this function is a gateway to thinking in R's vectorized paradigm rather than writing procedural code.

Core Principles & Definitions

Before diving into syntax, it is essential to understand the conceptual pillars that make ifelse() work. The function operates on three arguments and produces a result vector whose length matches the test vector. Each position in the output is independently determined by the corresponding logical value in test. This element-wise independence is the defining characteristic that distinguishes ifelse() from the scalar if...else construct.

1

Vectorized Evaluation

The test argument is a logical vector. R evaluates every element simultaneously—no loop required. The result vector has the same length as test.
2

Three-Argument Signature

The call signature is ifelse(test, yes, no). For each TRUE in test, the corresponding element of yes is selected; for each FALSE, the element of no is used.
3

Recycling Rule

If yes or no are shorter than test, R recycles them. A scalar is broadcast to every position—a pattern familiar from NumPy's broadcasting.
4

Both Branches Evaluated

Unlike if...else, ifelse() evaluates both yes and no fully before selecting elements—be cautious with side effects.
5

NA Propagation

If an element of test is NA, the corresponding output element is also NA. The function does not guess; missing data stays missing.
KEY TAKEAWAY
Think of ifelse() like a multiplexer in digital logic: it has a selector input (the test vector) and two data inputs (yes and no). At each bit position, the selector wire routes either data-input-A or data-input-B to the output line. The entire selection happens in parallel, not sequentially—just as a hardware MUX operates on all its gates simultaneously.

Visual Explanation

The following diagram illustrates how ifelse() processes a concrete example. Consider a numeric vector x <- c(85, 42, 73, 91, 56) and the call ifelse(x >= 60, "Pass", "Fail"). The diagram shows how the logical test vector acts as a routing mask, directing the selection from either the yes or no branch at each index.

The test vector acts as a selector mask. Green dashed arrows route TRUE positions to the yes branch; red dashed arrows route FALSE positions to the no branch. The scalar strings "Pass" and "Fail" are recycled to match the length of test.

Notice that the result vector preserves the positional correspondence of the input. Index 1 of x (85) maps to index 1 of the output ("Pass"), index 2 (42) maps to index 2 ("Fail"), and so on. This one-to-one, position-preserving mapping is what makes ifelse() safe to use inside data-frame pipelines—the output vector aligns with the rows of the original data without any reordering.

How ifelse() Works Internally

Understanding the internal mechanism of ifelse() clarifies several behaviors that can surprise newcomers. The function is implemented in base R (you can inspect it by typing ifelse at the console without parentheses). Conceptually, its logic can be expressed as a pseudocode algorithm that leverages R's vectorized indexing.

Pseudocode Representation

IFELSE SEMANTICS
result[i] = { yes[i] if test[i] = TRUE, no[i] if test[i] = FALSE, NA if test[i] = NA }
Where i ranges from 1 to length(test). The vectors yes and no are recycled if their lengths differ from test. The output inherits attributes (including class) from test, not from yes or no.

Step-by-Step Internal Execution

  1. Allocate output: Create a vector of the same length and mode as test, initialized with NA values.
  2. Evaluate both branches: R computes yes and no in their entirety. This is why side-effect-producing expressions in either branch execute for all elements.
  3. Fill TRUE positions: Using vectorized indexing, assign result[test & !is.na(test)] <- yes[test & !is.na(test)].
  4. Fill FALSE positions: Assign result[!test & !is.na(test)] <- no[!test & !is.na(test)].
  5. NA positions remain NA: Positions where test is NA are never overwritten, preserving missingness.
⚠️ Attribute Inheritance Gotcha
Because the output vector inherits the shape and attributes of test, class information from yes or no may be stripped. A classic pitfall: ifelse(test, as.Date("2024-01-01"), as.Date("2024-12-31")) returns raw numeric values, not Date objects. The dplyr function if_else() fixes this by enforcing type consistency.
RECYCLING RULE
yes_recycled[i] = yes[(i − 1) mod length(yes) + 1]
When yes is a scalar (length 1), (i − 1) mod 1 + 1 = 1 for all i, so every TRUE position receives that single value. The same logic applies to no.

Common Usage Patterns & Nesting

In practice, ifelse() appears in several recurring patterns. The simplest is binary classification—assigning one of two labels based on a threshold, as shown in the visual section. More complex scenarios involve nested calls, where the no argument is itself another ifelse(), implementing multi-way classification analogous to an if...else if...else chain.

A decision tree showing three nested ifelse() calls. The outermost call checks score >= 90; its FALSE branch contains a second ifelse() checking >= 80, and so on. While functional, deeply nested calls become hard to read—dplyr::case_when() is preferred for more than two levels.
Common ifelse() usage patterns in R
PatternCode ExampleUse Case
Binary labelifelse(x > 0, "pos", "non-pos")Simple two-category classification
Clampingifelse(x > cap, cap, x)Enforce upper bound on values (winsorization)
NA replacementifelse(is.na(x), 0, x)Impute missing data with a default value
Conditional mathifelse(x >= 0, sqrt(x), NA)Apply a function only where it's mathematically valid
Nested (multi-way)ifelse(a, "X", ifelse(b, "Y", "Z"))Three-category classification (prefer case_when for 4+)

Worked Example

Suppose you have a data frame of server response times (in milliseconds) and you want to create a new column categorizing each request as either "OK" (under 200 ms) or "SLOW" (200 ms or above). Some measurements are missing (NA).

Categorizing Server Response Times
1
Step 1 — Create the DataDefine the data frame with a response_ms column: df <- data.frame(request_id = 1:6, response_ms = c(120, 350, 95, NA, 200, 180))
A 6-row data frame with one NA value at row 4.
2
Step 2 — Apply ifelse()Use ifelse() to create a new column. The test is df$response_ms < 200; the yes value is "OK"; the no value is "SLOW". df$status <- ifelse(df$response_ms < 200, "OK", "SLOW")
A character vector: c("OK", "SLOW", "OK", NA, "SLOW", "OK")
3
Step 3 — Trace the Logic Element by ElementRow 1: 120 < 200 → TRUE → "OK". Row 2: 350 < 200 → FALSE → "SLOW". Row 3: 95 < 200 → TRUE → "OK". Row 4: NA < 200 → NA → NA. Row 5: 200 < 200 → FALSE → "SLOW". Row 6: 180 < 200 → TRUE → "OK".
4
Step 4 — Verify the OutputPrint the data frame to confirm: print(df)
The status column contains "OK", "SLOW", "OK", NA, "SLOW", "OK". The NA at row 4 correctly propagated because the comparison NA < 200 yields NA, not TRUE or FALSE.

ifelse() vs. Alternatives

R offers several mechanisms for conditional logic, and choosing the right one depends on whether you are operating on scalars or vectors, and how strict you want type checking to be. The table below compares ifelse() with its most common alternatives. Understanding these tradeoffs is critical for writing robust, maintainable R code.

Comparison of R conditional mechanisms
Featureif...else (scalar)ifelse() (base R)dplyr::if_else()
Vectorized?No — operates on length-1 logicalYesYes
Type checkingNone (returns any type)Lenient — may silently coerceStrict — yes and no must be same type
Preserves class?Yes (returns branch as-is)No — strips Date, POSIXct, factorYes
NA handlingErrors if condition is NAReturns NA at NA positionsReturns NA (or custom via missing arg)
DependencyBase R (keyword)Base R (function)Requires dplyr package
Lazy evaluation?Yes — only chosen branch runsNo — both branches fully evaluatedNo — both branches fully evaluated
🔀 WHEN TO USE WHAT
Use if...else for control flow decisions that branch your program's execution (e.g., choosing which algorithm to run). Use ifelse() when you need to produce a new vector by applying a conditional rule element-wise—think of it as a data transformation, not a control flow statement. If you are already in a tidyverse pipeline and need type safety, prefer dplyr::if_else().

Connection to Advanced Vectorized Operations

Mastering ifelse() is the first step toward fluency in R's broader ecosystem of vectorized transformations. As your conditional logic grows more complex—multiple thresholds, combinations of predicates, or transformations that depend on group membership—you will encounter more powerful abstractions built on the same fundamental idea.

From ifelse() to advanced vectorized conditionals
Conceptifelse() (This Lesson)Advanced Version
Multi-way branchingNested ifelse() callsdplyr::case_when() — flat, readable multi-condition syntax
Type-safe branchingMay strip class attributesdplyr::if_else() — enforces identical types for yes/no
Value-based lookupChained ifelse() for matchingmatch() or switch() for exact value mapping
Performance-critical pathsGood for moderate vectorsdata.table::fifelse() — optimized C implementation, faster on large data
Grouped conditionalsManual subsettingdplyr::mutate() + case_when() inside grouped pipelines

The conceptual leap from ifelse() to case_when() mirrors the progression from binary classifiers to multi-class classifiers in machine learning—the underlying mechanism (element-wise predicate evaluation) is the same, but the interface scales to handle richer decision boundaries. Similarly, data.table::fifelse() demonstrates a common pattern in the R ecosystem: a base R function is reimplemented in C for performance while preserving the same vectorized semantics. Understanding ifelse() deeply means you already understand 80% of how these advanced tools work.

Practice Problems

PROBLEM 1CONCEPTUAL
Explain why if (c(TRUE, FALSE)) { "A" } else { "B" } produces a warning, whereas ifelse(c(TRUE, FALSE), "A", "B") does not. What fundamental design difference accounts for this?
PROBLEM 2BASIC CALCULATION
Given temps <- c(32, 75, 100, -5, 212), write a single ifelse() call that returns "freezing" for values ≤ 32 and "above freezing" otherwise. What is the resulting vector?
PROBLEM 3INTERMEDIATE
Given v <- c(4, -1, NA, 9, -3), predict the output of ifelse(v >= 0, sqrt(v), NA). Does R compute sqrt(-1) during this call? If so, does that produce an error or a warning?
PROBLEM 4APPLIED
You have a data frame orders with columns quantity and unit_price. Write code to add a total column that equals quantity * unit_price if the quantity is 10 or more (bulk discount: 10% off), and quantity * unit_price at full price otherwise. Use a single ifelse() call.
PROBLEM 5CRITICAL THINKING
Consider d <- as.Date(c("2024-01-15", "2024-07-04")) and result <- ifelse(d > as.Date("2024-06-01"), d + 30, d). What is the class of result? Explain why it is not what you might expect, and propose two different solutions to preserve the Date class.

Summary

The ifelse(test, yes, no) function is R's primary tool for vectorized conditional assignment. It evaluates a logical test vector element by element, selecting corresponding values from yes where the test is TRUE and from no where it is FALSE. Positions where the test is NA propagate as NA in the output. The recycling rule allows scalar yes/no values to be broadcast across all positions, making the most common use case—binary labeling—clean and concise.

Key caveats to remember: both branches are fully evaluated before selection (watch for side effects and warnings), and class attributes may be stripped from Date, factor, and POSIXct objects. For type-safe vectorized conditionals, consider dplyr::if_else(); for multi-way classification beyond two categories, dplyr::case_when() provides a more readable alternative to deeply nested ifelse() calls. Master ifelse() first—it is the foundation upon which all of R's vectorized conditional tooling is built.

Varsity Tutors • R Programming • ifelse() — Use ifelse() for vectorized conditional assignment (intro)