rguides

setdiff()

setdiff(x, y)

The setdiff() function returns elements that are in the first vector but not in the second. It performs a set difference operation.

Syntax

setdiff(x, y)

Parameters

ParameterTypeDescription
xvectorFirst input vector
yvectorSecond input vector

Examples

Basic usage

The simplest use of setdiff() passes two numeric vectors and receives the elements present in x that are absent from y. The result preserves the order of x, which distinguishes setdiff() from a pure mathematical set operation — the output includes only the difference set but keeps the sequence the elements first appeared in. This ordering property is useful when the input vector order carries meaning, such as chronological arrival of IDs or sorted priority rankings, and you want to preserve that structure after filtering.

x <- c(1, 2, 3, 4, 5)
y <- c(3, 4, 5, 6, 7)

setdiff(x, y)
# [1] 1 2

Character vectors

setdiff() works on character vectors with the same semantics as numeric vectors, comparing elements by value and returning those unique to x. This makes it a natural tool for comparing lists of names, identifiers, or categories — for instance, finding which packages are installed in one R library but missing from another, or which gene symbols appear in an experimental group but not the control. The comparison is exact and case-sensitive, so "Apple" and "apple" are treated as distinct elements, which is consistent with how R handles character matching in %in% and match().

a <- c("apple", "banana", "cherry")
b <- c("banana", "cherry", "date")

setdiff(a, b)
# [1] "apple"

Order is preserved

The output order of setdiff() follows the order of first appearance in x, not sorted order. When x is c(5, 3, 1, 4, 2) and y contains 4 and 2, the result is c(5, 3, 1) — the remaining elements keep their original positions relative to each other. This is different from setequal() which ignores order entirely, and different from sort(setdiff(x, y)) which would give c(1, 3, 5). Relying on this ordering behaviour is safe because it is documented and consistent across R versions, but if you need sorted output for readability, call sort() explicitly on the result.

x <- c(5, 3, 1, 4, 2)
y <- c(4, 2)

setdiff(x, y)
# [1] 5 3 1

Common patterns

Finding unique rows in one data frame vs another

Comparing data frames by a key column is a frequent task in data cleaning workflows — you have two versions of a dataset and need to identify which records are new, removed, or changed between them. Extracting the key column from each data frame and passing the vectors to setdiff() gives you the set of IDs unique to the first frame, and you can then use %in% to filter the original data frame to only those rows. This two-step pattern is more readable than a full anti-join when the comparison involves only a single key and the data frames are small enough that vectorised lookups are fast.

df1 <- data.frame(id = 1:5)
df2 <- data.frame(id = 3:7)
ids1 <- df1$id
ids2 <- df2$id
new_ids <- setdiff(ids1, ids2)
df1[df1$id %in% new_ids, ]
#   id
# 1  1
# 2  2

Removing handled cases

When processing items in batches, keeping track of what remains to be done is a common logistical pattern. You maintain a vector of all case identifiers and a vector of already-processed IDs, and setdiff(all_cases, processed) gives you the outstanding items in a single call. This is simpler than maintaining a boolean “done” flag on each case and filtering, and it works well in iterative scripts where the processed set grows over successive runs. The resulting vector is immediately ready for the next processing iteration without additional cleanup.

# All cases minus already processed
all_cases <- 1:100
processed <- c(5, 10, 15, 20)
remaining <- setdiff(all_cases, processed)
length(remaining)
# [1] 96

setdiff() in practice

setdiff(x, y) returns elements that are in x but not in y. The order follows x. Duplicates in x are collapsed, each unique element appears at most once in the result.

setdiff() is asymmetric: setdiff(a, b) and setdiff(b, a) generally give different results. Think of it as “remove from x everything that appears in y.”

A common use case is finding new records: setdiff(current_ids, previous_ids) gives IDs that appeared since the last run. The complementary operation setdiff(previous_ids, current_ids) gives IDs that were removed. Together they give the full picture of what changed between two snapshots.

For checking whether one vector is a subset of another, use all(a %in% b) or length(setdiff(a, b)) == 0. Both check whether a contains any elements not in b.

When working with database-style data, setdiff() on a key column is a filter: df[df$id %in% setdiff(all_ids, excluded_ids), ] selects rows not in the excluded set. This pattern is equivalent to a SQL WHERE id NOT IN (...) clause and is more readable than the equivalent !df$id %in% excluded_ids when the excluded set has a name.

setdiff() does not modify its inputs, it returns a new vector. All R vector operations follow this copy-on-modify semantics. If you need to remove elements from a vector in a loop, accumulate the result in a separate variable rather than trying to modify the original.

Note that setdiff() automatically de-duplicates the output: if x contains c(1, 1, 2, 3) and y contains c(2), the result is c(1, 3), not c(1, 1, 3). The de-duplication always applies to the output regardless of input.

When y contains all elements of x, setdiff(x, y) returns an empty vector of the appropriate type — check length(result) == 0 after calling if you need to handle this case. When y is empty or has length zero, setdiff(x, y) returns unique(x), the distinct elements of x.

Comparing file lists between directories

# Files in two project directories
dir_a <- c("README.md", "script.R", "data.csv", "output.png", "notes.txt")
dir_b <- c("script.R", "data.csv", "results.pdf", "output.png")

# Files only in directory A (not yet copied to B)
only_in_a <- setdiff(dir_a, dir_b)
only_in_a
# [1] "README.md"   "notes.txt"

# Files only in B (may have been added independently)
only_in_b <- setdiff(dir_b, dir_a)
only_in_b
# [1] "results.pdf"

# Files that moved (present in one but not both)
moved <- c(setdiff(dir_a, dir_b), setdiff(dir_b, dir_a))
moved
# [1] "README.md"   "notes.txt"   "results.pdf"

Because setdiff() is asymmetric, calling it twice with swapped arguments gives you a complete picture of the differences between two collections. The first call finds files that exist in A but not B — perhaps files that still need to be transferred — while the reversed call catches files that appeared in B independently, such as results generated by a separate process. Combining both differences into a single vector with c() gives you all files that are not shared between the directories, which is useful for auditing partial synchronisation or checking for orphaned artifacts.

See also