Matrices and Arrays
In Lesson 3 you learned to x-ray any object with str(), dim() and class(). Now we meet the object those tools describe most cleanly: R's plain rectangle of numbers.
The trail club ran four hikes this month. For each of three members, Maya, Theo and Ada, you wrote down the miles they walked on each hike. Stack those numbers into a grid, three rows of members by four columns of hikes, and every single cell is a number. No names, no TRUE/FALSE, just numbers. That all-one-type grid is a matrix, and it is the shape R reaches for whenever the data is pure numbers: images, distances, correlations, the insides of a model.
By the end of this lesson you will be able to:
- Build a matrix from a vector and control how R fills it (down columns, or by row)
- Index it by
[row, column]to pull a single cell, a whole row or column, or a sub-grid - Collapse it to one summary per row or per column with
apply()
Prerequisites: you can run a line of R and assign with <-, and you know that a vector holds one type while a data frame is a list of equal-length, mixed-type columns. The hike grid below is the matrix you are about to build; press Show what changed to see the payoff, the whole grid reduced to one total per member.
A matrix is a grid of one type
You already know R's table, the data frame: a stack of columns where each column can be a different type (a text name, a number age, a logical member). A matrix is stricter and, in return, simpler: it is a rectangle where every cell is the same type. One type for the whole grid, not one type per column.
That is the right shape for the hike data, because every cell is just miles, a number. We describe a matrix by its shape, written \(r \times c\): \(r\) rows by \(c\) columns. Our grid is \(3 \times 4\), three members by four hikes. The value in row \(i\), column \(j\) is written \(a_{ij}\); so \(a_{2,3}\) is the entry for the second member on the third hike. Those two coordinates, row then column, are the whole idea, and we will use them constantly.
Let us build it for real. Each lesson runs in a fresh R session, so we make the grid right here (run it once; the later steps reuse m):
dimnames is optional, it just labels the rows and columns so the printout reads nicely; a matrix is perfectly happy with no names at all. Now ask R for its shape and its kind:
class(m) says "matrix", not "data.frame".How R fills the grid: down the columns
When you hand matrix() a flat vector of numbers, it has to decide where each one goes. The rule surprises everyone once: R fills down the first column, then the next, then the next, not left to right across the rows. That is why, above, the first three numbers 4, 3, 5 became the first column (hike 1 for Maya, Theo, Ada), not the first row.
The clearest way to see it is with the numbers 1 to 6 in a 2-row grid:
Same six numbers, two completely different grids. If your data is written out row by row (the way a person reads a table), pass byrow = TRUE so the numbers land where you expect. You only ever give nrow or ncol; R works out the other from how many values you handed it.
cbind() (column-bind) or as rows with rbind() (row-bind). rbind(Maya = c(4, 6, 7, 10), Theo = c(3, 5, 5, 8)) stacks two members into a 2-row matrix, no fill-order puzzle to think about.Where does each number land?
You run matrix(1:6, nrow = 2) with no other arguments. Which number ends up in position [1, 2], the first row, second column?
1, 2, so the second column starts at 3, and its first entry is position [1, 2].[row, column], not as a running count. R fills columns first, so [1, 2] is 3; the value 4 sits at [2, 2].Index by [row, column]
A vector took one coordinate; a data frame took two, df[rows, columns]. A matrix is the same two-coordinate idea in its purest form: m[i, j] means **row i, column j**. Leave a slot empty to mean "all of it", and you can use a name or a number in either slot.
Read every one of those as "row slot, comma, column slot". An empty slot keeps everything along that axis, so m[, "hike3"] keeps every row of one column, and m[1:2, ] keeps every column of two rows.
One sharp edge catches everyone. When you pull a single row or column, R helpfully drops the now-pointless second dimension and hands you back a plain vector, not a one-row matrix:
dim(). When you must keep the matrix shape, add drop = FALSE to the brackets.The widget shows the sub-grid move: keep the first two members and the third row falls away.
Pull one column
You want everyone's miles on hike 2, the whole second column. Fill the brackets: leave the row slot empty (all members) and ask for the hike2 column. The result should be 6 5 7 for Maya, Theo and Ada.
Show answer
m[, 2]
#> Maya Theo Ada
#> 6 5 7Collapse a whole direction with apply()
The most useful thing you do to a grid is reduce it: turn each row into one number (each member's total miles) or each column into one number (the average distance of each hike). The tool is apply(), and it takes three arguments: the matrix, a MARGIN, and the function to run.
The MARGIN is just a direction: \(1\) means "go along the rows" (one answer per row), \(2\) means "go along the columns" (one answer per column). Remember it from the shape \(r \times c\): \(r\) (rows) is the first dimension, so MARGIN 1 is rows; \(c\) (columns) is the second, so MARGIN 2 is columns.
So Maya walked 27 miles over the month, and the four hikes averaged 4, 6, 7 and 10 miles, the trips got longer as the month went on. apply() runs any function this way, sum, mean, max, even one you write yourself, which is what makes it so general.
rowSums(m) and rowMeans(m) for the per-row versions, colSums(m) and colMeans(m) for the per-column ones. rowSums(m) is exactly apply(m, 1, sum), just quicker to type and quicker to run.Which way does it reduce?
You run apply(m, 2, mean) on the hike grid. Remembering that the shape is 3 rows x 4 columns, what do you get back?
2 is the column direction, so mean runs once per column and returns four values, one average per hike.apply() reduces along one direction, not the whole grid. MARGIN 2 returns one mean per column (4 values); mean(m) would give the single grand average.Total miles per member
Now the per-row reduction. Give each member their total miles for the month by applying sum along the rows. Fill in the MARGIN. The answer should be 27 21 33 for Maya, Theo and Ada.
Show answer
apply(m, 1, sum)
#> Maya Theo Ada
#> 27 21 33A matrix is really a 2-D array
Look back at class(m): it said "matrix" "array". That second word is the bigger picture. A matrix is just a 2-dimensional array, and an array is the same all-one-type idea stretched to any number of dimensions. Two dimensions give you rows and columns; a third gives you stacked grids.
Say the club hiked for two months and you want both. That is a \(3 \times 4 \times 2\) array: members by hikes by month. You build it with array(), naming the size of each dimension in dim:
The same column-major fill and the same [ , ] indexing scale up: you just get one coordinate per dimension. Most everyday work stays in two dimensions, so you will reach for matrices far more often than higher arrays, but it is worth knowing that the matrix is simply the flat, two-dimensional case of one idea.
References
A few trustworthy places to take this further, all free:
- An Introduction to R: Arrays and matrices - the canonical first treatment of matrices, arrays, indexing and
apply, straight from the R project. - Advanced R (2e): Vectors (matrices and arrays) - how a matrix is just a vector with a
dimattribute, and where it differs from a data frame. - R documentation: matrix() - every argument of the function you used to build the grid, including
byrowanddimnames. - R documentation: apply() - the MARGIN argument in full, with examples of reducing rows versus columns.
Lesson 4 complete
You met R's all-one-type rectangle, the matrix: a grid where every cell shares a type, described by its shape \(r \times c\). You built one with matrix() (learning that R fills down the columns unless you say byrow = TRUE), indexed it by [row, column] to pull a cell, a row, a column or a sub-grid (minding the silent drop to a vector), and collapsed it with apply() along MARGIN 1 (rows) or 2 (columns), plus the rowSums/colMeans shortcuts. Finally you saw that a matrix is just the 2-D case of an array that can stretch to more dimensions.
Next, Lesson 5: Type Conversion in Practice. You can now build and read every core structure; the last skill is changing what type your data is, converting on purpose with as.numeric(), as.character() and as.factor(), and fixing a column that imported as the wrong type.