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.

A statistical scatter plot with code braces and a variable symbol for R programming.
Inside this lesson 10 sections 5 questions 7 figures

SECTION 1

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.

SECTION 2

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.

SECTION 3

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.

ifelse(x >= 60, "Pass", "Fail") — Element-wise Routingx vector:8542739156test (x >= 60):TRUEFALSETRUETRUEFALSEyes branch:"Pass" "Pass" "Pass" "Pass" "Pass" (recycled scalar)no branch:"Fail" "Fail" "Fail" "Fail" "Fail" (recycled scalar)result:"Pass""Fail""Pass""Pass""Fail"Each index is routed independently: TRUE → yes, FALSE → noifelse(c(85,42,73,91,56) >= 60, "Pass", "Fail")## [1] "Pass" "Fail" "Pass" "Pass" "Fail"
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.

SECTION 4

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]iftest[i]=TRUE,no[i]iftest[i]=FALSE,NAiftest[i]=NAresult[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

yesrecycled[i]=yes[(i−1)modlength(yes)+1]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.

SECTION 5

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.

Nested ifelse() — Multi-way Classificationifelse(score >= 90, "A", ifelse(score >= 80, "B", ifelse(score >= 70, "C", "F")))score >= 90?TRUEFALSE"A"score >= 80?TRUEFALSE"B"score >= 70?TRUEFALSE"C""F"Each nesting level handles one additional threshold—equivalent to a cascading if-else-if 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.
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+)

Common ifelse() usage patterns in R

SECTION 6

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. 1

    Step 1 — Create the Data

    Define 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. 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. 3

    Step 3 — Trace the Logic Element by Element

    Row 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. 4

    Step 4 — Verify the Output

    Print 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.

SECTION 7

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.

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

Comparison of R conditional mechanisms

🔀

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().

SECTION 8

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.

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

From ifelse() to advanced vectorized conditionals

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.

SECTION 9

Practice Problems

Question 1: CONCEPTUAL

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?
Show answer
The if keyword is a scalar control-flow construct that expects a single logical value. When given a vector of length > 1, R uses only the first element and issues a warning: "the condition has length > 1 and only the first element will be used." In contrast, ifelse() is a vectorized function explicitly designed to accept logical vectors of any length. It processes every element independently, returning a vector of the same length as the input. The key design difference is that if controls program flow (which code path to execute), while ifelse() transforms data (which values to select at each position).

Question 2: BASIC 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?
Show answer
The call is: ifelse(temps <= 32, "freezing", "above freezing"). The test vector evaluates to c(TRUE, FALSE, FALSE, TRUE, FALSE). At TRUE positions (indices 1 and 4), the yes value "freezing" is selected. At FALSE positions (indices 2, 3, 5), the no value "above freezing" is selected. The result is c("freezing", "above freezing", "above freezing", "freezing", "above freezing"). Note that 32 is included in "freezing" because we used <=.

Question 3: INTERMEDIATE

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?
Show answer
The test vector is c(TRUE, FALSE, NA, TRUE, FALSE). The output is c(2, NA, NA, 3, NA). However, R does compute sqrt(v) for the entire vector as part of evaluating the yes argument, which means sqrt(-1) and sqrt(-3) are computed, producing NaN values and a warning: "NaNs produced". These NaN values are discarded since their positions correspond to FALSE in the test, but the warning still appears. This illustrates the 'both branches evaluated' principle.

Question 4: APPLIED

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.
Show answer
The solution is: orders$total <- ifelse(orders$quantity >= 10, orders$quantity * orders$unit_price * 0.9, orders$quantity * orders$unit_price). The test is orders$quantity >= 10. For TRUE positions, the discounted price quantity * unit_price * 0.9 (10% off) is selected. For FALSE positions, the full price quantity * unit_price is used. Both the yes and no arguments are themselves vectorized expressions, so R computes them element-wise across all rows before the selection occurs. This is idiomatic R for adding computed columns without writing loops.

Question 5: CRITICAL 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.
Show answer
The class of result is "numeric" (specifically, the internal integer representation of Date objects), not "Date". This happens because ifelse() strips class attributes from the result, inheriting the structure of the test vector (a logical vector, which has no Date class). Two solutions: (1) Wrap the result with as.Date(result, origin = "1970-01-01") to restore the Date class. (2) Use dplyr::if_else(), which enforces type consistency and preserves the Date class automatically: dplyr::if_else(d > as.Date("2024-06-01"), d + 30, d). This is one of the most important practical reasons to prefer if_else() in production code.

SUMMARY

Summary

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)