Matrices

Two-dimensional tables of one type: create, bind, slice and multiply

Create matrices with matrix(), fill them by row or column, add rows and columns, name dimensions, extract elements (including drop = FALSE), and use element-wise and matrix arithmetic, transposition, solve() and row/column summaries.

Level 1 · Foundations ⏱ ~50 min Prerequisites: Vectors

NoteLearning objectives

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

  • create a matrix with matrix(), choosing how it is filled with byrow;
  • inspect and set its dimensions and names with dim() and dimnames();
  • add rows and columns with rbind() and cbind();
  • extract elements, rows and columns with [row, col], keeping the matrix shape with drop = FALSE;
  • tell element-wise multiplication * from matrix multiplication %*%, and compute row and column summaries.
# Base R only: nothing to load.

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: if FALSE (default) values fill the matrix column by column; if TRUE, 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
TipHow to read this output

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.

WarningCommon pitfalls
  1. Forgetting that matrix() fills by column. If your data are written row by row, use byrow = TRUE.
  2. Forgetting the comma. m[2] is the 2nd element; m[2, ] is the 2nd row.
  3. Unexpected dropping. m[, 1] is a vector, not a matrix. Use drop = FALSE when the shape matters.
  4. * instead of %*%. Element-wise and matrix products are different; with square matrices R will not warn you.
  5. Mixed types. A single text value turns the whole matrix into text.
  6. Silent recycling in cbind(). cbind(1:3, 1:2) only warns and recycles. Check dimensions with dim().

9. Check your understanding

6. Filled by column, the matrix is 1 3 5 in row 1 and 2 4 6 in row 2; row 2, column 3 is 6.

length(X) is 300 (all elements), and dim(t(X)) is 3 100.

Because its columns have different types (factors, numbers). A matrix would coerce everything to character.

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]    # 7

Filled 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.10

Each 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.
Back to top