How do I pick rows and columns?

Cut a price table down to the rows and columns you want, first with base R square brackets and then with the dplyr verbs filter, select and arrange.

A data frame takes two indices: p[rows, columns]. Rows go before the comma, columns after it. Leave a side blank and you keep all of it.

In this lesson I cut the price table down two ways. First with square brackets, which are base R and always there. Then with filter(), select() and arrange() from dplyr, which say the same thing in words. Both give the same rows.

Step 1. Load the table

prices.csv holds simulated closing prices for three tickers over 40 business days. read.csv() hands back Dates as text, so I convert the column with as.Date() and the sorting later comes out by calendar order rather than alphabetically.

p <- read.csv("prices.csv")   # 120 rows, four columns
p$Date <- as.Date(p$Date)     # text to real dates

print(dim(p))                 # -> [1] 120   4
[1] 120   4
print(head(p, 3))
        Date Ticker  Close  Volume
1 2026-01-02    AAA 186.66 2183494
2 2026-01-02    CCC 416.98 3117814
3 2026-01-02    DDD 125.18 7447373
# ->         Date Ticker  Close  Volume
# -> 1 2026-01-02    AAA 186.66 2183494
# -> 2 2026-01-02    CCC 416.98 3117814
# -> 3 2026-01-02    DDD 125.18 7447373

Three tickers stacked on top of each other, one row per ticker per day.

Step 2. Rows with a condition

p$Ticker == "AAA" compares every element of the column against "AAA" and returns 120 TRUEs and FALSEs, exactly the logical vector of Lesson 3. Put it before the comma and R keeps the rows where it is TRUE.

aapl <- p[p$Ticker == "AAA", ]   # rows before the comma, blank after it

print(dim(aapl))                  # -> [1] 40  4
[1] 40  4
print(head(aapl, 3))
        Date Ticker  Close  Volume
1 2026-01-02    AAA 186.66 2183494
4 2026-01-05    AAA 186.38 8463199
7 2026-01-06    AAA 188.51 5299573
# ->         Date Ticker  Close  Volume
# -> 1 2026-01-02    AAA 186.66 2183494
# -> 4 2026-01-05    AAA 186.38 8463199
# -> 7 2026-01-06    AAA 188.51 5299573

Forty rows out of 120. The numbers on the left are row names, and they are 1, 4, 7 because those were the AAA rows in the full table. Selecting rows keeps the old labels.

The comma is not optional. p[p$Ticker == "AAA"] without it asks for columns, not rows, and errors.

Step 3. Columns, and both at once

Names after the comma pick columns. Leave the row side blank to keep every row.

two <- p[, c("Date", "Close")]    # all rows, two columns

print(head(two, 3))
        Date  Close
1 2026-01-02 186.66
2 2026-01-02 416.98
3 2026-01-02 125.18
# ->         Date  Close
# -> 1 2026-01-02 186.66
# -> 2 2026-01-02 416.98
# -> 3 2026-01-02 125.18

Fill in both sides and you cut the table down in one go.

print(head(p[p$Ticker == "AAA", c("Date", "Close")], 3))
        Date  Close
1 2026-01-02 186.66
4 2026-01-05 186.38
7 2026-01-06 188.51
# ->         Date  Close
# -> 1 2026-01-02 186.66
# -> 4 2026-01-05 186.38
# -> 7 2026-01-06 188.51

One column asked for on its own comes back as a plain vector, not a table. Two or more stay a data frame.

print(class(p[, "Close"]))               # -> [1] "numeric"
[1] "numeric"
print(class(p[, c("Date", "Close")]))    # -> [1] "data.frame"
[1] "data.frame"

Step 4. Two conditions with &

& compares element by element, so two logical vectors of length 120 combine into a third of length 120, TRUE only where both are TRUE.

keep <- p$Close > 190 & p$Ticker == "AAA"   # TRUE where both hold

print(class(keep))          # -> [1] "logical"
[1] "logical"
print(length(keep))         # -> [1] 120
[1] 120
print(sum(keep))            # -> [1] 6        <- TRUE counts as 1
[1] 6

Six rows pass. Hand keep to the row side.

big <- p[keep, c("Date", "Close")]   # the six rows, two columns

print(big)
         Date  Close
