Lesson 1 of 5

Subsetting and Replacement

You are tutoring a small weekend R study group. Five friends, Mara, Dev, Ada, Theo and Iris, just took a quiz graded out of 100, and you have their scores: 58, 91, 73, 49 and 84. Almost everything you will ever do with data comes down to two moves. First, reaching in to pull out the pieces you want: the top scorers, the ones who need help, one person's result. Second, writing new values back in: fixing a mis-marked answer, applying a grade floor. Those two moves are subsetting (reading a part) and replacement (writing a part), and this lesson is about doing both cleanly.

By the end of this lesson you will be able to:

  • Pull elements out of a vector with [ using positions, a "drop these" minus sign, a TRUE/FALSE condition, and names
  • Tell [ (keeps the container) apart from [[ and $ (hand you the one thing inside)
  • Subset a data frame by rows and columns, and overwrite exactly the values you choose

Prerequisites: you can build a vector, a list and a data frame, and you have met R's comparison operators like >= and ==.

Pull by position

The square bracket picks elements out

The single square bracket, [ ], is how you reach into a vector and pull elements out. You write the vector, then in the brackets you say which elements you want. The simplest "which" is a position number.

First, build the scores once. Each lesson runs in a fresh R session, so we create the data right here (run this):

RInteractive R
scores <- c(Mara = 58, Dev = 91, Ada = 73, Theo = 49, Iris = 84) scores #> Mara Dev Ada Theo Iris #> 58 91 73 49 84

  

These scores have names (the friends), printed above each value. Now pull Dev out. Dev is the 2nd element, so ask for position 2:

RInteractive R
scores[2] #> Dev #> 91

  

To pull out several at once, put a vector of positions inside the brackets. c(1, 3) gives the 1st and 3rd; the colon 2:4 is shorthand for "2, 3, 4":

RInteractive R
scores[c(1, 3)] # Mara and Ada #> Mara Ada #> 58 73 scores[2:4] # Dev, Ada, Theo #> Dev Ada Theo #> 91 73 49

  
Key Insight
scores[i] returns a vector, the same kind of thing you started with, just shorter. It can hold zero, one, or many elements depending on what you ask for. That "keeps the container" idea matters later when we meet the double bracket.
Drop, test, and name

Three more ways to say "which"

A position number says "give me this one." Three other kinds of index are far more useful in practice.

A minus sign drops positions. scores[-4] means "everything except the 4th." The minus is not a value to look up; it removes that position. This is how you say "all the others":

RInteractive R
scores[-4] # everyone except Theo (position 4) #> Mara Dev Ada Iris #> 58 91 73 84 scores[-c(4, 5)] # drop the last two #> Mara Dev Ada #> 58 91 73

  

A TRUE/FALSE vector keeps the TRUEs. This is the workhorse. A comparison like scores >= 60 produces one TRUE or FALSE per element. Put that vector in the brackets and R keeps only the elements lined up with a TRUE:

RInteractive R
scores >= 60 # the test result: one logical per student #> Mara Dev Ada Theo Iris #> FALSE TRUE TRUE FALSE TRUE scores[scores >= 60] # keep only the TRUE ones: the students who passed #> Dev Ada Iris #> 91 73 84

  

A name selects by name. Because the scores are named, you can ask for "Dev" directly instead of counting to position 2:

RInteractive R
scores["Dev"] #> Dev #> 91 scores[c("Mara", "Iris")] #> Mara Iris #> 58 84

  
Warning
A logical index should be the same length as the vector. If it is shorter, R silently recycles it, reusing it from the start, which quietly returns the wrong elements. Build the condition from the vector itself (scores >= 60) and it always lines up.
Check yourself

Which line returns the passing scores?

You want the scores themselves of everyone who passed (60 or more), not just a column of TRUE/FALSE. Which line does that?

Right. The condition goes inside the brackets, so R keeps the elements where it is TRUE and hands you those scores.
R cannot read a bare condition in the brackets. It needs a full logical vector, which you get by writing scores >= 60 first.
Your turn

Find the students who need help

The friends who scored below 60 need a follow-up session. Replace the blank so the line returns just their scores from scores. (You should get back Mara and Theo.)

Exactly. scores < 60 builds the TRUE/FALSE test, and putting it inside scores[ ... ] keeps just the failing scores, Mara and Theo.Build the condition, then index with it: scores[scores < 60].
Show answer
scores[scores < 60]
#> Mara Theo 
#>   58   49
One thing, or the box around it

Single bracket vs double bracket

So far [ ] always handed back a vector. But sometimes you want the one element inside, not a smaller container holding it. That is the difference between the single bracket [ ] and the double bracket [[ ]].

Think of a spice rack. rack[1] gives you the rack with only the first jar in it, still a rack. rack[[1]] reaches in and hands you the jar itself. Lists make the difference visible. Build one student's record, which mixes a name, a vector of three quiz attempts, and a TRUE/FALSE:

RInteractive R
student <- list(name = "Dev", scores = c(91, 88, 95), passed = TRUE) student[["name"]] # reach in: the value itself #> [1] "Dev" student["name"] # keep the container: a one-item list #> $name #> [1] "Dev"

  

They look similar but are different types. Ask R what each one is:

RInteractive R
class(student[["name"]]) # the thing inside #> [1] "character" class(student["name"]) # a list holding the thing #> [1] "list"

  

The dollar sign $ is a friendly shorthand for [[ by name. These two lines are identical, and you can keep drilling, here taking the 2nd quiz attempt:

RInteractive R
student$scores # same as student[["scores"]] #> [1] 91 88 95 student$scores[2] # drill into scores, then index position 2 #> [1] 88

  
Warning
$ only works with a name you type literally. If the name is stored in a variable, $ will not use it, so reach for [[: with col <- "scores", use student[[col]], not student$col. Also, $ on a name that does not exist returns NULL with no error, an easy bug to miss.
Rows and columns

Subsetting a data frame

A data frame is the table you will spend most of your time in. It is subset with two indices inside one bracket pair: df[rows, cols], rows first, columns second. Build the gradebook, then take a slice:

RInteractive R
gradebook <- data.frame( name = c("Mara", "Dev", "Ada", "Theo", "Iris"), score = c(58, 91, 73, 49, 84), attended = c(8, 10, 9, 5, 10), stringsAsFactors = FALSE ) gradebook[c(2, 5), c("name", "score")] # rows 2 and 5, two columns #> name score #> 2 Dev 91 #> 5 Iris 84

  

Leave a slot blank to mean "all of them." gradebook[1, ] is the whole first row; gradebook[, "score"] is the whole score column. A single column comes back as a plain vector, while $ and [[ do the same:

RInteractive R
gradebook[1, ] # all columns of row 1 #> name score attended #> 1 Mara 58 8 gradebook$score # the column, as a vector (same as gradebook[["score"]]) #> [1] 58 91 73 49 84

  

The logical-index trick scales straight up to rows: a condition in the row slot filters the table. That is the line from the cover:

RInteractive R
gradebook[gradebook$score >= 60, ] # keep the rows where score passed #> name score attended #> 2 Dev 91 10 #> 3 Ada 73 9 #> 5 Iris 84 10

  
Warning
Pulling a single column with gradebook[, "score"] drops it to a vector. If you need it to stay a one-column data frame, say gradebook[, "score", drop = FALSE]. The single bracket gradebook["score"] (no comma) also keeps it a data frame.
Check yourself

Plain vector, or one-column table?

From the gradebook data frame, which expression gives you the score column as a plain numeric vector, not a one-column data frame?

Right. The double bracket (like gradebook$score) reaches in and hands back the column itself, a numeric vector.
drop = FALSE deliberately KEEPS it a one-column data frame, the opposite of what you want here.
Write it back

Replacement: a subset on the left of the arrow

Here is the move that ties everything together. Any subset you can read, you can also write to by putting it on the left of the assignment arrow <-. R writes the right-hand values into exactly the positions the subset picked out.

Start with a department rule: nobody scores below 60. Select the failing scores with the same logical index as before, but this time assign to them. The single value 60 on the right is recycled to fill every matched position:

RInteractive R
scores[scores < 60] <- 60 # bump every failing score up to the floor scores #> Mara Dev Ada Theo Iris #> 60 91 73 60 84

  

Mara (58) and Theo (49) were both pulled up to 60; everyone else was untouched. You can also write to a single element by name, say Theo's quiz was re-marked to 67:

RInteractive R
scores["Theo"] <- 67 scores #> Mara Dev Ada Theo Iris #> 60 91 73 67 84

  

The same idea adds a new column: assign to a column name that does not exist yet and R creates it. Here ifelse() returns "pass" or "fail" per row:

RInteractive R
gradebook$grade <- ifelse(gradebook$score >= 60, "pass", "fail") gradebook #> name score attended grade #> 1 Mara 58 8 fail #> 2 Dev 91 10 pass #> 3 Ada 73 9 pass #> 4 Theo 49 5 fail #> 5 Iris 84 10 pass

  
Put it together

Apply a grade floor to the table

The same 60-point floor now has to be applied to the **gradebook's score column**, not the standalone scores vector. Select the failing scores inside the column and assign 60 to them. Replace the blank, then check. (Mara and Theo should land on 60.)

That is the whole lesson in one line: reach into gradebook$score, pick the failing values with a logical index, and write 60 into exactly those positions.Index the column with a condition, then assign: gradebook$score[gradebook$score < 60] <- 60.
Show answer
gradebook$score[gradebook$score < 60] <- 60
gradebook$score
#> [1] 60 91 73 60 84
Go deeper

References

A few authoritative places to take subsetting further, all free:

Lesson 1 complete

You can now reach into any R object and change it. You pulled elements from a vector by position, with a "drop these" minus sign, with a TRUE/FALSE condition, and by name; you separated [ (which keeps the container) from [[ and $ (which hand you the one thing inside); you sliced a data frame by rows and columns; and you put any subset on the left of <- to overwrite exactly the values you chose, including a whole column at once.

Next, Lesson 2: Control Flow in R. Subsetting let you pick the right rows; control flow lets your code make decisions and repeat work, with if/else to branch and for/while loops to run a step many times, watched one iteration at a time.