# Base R only: nothing to load.Matrices
Two-dimensional tables of one type: create, bind, slice and multiply
By the end of this lesson you will be able to:
- create a matrix with
matrix(), choosing how it is filled withbyrow; - inspect and set its dimensions and names with
dim()anddimnames(); - add rows and columns with
rbind()andcbind(); - extract elements, rows and columns with
[row, col], keeping the matrix shape withdrop = FALSE; - tell element-wise multiplication
*from matrix multiplication%*%, and compute row and column summaries.
1. Why matrices?
A vector is a single line of values. Many things in statistics are naturally rectangular: a table of exam marks with one row per student and one column per subject; the counts in a contingency table; a correlation table between variables; the pixel intensities of a grey-scale image.
A matrix is a vector arranged in rows and columns. Like a vector, it has one type only: all numbers, or all text, or all logicals. This restriction makes matrices fast and is what allows linear algebra (the mathematics behind regression, PCA and many machine-learning methods), but it also explains why we usually store datasets in data frames instead (Section 8).
2. Creating a matrix
matrix(data, nrow = 1, ncol = 1, byrow = FALSE, dimnames = NULL)data: the values, usually a vector;nrow,ncol: number of rows and columns (giving one is enough: R deduces the other);byrow: ifFALSE(default) values fill the matrix column by column; ifTRUE, row by row;dimnames: optional row and column names.
matrix_a <- matrix(1:10, nrow = 5, ncol = 2, byrow = TRUE)
matrix_a#> [,1] [,2]
#> [1,] 1 2
#> [2,] 3 4
#> [3,] 5 6
#> [4,] 7 8
#> [5,] 9 10
matrix_b <- matrix(1:10, nrow = 5, ncol = 2, byrow = FALSE)
matrix_b#> [,1] [,2]
#> [1,] 1 6
#> [2,] 2 7
#> [3,] 3 8
#> [4,] 4 9
#> [5,] 5 10
Compare the two outputs: the same ten numbers, arranged differently. In matrix_a the numbers 1, 2 fill the first row; in matrix_b the numbers 1 to 5 fill the first column. Column-wise filling is R’s default, and it is worth remembering because it also explains how R stores matrices internally: a matrix is just a vector, read column after column.
m <- matrix(1:12, ncol = 3)
m#> [,1] [,2] [,3]
#> [1,] 1 5 9
#> [2,] 2 6 10
#> [3,] 3 7 11
#> [4,] 4 8 12
dim(m) # rows, columns#> [1] 4 3
nrow(m)#> [1] 4
ncol(m)#> [1] 3
length(m) # total number of elements = rows x columns#> [1] 12
The labels [1,], [2,], … on the left are row indices and [,1], [,2], … on top are column indices: they show exactly how you would extract that row or column. dim(m) returns 4, 3: 4 rows and 3 columns. Note that length() counts all elements (12), not the columns.
Names for rows and columns
marks <- matrix(
c(28, 30, 24, 27, 22, 26),
nrow = 3,
dimnames = list(
c("anna", "ben", "carla"), # row names
c("statistics", "programming") # column names
)
)
marks#> statistics programming
#> anna 28 27
#> ben 30 22
#> carla 24 26
rownames(marks)#> [1] "anna" "ben" "carla"
colnames(marks)#> [1] "statistics" "programming"
dimnames is a list of two vectors: row names first, column names second. You can also set them afterwards with rownames(m) <- ... and colnames(m) <- ....
3. Adding rows and columns: cbind() and rbind()
cbind() binds objects side by side as columns; rbind() stacks them as rows.
cbind(matrix_a, 1:5) # add one column#> [,1] [,2] [,3]
#> [1,] 1 2 1
#> [2,] 3 4 2
#> [3,] 5 6 3
#> [4,] 7 8 4
#> [5,] 9 10 5
m2 <- rbind(m, c(100, 200, 300)) # add one row at the bottom
m2#> [,1] [,2] [,3]
#> [1,] 1 5 9
#> [2,] 2 6 10
#> [3,] 3 7 11
#> [4,] 4 8 12
#> [5,] 100 200 300
dim(m2)#> [1] 5 3
Two matrices can be bound together if they have the right shape: the same number of rows for cbind(), the same number of columns for rbind().
left <- matrix(1:12, ncol = 3) # 4 x 3
right <- matrix(13:24, ncol = 3) # 4 x 3
dim(cbind(left, right)) # 4 x 6#> [1] 4 6
dim(rbind(left, right)) # 8 x 3#> [1] 8 3
With named arguments, the names become column (or row) names: cbind(marks, history = c(25, 29, 30)) adds a column called history.
4. Extracting elements: [row, col]
Inside the square brackets, a comma separates the row selection (before) from the column selection (after). Leaving one side empty means “all”.
| Code | Selects |
|---|---|
m[1, 2] |
the element in row 1, column 2 |
m[1:3, 2:3] |
rows 1–3 and columns 2–3 |
m[, 1] |
all rows of column 1 |
m[1, ] |
all columns of row 1 |
m[-1, ] |
everything except row 1 |
m#> [,1] [,2] [,3]
#> [1,] 1 5 9
#> [2,] 2 6 10
#> [3,] 3 7 11
#> [4,] 4 8 12
m[1, 2]#> [1] 5
m[1:3, 2:3]#> [,1] [,2]
#> [1,] 5 9
#> [2,] 6 10
#> [3,] 7 11
m[, 1]#> [1] 1 2 3 4
m[1, ]#> [1] 1 5 9
m[1, 2] is 5: row 1, column 2. Every indexing technique from Vectors works on each side of the comma: negative positions, logical conditions, names.
marks["ben", "programming"]#> [1] 22
marks[marks[, "statistics"] >= 26, ] # students with at least 26 in statistics#> statistics programming
#> anna 28 27
#> ben 30 22
drop = FALSE: keeping the matrix shape
Notice that m[, 1] printed a plain vector, not a one-column matrix. When a selection has only one row or one column, R drops the unnecessary dimension. This is usually convenient, but inside functions it can break code that expects a matrix. Add drop = FALSE to keep the result two-dimensional:
dim(m[, 1]) # NULL: it is a vector#> NULL
m[, 1, drop = FALSE]#> [,1]
#> [1,] 1
#> [2,] 2
#> [3,] 3
#> [4,] 4
dim(m[, 1, drop = FALSE]) # 4 x 1#> [1] 4 1
Without a comma, a single index treats the matrix as the underlying vector (column by column): m[5] is the 5th element counting down the columns, and m[m > 6] returns all the values greater than 6 as a vector.
5. Matrix arithmetic
Ordinary operators work element by element, exactly as for vectors:
A <- matrix(c(2, 1, 1, 3), nrow = 2)
B <- matrix(1:4, nrow = 2)
A#> [,1] [,2]
#> [1,] 2 1
#> [2,] 1 3
B#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 4
A * B # element by element#> [,1] [,2]
#> [1,] 2 3
#> [2,] 2 12
A + 10 # 10 is recycled to every element#> [,1] [,2]
#> [1,] 12 11
#> [2,] 11 13
Matrix multiplication in the linear-algebra sense — rows times columns — uses a different operator, %*%:
A %*% B#> [,1] [,2]
#> [1,] 4 10
#> [2,] 7 15
The top-left element of A %*% B is (row 1 of A) · (column 1 of B) \(= 2 \times 1 + 1 \times 2 = 4\). Confusing * with %*% does not produce an error when the matrices are square: it silently gives a different answer.
Two more operations you will meet in statistics:
t(B) # transpose: rows become columns#> [,1] [,2]
#> [1,] 1 2
#> [2,] 3 4
solve(A) # inverse of A#> [,1] [,2]
#> [1,] 0.6 -0.2
#> [2,] -0.2 0.4
round(A %*% solve(A), 10) # identity matrix (rounded: floating point!)#> [,1] [,2]
#> [1,] 1 0
#> [2,] 0 1
solve(A, c(5, 10)) # solves the system A x = (5, 10)#> [1] 1 3
We rounded A %*% solve(A) because tiny floating-point errors (see Data types) would otherwise show up as numbers like \(10^{-16}\) instead of exact zeros.
For \(A\) of size \(n \times k\) and \(B\) of size \(k \times p\), the product \(C = AB\) is the \(n \times p\) matrix with \[
c_{ij} = \sum_{l=1}^{k} a_{il}\, b_{lj} .
\] The number of columns of \(A\) must equal the number of rows of \(B\). solve(A, b) finds the vector \(x\) such that \(Ax = b\); for our example, \[
\begin{pmatrix} 2 & 1 \\ 1 & 3 \end{pmatrix}
\begin{pmatrix} x_1 \\ x_2 \end{pmatrix}
=
\begin{pmatrix} 5 \\ 10 \end{pmatrix}
\quad\Rightarrow\quad x = (1, 3).
\] Least-squares regression (Simple regression) is, at its core, the solution of such a system: \(\hat\beta = (X^\top X)^{-1} X^\top y\). R’s lm() uses a numerically more stable method (a QR decomposition) rather than computing the inverse explicitly.
6. Row and column summaries
Summaries by row or by column are very common, and R has fast dedicated functions:
marks#> statistics programming
#> anna 28 27
#> ben 30 22
#> carla 24 26
rowMeans(marks) # average mark of each student#> anna ben carla
#> 27.5 26.0 25.0
colMeans(marks) # average mark in each subject#> statistics programming
#> 27.33 25.00
rowSums(marks)#> anna ben carla
#> 55 52 50
colSums(marks)#> statistics programming
#> 82 75
For any other function, apply() applies it to each row (MARGIN = 1) or each column (MARGIN = 2):
apply(marks, 1, max) # best mark of each student#> anna ben carla
#> 28 30 26
apply(marks, 2, sd) # spread of marks in each subject#> statistics programming
#> 3.055 2.646
apply() is your first taste of iteration without loops; it is developed in Iteration.
7. One type only
Try to put numbers and text in the same matrix:
matrix(c(1, "a", TRUE, 2), nrow = 2)#> [,1] [,2]
#> [1,] "1" "TRUE"
#> [2,] "a" "2"
Everything was coerced to character (note the quotes), following the rules from Data types. The same happens if you convert a dataset with mixed columns:
as.matrix(penguins[1:3, c("species", "bill_len")])#> species bill_len
#> 1 "Adelie" "39.1"
#> 2 "Adelie" "39.5"
#> 3 "Adelie" "40.3"
The bill lengths are now text: you can no longer compute their mean.
8. Matrix or data frame?
| Matrix | Data frame | |
|---|---|---|
| Types | One type for the whole table | One type per column |
| Typical use | Linear algebra, numeric tables, images, distance and correlation tables | Datasets: rows = observations, columns = variables |
| Row and column access | m[i, j] |
df[i, j], df$col |
A real dataset almost always mixes types — penguins has factors (species, island, sex), decimals and integers — so we store it as a data frame (Data frames). Matrices appear when a method needs pure numbers: you will create them explicitly with as.matrix() before PCA or clustering, for instance.
- Forgetting that
matrix()fills by column. If your data are written row by row, usebyrow = TRUE. - Forgetting the comma.
m[2]is the 2nd element;m[2, ]is the 2nd row. - Unexpected dropping.
m[, 1]is a vector, not a matrix. Usedrop = FALSEwhen the shape matters. *instead of%*%. Element-wise and matrix products are different; with square matrices R will not warn you.- Mixed types. A single text value turns the whole matrix into text.
- Silent recycling in
cbind().cbind(1:3, 1:2)only warns and recycles. Check dimensions withdim().
9. Check your understanding
10. Exercises
Interactive (run in your browser)
★ Exercise 1 — build a matrix. Create a 3 x 4 matrix q containing the numbers 1 to 12 filled by row. Then print its dimensions and the element in row 2, column 3.
Three rows, byrow = TRUE, and q[row, column].
q <- matrix(1:12, nrow = 3, byrow = TRUE)
q
dim(q) # 3 4
q[2, 3] # 7Filled by row, row 2 contains 5, 6, 7, 8, so the third element is 7. With the default byrow = FALSE, it would be 8.
★★ Exercise 2 — slicing and drop. Using the matrix marks below: (a) extract the programming marks of all students; (b) extract the same column as a one-column matrix; (c) add a column history with marks 25, 29, 30; (d) compute each student’s average.
Use drop = FALSE; a named argument in cbind() becomes the column name; rowMeans() averages each row.
marks[, "programming", drop = FALSE]
marks <- cbind(marks, history = c(25, 29, 30))
rowMeans(marks) # anna 26.67, ben 27.00, carla 26.67★★ Exercise 3 — predict the output: * vs %*%. Without running the code, predict the top-left element of each result. Then run it.
P is 1 3 / 2 4 (filled by column) and Q is 0 1 / 1 0. For %*%, multiply row 1 of the left matrix by column 1 of the right one and add.
P * Q has top-left \(1 \times 0 = 0\). P %*% Q has top-left \(1 \times 0 + 3 \times 1 = 3\) — multiplying by Q on the right swaps the columns of P. Q %*% P has top-left \(0 \times 1 + 1 \times 2 = 2\) — multiplying on the left swaps the rows. So, unlike numbers, P %*% Q and Q %*% P differ.
★★ Exercise 4 — column summaries on real data. Build a numeric matrix with the four measurement columns of penguins for complete rows only, then compute the mean and the standard deviation of each column.
colMeans() for the means; MARGIN = 2 means “by column”.
colMeans(meas)
apply(meas, 2, sd)complete.cases() keeps the 342 rows without any missing value. The columns are on very different scales (millimetres vs grams): this is why methods such as PCA usually standardise variables first.
Practice on your own computer
★★★ Exercise 5 — a linear system. A café sells coffee and croissants. On Monday, 3 coffees and 2 croissants cost 7.30 €; on Tuesday, 1 coffee and 4 croissants cost 7.10 €. Write the system as \(A x = b\) and find the two prices with solve(). Check your answer with %*%.
A <- matrix(c(3, 2,
1, 4), nrow = 2, byrow = TRUE)
b <- c(7.30, 7.10)
prices <- solve(A, b)
prices # coffee 1.50, croissant 1.40
A %*% prices # should give back 7.30 and 7.10Each row of A is one day, each column one product; byrow = TRUE lets you type the system as you would write it on paper.
Summary
| Function | What it does |
|---|---|
matrix(x, nrow, ncol, byrow) |
Create a matrix (filled by column by default) |
dim(), nrow(), ncol() |
Dimensions |
dimnames(), rownames(), colnames() |
Row and column names |
cbind(), rbind() |
Add columns / rows |
m[i, j], m[i, ], m[, j], drop = FALSE |
Extract elements, rows, columns |
* vs %*% |
Element-wise vs matrix product |
t(), solve() |
Transpose; inverse or solve \(Ax = b\) |
rowSums(), colMeans(), apply() |
Row/column summaries |
- A matrix is a vector with two dimensions and one type.
[row, col]: a comma separates rows from columns; an empty side means “all”.- Single rows/columns are dropped to vectors unless
drop = FALSE. - For datasets with mixed types, use data frames.