Filtering Rows: The Logical Mask
To filter a data frame in R, put a condition in the row position of the brackets: df[condition, ]. The condition evaluates to a vector of TRUE/FALSE, one per row, and R keeps the TRUE rows:
Read scores[scores$score > 80, ] from the inside out: scores$score > 80 produces TRUE FALSE TRUE TRUE FALSE, and the brackets keep rows 1, 3, and 4. The trailing comma is not optional. Brackets on a data frame take [rows, columns]; leaving the column slot empty means "all columns". Forget the comma and you are indexing columns instead - one of R's most common beginner errors.
Notice the printed row numbers are 1 3 4, not 1 2 3 - filtering keeps the original row labels. Harmless, but it surprises people.
Multiple Conditions: & and | (Not &&)
Combine conditions with & (AND) and | (OR). Each condition gets its own complete comparison:
The trap: R also has && and ||, and they are not interchangeable with & and | here. The double forms work on single values (they exist for if conditions); given whole columns, modern R (4.3+) stops with an error like 'length = 5' in coercion to 'logical(1)', and older R silently compared only the first row - arguably worse. Inside brackets, always the single & and |. The operators doc covers the distinction in full.
%in%: Match Against a Set of Values
When a column should match any of several values, don't chain == with | - use %in%:
x %in% set returns TRUE wherever x is one of the values in set. It reads better than status == "pending" | status == "shipped", scales to any number of values, and negates cleanly: !(orders$status %in% c("refunded")).
Selecting Columns
The column slot of the brackets takes names or positions:
Two notes. Selecting by name survives column reordering; selecting by position (scores[, c(1, 3)]) breaks the day someone inserts a column. And asking for a single column returns a plain vector, not a data frame - that is why the second print() shows 91 88 with no table around it. When you need to keep the data-frame shape, add drop = FALSE: scores[, "score", drop = FALSE].
subset(): The Friendly Base One-Liner
Base R ships a wrapper that does rows and columns in one readable call - no $, no comma bookkeeping:
Inside subset() you write bare column names (score, not scores$score) - it evaluates them against the data frame. This is called non-standard evaluation, and it comes with a documented caveat: it resolves names at run time in ways that misbehave inside your own functions (a variable named score in your function can be shadowed by a column named score). The rule of thumb, straight from ?subset: great interactively, avoid it in programming - use bracket indexing there.
The NA Trap
A comparison against NA yields NA, not FALSE - and bracket filtering keeps NA-mask rows as rows full of NA:
The first result contains a junk row of NAs because row 2's condition was neither TRUE nor FALSE. The fix is to require !is.na() explicitly. subset() and dplyr's filter() both drop NA-condition rows silently - convenient, but know that it is happening. The missing values doc covers why NA spreads like this.
The dplyr Way: filter() and select()
The dplyr package splits the job into two verbs - filter() for rows, select() for columns - with bare column names and no df$ repetition (static; the sandbox runs base R only):
library(dplyr)
scores |> filter(score > 80)
scores |> filter(score > 80, team == "red") # comma = AND
scores |> filter(status %in% c("pending", "shipped"))
scores |> select(name, score)
scores |> filter(team == "red") |> select(name, score)
filter() drops NA-condition rows automatically and always returns a data frame (never a bare vector), which removes both base gotchas above. The trade-off is a package dependency and the same non-standard evaluation caveats as subset() - see the dplyr intro for the full verb tour.
What You Take Away
df[condition, ]is the fundamental idiom - and the comma is mandatory.- Combine conditions with single
&and|;&&errors on vectors. %in%beats chained==for matching a set of values.- Select columns by name, and remember single columns drop to vectors unless
drop = FALSE. subset()is the pleasant interactive one-liner; brackets are the programmable one.- Comparisons with
NAproduce ghost rows in brackets - guard with!is.na().
Next up: adding and transforming columns - df$new <- ..., ifelse(), and dplyr's mutate().
Frequently Asked Questions
How do I filter rows of a data frame in R?
Put a logical condition in the row position of the brackets: df[df$score > 80, ]. The condition produces a TRUE/FALSE vector, and R keeps the TRUE rows. The comma matters - it separates the row filter from the (empty) column filter. subset(df, score > 80) does the same with less typing.
How do I filter with multiple conditions in R?
Combine conditions with & (AND) and | (OR): df[df$score > 80 & df$team == "red", ]. Use the single &, not && - the double form works on single values only and errors on vectors in modern R.
What is the difference between subset() and bracket filtering in R?
They keep the same rows, with two differences: subset() silently drops rows where the condition is NA (brackets keep them as NA-filled rows), and subset() uses non-standard evaluation - you write bare column names, which is convenient interactively but unreliable inside your own functions.
How do I filter rows where a column matches a list of values?
Use %in%: df[df$team %in% c("red", "blue"), ] keeps rows whose team is any of the listed values. It replaces a chain of == comparisons joined by |, and unlike ==, it never returns NA.