16 Matrices and Arrays
In this chapter we introduce R arrays
, which are multidimensional
data objects including 2-dimensional arrays better known as matrices, and
N-dimensional arrays (generic arrays).
16.1 From Vectors to Arrays
In thre previous three chapters, we discussed a number of ideas and concepts that basically have to do with vectors. As you know, a vector is a one-dimensional atomic object. While many data sets can be handled with a vector, there are occasions in which one dimension is not enough. The classic example for when one-dimensional objects are not enough involves working with data that fits better into a tabular structure. This type of structure requires two dimensions: one for rows, and another one for columns.
R provides a handful of rectangular or 2-dimensional objects. One of them is
matrix
which is a two-dimensional atomic object. In turns out that an R matrix
is a special type of multi-dimensional atomic object called array
.
So, how do you go from a vector
into a generic array
? Conceptually, we can
transform a vector into an n-dimensional array by giving it a dimensions
"dim"
attribute with dim()
:
# simple vector
x <- 1:8
x
#> [1] 1 2 3 4 5 6 7 8
# adding 'dim' attribute: 2 rows, 4 columns
dim(x) <- c(2, 4)
x
#> [,1] [,2] [,3] [,4]
#> [1,] 1 3 5 7
#> [2,] 2 4 6 8
A couple of things to notice:
- a vector can be given a
dim
attribute - a
dim
attribute is a numeric vector of lengthn
> 2 - R will reorganize the elements of the vector into
n
dimensions - each dimension will have as many rows (or columns, etc.) as the
n
-th value of thedim
vector
Here’s another example converting a vector into an array with three dimensions:
# simple vector
x <- 1:8
# dim attribute with 3 dimensions
dim(x) <- c(2, 2, 2)
x
#> , , 1
#>
#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 4
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] 5 7
#> [2,] 6 8
The vector c(2, 2, 2)
passed to dim()
indicates that the produced array
should have 2 slices (first index 2), each slice containing 2-rows (second
index 2) and 2-columns (third index 2).
One important detail to keep in mind when converting a vector into either a
matrix or a generic array, is to make sure that the length of the vector
matches the product of the values given for each dimension. In the previous
examples, we first converted vector x
of length 8, into a matrix of 2 rows
and 4 columns (\(2 \times 4 = 8\)); and then we converted x
into an array of
three dimensions each one having 2 elements (\(2 \times 2 \times 2 = 8\)).
We should say that these examples are purely conceptual, and the main purpose
is to illustrate the notion of dimensions in atomic objects, and how R can
internally generate a matrix or an array from an input vector by giving it a
dimensions attribute via the function dim()
. We should also say that using
dim()
like in the above examples, is not really how you should create
matrices and arrays. You should instead use dedicated functions such as
matrix()
and array()
:
# create a matrix with matrix()
mat <- matrix(1:12, nrow = 3, ncol = 4)
mat
#> [,1] [,2] [,3] [,4]
#> [1,] 1 4 7 10
#> [2,] 2 5 8 11
#> [3,] 3 6 9 12
# create an array with array()
arr <- array(1:12, dim = c(2, 2, 3))
arr
#> , , 1
#>
#> [,1] [,2]
#> [1,] 1 3
#> [2,] 2 4
#>
#> , , 2
#>
#> [,1] [,2]
#> [1,] 5 7
#> [2,] 6 8
#>
#> , , 3
#>
#> [,1] [,2]
#> [1,] 9 11
#> [2,] 10 12
16.2 Matrices
The most common type of array in R is the two-dimensional array also know as
matrix. We saw how to take a vector and convert it into a matrix with dim()
.
In practice, however, the formal way to create matrices is with matrix()
,
which has the following usage syntax:
matrix(data = NA, nrow = 1, ncol = 1, byrow = FALSE, dimnames = NULL)
To make a 2-by-4 matrix from an input vector x <- 1:8
, you use matrix()
and specify the number of rows and columns with arguments nrow = 2
and
ncol = 4
as follows:
# vector to matrix
x <- 1:8
x
#> [1] 1 2 3 4 5 6 7 8
X <- matrix(x, nrow = 2, ncol = 4)
X
#> [,1] [,2] [,3] [,4]
#> [1,] 1 3 5 7
#> [2,] 2 4 6 8
What happens when a matrix is created with matrix()
:
R always fills up each matrix by columns.
You can also assign dim-names to an array.
If you don’t specify names for rows and columns, which is the default value of argument
dimnames = NULL
, rows and columns will be unnamed.As mentioned before, the number of cells in a matrix—given by the product of number of rows and columns—has to match the length of the input vector, otherwise R will give you an error.
Internally, R always stores matrices by columns, and this is technically known
as column-major. Many other languages store matrices or 2-dimensional
arrays in this form, such as Fortran, Matlab, and Julia (but not like C or
Python with "numpy"
).
Even though internally a matrix is always stored by columns, sometimes you may
want to create a matrix by rows. This is possible thanks to the argument byrow
.
# vector to matrix (default behavior)
matrix(1:8, nrow = 2, ncol = 4, byrow = FALSE)
#> [,1] [,2] [,3] [,4]
#> [1,] 1 3 5 7
#> [2,] 2 4 6 8
# vector to matrix (by rows)
matrix(1:8, nrow = 2, ncol = 4, byrow = TRUE)
#> [,1] [,2] [,3] [,4]
#> [1,] 1 2 3 4
#> [2,] 5 6 7 8
16.2.1 Bracket Notation
The matrix mat
is a 2-dimensional object: the 1st dimension corresponds
to the rows, while the 2nd dimension corresponds to the columns.
Because mat
has two dimensions, the bracket notation involves
working with matrices in this form: mat[ , ]
.
In other words, you have to specify values inside the
brackets for the 1st index, and the 2nd index: mat[index1, index2]
.
Selecting cells
By specifying index vectors in the row and column margins, you can select sets of cells in a matrix. Typically, the input index vectors consist of positive numeric values indicating the position of the row(s) and column(s) to focus on. Likewise, it is also possible to exclude certain rows-and-columns by passing negative numeric indices.
Selecting rows
With bracket notation, you can focus just on the rows of a matrix. This is done by specifying a vector for the row index, while leaving empty the position of the columns.
Selecting columns
You can also focus just on the columns of a matrix by specifying a vector for the column index, while leaving empty the position of the rows.
Row and Column Binding
To combine two or more matrices into a single one, you can use the binding
functions rbind()
and cbind()
. Row binding with rbind()
allows you to
stack two or more matrices, as long as they have the same number of columns.
In turn, column binding with cbind()
allows you to join two or more
matrices, as long as they have the same number of rows.
R objects versus Algebra objects
It is important to distinguish vectors and matrices, especially in R:
In matrix algebra we use the convention that vectors are column vectors (i.e. they are \(n \times 1\) matrices).
In R, a
vector
with \(n\) elements is not the same as an \(n \times 1\)matrix
.Vectors in R behave more like “row vectors” (especially when displayed).
However, depending on the type of functions you apply to vectors, sometimes R will handle vectors like if they were column vectors.
R matrices versus R data frames
It’s also important to distinguish between a matrix
and a data.frame
. We haven’t talked that much about data frames (to be formally covered in chapter
R Data Frames), but you’ve already worked with them in part III
of the book. Both matrices and data frames are tabular structures provided by R,
with some similarities and some important differences:
Both objects allow us to store data in a 2-dimensional object.
In many cases, both R matrices and data frames have similar behaviors.
This is mostly the case when they are displayed on the screen.
And sometimes it is hard to distinguish between a matrix and a data frame by just looking at the displayed content on the screen.
About Arrays
- Arrays store values in an n-dimensional array
- To create an array, give a vector to
array()
and specify number of dimensions
16.2.2 Basic Matrix Operations
We first quickly go through the basic matrix operations in R
- transpose
- addition
- scalar multiplication
- matrix-vector multiplication
- matrix-matrix multiplication
16.2.3 Transpose
The transpose of a \(n \times p\) matrix \(\mathbf{X}\) is the \(p \times n\) matrix
\(\mathbf{X}^{\mathsf{T}}\). In R the transpose is given by the function t()
16.2.4 Matrix Addition
Matrix addition of two matrices \(\mathbf{A} + \mathbf{B}\) is defined when \(\mathbf{A}\) and \(\mathbf{B}\) have the same dimensions:
16.2.5 Scalar Multiplication
We can multiply a matrix by a scalar using the usual product operator *
,
moreover it doesn’t matter if we pre-multiply or post-multiply:
X <- matrix(1:3, 3, 4)
# (pre)multiply X by 0.5
(1/2) * X
#> [,1] [,2] [,3] [,4]
#> [1,] 0.5 0.5 0.5 0.5
#> [2,] 1.0 1.0 1.0 1.0
#> [3,] 1.5 1.5 1.5 1.5
You can also postmultiply by a scalar (although this is not recommended because may confuse readers):
16.2.6 Matrix-Matrix Multiplication
The matrix product operator in R is %*%
.
We can multiply matrices \(\mathbf{A}\) and \(\mathbf{B}\) if the number of columns
of \(\mathbf{A}\) is equal to the number of rows of \(\mathbf{B}\)
We can multiply matrices \(\mathbf{A}\) and \(\mathbf{B}\) if the number of columns of \(\mathbf{A}\) is equal to the number of rows of \(\mathbf{B}\)
16.2.7 Cross-Products
A very common type of products in multivariate data analysis are \(\mathbf{X^{\mathsf{T}} X}\) and \(\mathbf{X X^{\mathsf{T}}}\), sometimes known as cross-products:
# cross-product
t(A) %*% A
#> [,1] [,2] [,3]
#> [1,] 5 11 17
#> [2,] 11 25 39
#> [3,] 17 39 61
# cross-product
A %*% t(A)
#> [,1] [,2]
#> [1,] 35 44
#> [2,] 44 56
R provides functions crossprod()
and tcrossprod()
which are formally
equivalent to:
\(\texttt{crossprod(X, X)} \equiv \texttt{t(X) \%*\% X}\)
\(\texttt{tcrossprod(X, X)} \equiv \texttt{X \%*\% t(X)}\)
However, crossprod()
and tcrossprod()
are usually faster than using
t()
and %*%
16.2.8 Matrix-Vector Multiplication
We can post-multiply an \(n \times p\) matrix \(\mathbf{X}\) with a vector \(\mathbf{b}\) with \(p\) elements. This means making linear combinations (weighted sums) of the columns of \(\mathbf{X}\):
X <- matrix(1:12, 3, 4)
b <- seq(0.25, 1, by = 0.25)
X %*% b
#> [,1]
#> [1,] 17.5
#> [2,] 20.0
#> [3,] 22.5
In R we can pre-multiply a vector \(\mathbf{a}\) (with \(n\) elements) with an \(n \times p\) matrix \(\mathbf{X}\). This means making linear combinations (weighted sums) of the rows of \(\mathbf{X}\):
Notice that when we use the product operator %*%
, R is smart enough to use
the convention that vectors are \(n \times 1\) matrices.
Notice also that if we ask for a vector-matrix multiplication, we can use both formulas:
a %*% X
t(a) %*% X
In case you are curious, here are some other interesting functions for matrices
det()
: determinantdiag()
: extract or replace the diagonal elementssolve()
: solve system of equationssvd()
: singular value decompositioneigen()
: eigen-decompositionqr()
: QR decompositionchol()
: Choleski decomposition
16.3 Exercises
1) Given the following vector hp
, how would you create the matrix
displayed below?
[,1] [,2]
[1,] "Harry" "Weasley"
[2,] "Potter" "Hermione"
[3,] "Ron" "Granger"
2) Given the following vector sw
, how would you create the matrix
displayed below?
[,1] [,2]
[1,] "Luke" "Skywalker"
[2,] "Leia" "Organa"
[3,] "Han" "Solo"
3) Consider the following vectors a1, a2, a3
:
Column-bind the vectors a1, a2, a3
to form the following matrix A
:
#> a1 a2 a3
#> 1 2 1.88 80
#> 2 3 2.05 90
#> 3 6 1.70 70
#> 4 7 1.60 50
#> 5 10 1.78 75
4) Consider the following vectors b1, b2, b3
:
Row-bind the vectors b1, b2, b3
to form the following matrix B
:
#> 1 2 3 4 5
#> b1 1.00 4.00 5.0 8.0 9.00
#> b2 1.22 1.05 3.6 0.4 2.54
#> b3 20.00 40.00 30.0 80.0 100.00
5) With matrices A
and B
created above, use the matrix-multiplication
operator %*%
and the transpose function t()
to compute the matrix products:
\(\mathbf{AB}\)
\(\mathbf{BA}\)
\(\mathbf{A^\mathsf{T} B^\mathsf{T}}\)
\(\mathbf{B^\mathsf{T} A^\mathsf{T}}\)