R from the ground up

Choosing, sorting and filtering rows

Keeping the rows that pass a test, asking the same question with subset, picking columns, and sorting a table by one of them.

Part 6 of 12

Intermediate17 min readBack to Courses

A test, put inside the brackets

A frame is indexed with two parts separated by a comma: rows first, columns second. sales[sales$price < 70, ] keeps the rows where the price test is TRUE, and the empty part after the comma means "every column".

That is all there is to filtering. The test on the left is a logical vector, one value per row, and R keeps the positions where it is TRUE. The same trick works for the columns: sales[, "price"] is one column, and sales[1:2, c("item", "price")] is two rows of two named columns. Build the test from a column of the same frame and it has one value per row by construction; a test of any other length is recycled or refused.

The same question, spelled shorter

subset(sales, units >= 10) does exactly what the bracket version does, with the column names in scope so you do not have to write sales$ twice. It reads better for long conditions, which is why it survives in code written today.

Both forms keep the original row numbers as names, which is a small thing with a large payoff: after filtering, you can still see which record each row came from. Resetting them is rarely what you want. subset also drops rows whose condition is NA, which the bracket form does not, and that difference matters the first time a column has missing values.

Sorting by a column

order(sales$price) does not sort the values, it returns the positions that would put them in order. You then use those positions to rearrange the frame: sales[order(sales$price), ]. Once that clicks, sorting by any combination of columns is a small step away.

Add decreasing = TRUE to turn it around. sort works on a plain vector when that is all you need, and rev reverses one that is already sorted. order puts NA last by default, and na.last = FALSE moves them first — worth knowing before a report silently files the missing rows at the bottom.

Rows by test, rows by position, rows by order

sales[sales$price < 70, ]
subset(sales, units >= 10)

sales[, "price"]
sales[1:2, c("item", "price")]

order(sales$price)
sales[order(sales$price), ]
sales[order(sales$units, decreasing = TRUE), ]

sort(c(3, 9, 4))
rev(sort(c(3, 9, 4)))

Five products, listed four ways

Run it and compare the two lists of names: cheap rows and busy rows turn out to be the same four products. Then change the price cut-off to 30 and predict both lists before you Run.

Output
Click "Run" to execute your code

A filter changes the number of rows, so a check afterwards is cheap and worth it: nrow(cheap) is the count that survived. When the count is zero, the test is almost always testing the wrong column or the wrong direction.

The comma, and what each side means

Frame indexing always has two parts separated by a comma: rows on the left, columns on the right. sales[1, ] is the first row with every column; sales[, 1] is every row of the first column. Leaving one side empty means "all of it", and that empty space is not optional — sales[1] selects a column, not a row.

By default a single column drops to a vector. sales[1:2, "price", drop = FALSE] keeps a frame, which is the form to use when the result is going to be bound to another table or passed to a function that expects one. The drop rule is a convenience that occasionally surprises; knowing the argument is enough to control it.

Conditions, combinations and missing rows

Filters combine with & and |, and the precedence rule from the truth chapter applies: & binds tighter, so parentheses say what you mean. %in% is the compact way to test a membership list, and ! negates a whole condition when it is wrapped around it.

A logical test with an NA in it produces an NA in the filter, and [ treats NA as "keep this row but fill it with NA" rather than dropping it. When a column may be missing values, make the test explicit with !is.na(x) & x > 10, or the result will contain rows you thought you had excluded.

order sorts by every key you give it, left to right, and a later key breaks a tie in the earlier one. order(sales$region, sales$price) reads as "region first, then price inside each region". decreasing = TRUE applies to the whole call, so to sort one numeric key downward and keep the rest ascending, negate that key instead.

Three filter mistakes cause most empty results. Forgetting the comma turns a row filter into a column selection. Using && instead of & tests only the first row. And writing a single = inside the brackets is a syntax error rather than an assignment, because the expression is being evaluated, not stored. When a filter returns zero rows, print the condition on its own and count the TRUEs.

Try these next

  • Filter the table to rows in one region with a price above 50, and print only the item and price columns.
  • Sort by units descending and price ascending in the same call, using order(-units, price) inside with.
  • Count the rows where the price is missing with sum(is.na(orders$price)).

A shortlist with two conditions

Run it and check the shortlist by hand against the table. Then change the unit cut-off from 5 to 10, predict the count, and add a condition that keeps only the north region.

Output
Click "Run" to execute your code

Check what you learned

One question at a time. A wrong answer explains itself — take another go.

Part 1 of 5

In sales[sales$price < 70, ], what does the empty part after the comma mean?