34 2026-01-19 190.92
40 2026-01-21 190.60
52 2026-01-27 193.08
55 2026-01-28 191.35
58 2026-01-29 192.76
61 2026-01-30 191.04
# ->          Date  Close
# -> 34 2026-01-19 190.92
# -> 40 2026-01-21 190.60
# -> 52 2026-01-27 193.08
# -> 55 2026-01-28 191.35
# -> 58 2026-01-29 192.76
# -> 61 2026-01-30 191.04

Use &, not &&. && looks at the first element only and gives a single TRUE or FALSE, which is what if wants, not what a row index wants.

Step 5. The dplyr spellings

dplyr gives each job a verb. filter() takes rows, select() takes columns. Both take the data frame as their first argument, and both let you write column names bare, without p$ and without quotes.

library(dplyr)                                     # filter, select, arrange, slice_head

rows <- filter(p, Ticker == "AAA", Close > 190)   # commas mean and
out  <- select(rows, Date, Close)                  # two columns by bare name

print(out)
        Date  Close
1 2026-01-19 190.92
2 2026-01-21 190.60
3 2026-01-27 193.08
4 2026-01-28 191.35
5 2026-01-29 192.76
6 2026-01-30 191.04
# ->         Date  Close
# -> 1 2026-01-19 190.92
# -> 2 2026-01-21 190.60
# -> 3 2026-01-27 193.08
# -> 4 2026-01-28 191.35
# -> 5 2026-01-29 192.76
# -> 6 2026-01-30 191.04

The same six rows as Step 4. The one difference is the row names: dplyr renumbers from 1, square brackets kept 34, 40, 52. Reset the names on the base R result and the two objects match exactly.

rownames(big) <- NULL          # drop the old labels
print(identical(big, out))     # -> [1] TRUE
[1] TRUE

select() also drops with a minus sign.

print(head(select(p, -Volume), 3))   # everything except Volume
        Date Ticker  Close
1 2026-01-02    AAA 186.66
2 2026-01-02    CCC 416.98
3 2026-01-02    DDD 125.18
# ->         Date Ticker  Close
# -> 1 2026-01-02    AAA 186.66
# -> 2 2026-01-02    CCC 416.98
# -> 3 2026-01-02    DDD 125.18

Step 6. Chain them with the pipe

|> passes the value on its left into the function on its right as the first argument. So p |> filter(Ticker == "AAA") is filter(p, Ticker == "AAA"). Because every dplyr verb takes the data frame first, they chain.

arrange() sorts, desc() sorts downward, and slice_head(n = 5) keeps the first five rows of whatever it is handed.

top <- p |>
  filter(Ticker == "AAA", Close > 190) |>   # six rows survive
  select(Date, Close) |>                     # two columns
  arrange(desc(Close)) |>                    # highest close first
  slice_head(n = 5)                          # keep the top five

print(top)
        Date  Close
1 2026-01-27 193.08
2 2026-01-29 192.76
3 2026-01-28 191.35
4 2026-01-30 191.04
5 2026-01-19 190.92
# ->         Date  Close
# -> 1 2026-01-27 193.08
# -> 2 2026-01-29 192.76
# -> 3 2026-01-28 191.35
# -> 4 2026-01-30 191.04
# -> 5 2026-01-19 190.92

Read it top to bottom: take p, keep AAA above 190, keep two columns, sort by close, show five. arrange() on its own sorts upward.

print(p |>
  filter(Ticker == "DDD") |>   # one ticker
  select(Date, Close) |>        # drop Ticker and Volume
  arrange(Close) |>             # lowest close first
  slice_head(n = 3))            # three cheapest days
        Date  Close
1 2026-02-23 101.43
2 2026-02-06 103.00
3 2026-02-09 103.26
# ->         Date  Close
# -> 1 2026-02-23 101.43
# -> 2 2026-02-06 103.00
# -> 3 2026-02-09 103.26

Square brackets do all of this too. A chain of three or four steps needs no intermediate table.

Your turn

Find the three highest CCC closes above 420. Show Date and Close only, highest first. How many CCC days clear 420?

top3 <- p |>
  filter(Ticker == "CCC", Close > 420) |>   # CCC days above 420
  select(Date, Close) |>                     # two columns
  arrange(desc(Close)) |>                    # highest first
  slice_head(n = 3)                          # keep three

print(top3)
# ->         Date  Close
# -> 1 2026-02-10 450.26
# -> 2 2026-01-19 445.76
# -> 3 2026-02-09 445.69

print(nrow(filter(p, Ticker == "CCC", Close > 420)))   # -> [1] 39

Picking rows and columns leaves the values alone. Lesson 14 adds a new column with mutate().