R from the ground up

Data frames: tables you can compute on

The table shape R is built around: build one from named vectors, reach into a column with the dollar sign, add a computed column, and read it back.

Part 5 of 12

Intermediate18 min readBack to Courses

A table whose columns are vectors

A data frame is a table. Each column is a vector, every column has the same number of values, and the rows line up because the position in each column refers to the same record. data.frame(item = ..., price = ...) builds one from named vectors, and the names you give become the column names.

This is the shape almost all real data arrives in, and it is why R feels at home with it: anything that works on a vector works on a column, unchanged. The row names are the frame’s own labels and default to "1", "2", "3"; they are text rather than numbers, which is why a filter leaves the original positions behind as names.

Reaching into a frame

The dollar sign takes a column out: sales$price is that column as a plain vector, so mean(sales$price) and sort(sales$price) work immediately. nrow(sales) counts the rows, ncol(sales) counts the columns, and names(sales) lists the column names.

You can add a column the same way: the new name on the left of the arrow, an expression over two columns on the right. The two vectors are combined element by element, exactly as they would be outside a table, and the result becomes the new column. A name that does not exist yet adds the column at the end; a name that does replaces it in place, and the other columns keep their order either way.

Looking at a table you have just made

head(sales, 3) prints the first three rows, which is the honest way to check a frame you have just built. print(sales) prints the whole thing, and on a real dataset that is a wall of text you will regret; head and tail are the habits to form.

Two more functions earn their keep. str(sales) describes the structure — one line per column, with its type and a few sample values. summary(sales) gives the five-number summary of every numeric column at once, and it is the first thing many people run on a table they have never seen. Run it before summary on unfamiliar data, because it answers the two questions that decide everything else: how many rows there are, and what type each column holds.

Build a frame, add a column, read it

sales <- data.frame(
  item = c("Mug", "Lamp"),
  price = c(12.5, 24),
  units = c(30, 12)
)

nrow(sales)
ncol(sales)
names(sales)

sales$price
mean(sales$price)

sales$revenue <- sales$price * sales$units

head(sales, 1)
str(sales)

Four products, one computed column

Run it and check the two totals against the printed table. Then add a fourth column of your own, perhaps the price with tax, and print the first two rows again.

Output
Click "Run" to execute your code

Notice that sales$price is a vector and not a special table thing: that is the whole trick. Once you have a column you can use every vector tool from the earlier chapters on it, and once you have a table you can hand it to a model, a plot or a summary.

One type per column, and the trap that follows

A data frame is strict about shape: every column has the same number of values, and every column has one type. That is what makes sales$price * sales$units work without a loop — both columns are ordinary numeric vectors of the same length.

The type rule has a famous trap. cbind builds a matrix, and a matrix holds one type, so data.frame(cbind(id = 1:3, name = c("a", "b", "c"))) turns the numbers into text. Build the table with data.frame directly and each column keeps its own type. str(sales) is the quick check: it prints the type of every column on one line.

factor is the exception worth meeting early. A column of categories can be stored as a factor with a fixed list of levels, which is what table and modelling functions expect. It prints like text but behaves like an integer underneath, so convert with as.character before treating it as a string.

Rows, columns, and the two bracket styles

sales$price and sales[["price"]] both return the column as a vector. sales["price"] returns a data frame with one column, because a single-bracket selection keeps the table shape. That difference matters when a function expects a frame or expects a vector, and it is the reason both forms exist.

sales[, "price"] also drops to a vector by default. Adding drop = FALSE keeps the frame shape, which is the safe form inside a function that might be given one column or several. And head and tail are not decoration: printing a frame you have just built is how you catch a column that arrived in the wrong order.

Three mistakes to watch for. Columns of unequal length are recycled or rejected: a length that divides evenly is repeated silently, which is rarely what you meant. sales[1] is a one-column frame, not the first value; use sales[[1]] or sales$price for the vector. And adding a column with the wrong length recycles values down the column, so check nrow after a $<- assignment as well as before it.

Try these next

  • Add a tax column that is 20 percent of price, then print the first two rows.
  • Compare books$pages, books[["pages"]] and books["pages"] with class().
  • Build a frame with an id column and a name column, then run str() and check that id stayed numeric.

A reading log with a computed column

Run it and check pages per day against the two columns beside it. Then add a fourth book, and decide whether `which.max` still names the fastest read before you Run.

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

What does sales$price give you?