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.
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.
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
taxcolumn that is 20 percent of price, then print the first two rows. - Compare
books$pages,books[["pages"]]andbooks["pages"]withclass(). - 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.
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?
