Intro to R: Linear Algebra. Vector Indexing, Eigenvector Solvers... | by Andre Ye | Towards Data Science

R is a very powerful language developed especially for data analysis, data visualization, and machine learning, which makes it a must-learn for any aspiring data scientist.
R is especially handy with linear algebra. Its built-in data types like vectors and matrices mesh well with built-in functions like eigenvalue and determinant solvers and dynamic indexing capabilities.
This Intro to R article will cover the following linear algebra topic implementations in R:
Vector Assignment
Vector Operations
Generating Sequences
Logical Vectors
Missing Values
Indexing Vectors
Arrays
Array Indexing
Matrix Indexing
Outer Product of 2 Arrays
Demonstration: All Possible Determinants of 2x2 Single-Digit Matrices
Generalized Transpose of an Array
Matrix Multiplication
Eigenvalues and Eigenvectors
Singular Value Decomposition and Determinants
Least Squares Fitting and QR Decomposition
Forming Partitioned Matrices
R operates on data structures, of which the simplest is the numeric vector. A vector is an ordered collection of numbers. To create a vector x with four elements 1, 2, 3, and 4, one can use the concatenate function c().
x <- c(1, 2, 3, 4)
The assignment operator <- is used in this scenario, and points to the object being assigned. In most contexts, <- can be switched with =.
The function assign() can also be used:
assign('x', c(1, 2, 3, 4))
The operator <- is thought to be a shortcut for this.
Assignments can also be made in the other direction:
c(1, 2, 3, 4) -> x
Vectors can also be used in a variety of ways.
The operation y <- c(x, 0, x) would assign a vector 1, 2, 3, 4, 0, 1, 2, 3, 4 to variable y.
Vectors can be freely multiplied and added by constants:
v <- 2*x + y + 1
Note that this operation is valid even when x and y are different lengths. In this case, R will simply recycle x (sometimes fractionally) until it meets the length of y. Since y is 9 numbers long and x is 4 units long, x will be repeated 2.25 times to match the length of y.
The arithmetic operators +, -, *, /, and ^ can all be used. log, exp, sin, cos, tan, sqrt, and more can also be used. max(x) and min(x) represent the largest and smallest elements of a vector x, and length(x) is the number of elements in x. sum(x) gives the total of the elements in x, and prod(x) their product.
mean(x) calculates the sample mean, and var(x) returns the sample variance. sort(x) returns a vector of the same size as x with elements arranged in increasing order.
R has many methods for generating sequences of numbers. 1:30 is the same as c(1, 2, ..., 29, 30). The colon as the highest priority in an expression, so2*1:15 will return c(2, 4, ..., 28, 30) instead of c(2, 3, ..., 14, 15).
30:1 may be used to generate the sequence backwards.
The seq() function can also be used to generate sequences. seq(2,10) returns the same vector as 2:10. In seq(), one can also specify the length of the step in which to take: seq(1,2,by=0.5) returns c(1, 1.5, 2).
A similar function is rep(), which replicates an object in various ways. For example, rep(x, times=5) will return five copies of x end-to-end.
Logical values in R are TRUE, FALSE, and NA. Logical vectors are set by conditions. val <- x > 13 sets val as a vector of the same length as x with values TRUE where the condition is met and FALSE where the condition is not.
The logical operators in r are <, <=, >, >=, ==, and !=, which mean less than, less than or equal to, greater than, greater than or equal to, equality, and inequality.
The function is.na(x) returns a logical vector of the same size as x with TRUE if the corresponding element to x is NA.
x == NA is different from is.na(x) since NA is not a value but a marker for an unavailable quantity.
A second type of ‘missing value’ is that which is produced by numerical computation, such as 0/0. In this case, NaN (Not a Number) values are treated as NA values; that is, is.na(x) will return TRUE for both NA and NaN values. is.nan(x) can be used only for identifying NaN values.
The first kind of indexing is through a logical vector. y <- x[!is.na(x)] sets y to the values of x that are not equal to NA or NaN.
(x+1)[(!is.na(x)) & x>0] -> z sets z to the values of x+1 that are not Na or NaN and larger than 0.
A second method is with a vector of positive integral quantities. In this case, the values must be in the set {1, 2, ..., length(x)}. The corresponding elements of the vector are selected and concatenated in that order to form a result. It is important to remember that unlike in other languages, the first index in R is 1 and not 0.
x[1:10] returns the first 10 elements of x, assuming length(x) is not less than 10. c(‘x’, ‘y’)[rep(c(1,2,2,1), times=4)] produces a character vector of length 16, where ‘x’, ‘y’, ‘y’, ‘x’ is repeated four times.
A vector of negative integral numbers specifies the values to be excluded rather than included. y <- x[-(1:5)] sets y to all but the first five values of x.
Lastly, a vector of character strings can be used when an object has a names attribute to identify its components. With fruit <- c(1, 2, 3, 4), one can set the names of each index of the vector fruit with names(fruit) <- c(‘mango’, ‘apple’, ‘banana’, ‘orange’). Then, one can call the elements by name with lunch <- fruit[c(‘apple’, ‘orange’)].
The advantage of this is that alphanumeric names can sometimes be easier to remember than indices.
Note that an indexed expression can also appear on the receiving end of an assignment, in which the assignment is only performed on those elements of a vector. For example, x[is.na(x)] <- 0 replaces all NA and NaN values in vector x with the value 0.
Another example: y[y<0] <- -y[y<0] has the same effect as y <- abs(y). The code simply replaces all the values that are less than 0 with the negative of that value.
An array is a subscripted collection of data entries, not necessarily numeric.
A dimension vector is a vector of non-negative integers. If the length is k then the array is k-dimensional. The dimensions are indexed from one up to the values given in the dimension vector.
A vector can be used by R as an array as its dim attribute. If z were a vector of 1500 elements, the assignment dim(z) <- c(100, 5, 3) would mean z is now treated as a 100 by 5 by 3 array.
Individual elements of an array can be referenced by giving the name of the array followed by the subscripts in square brackets, separated by columns.
A 3 by 4 by 6 vector a could have its first value called via a[1, 1, 1] and its last value called via a[3, 4, 6].
a[,,] represents the entire array; hence, a[1,1,] takes the first row of the first 2-dimensional cross-section in a.
The following code generates a 4 by 5 array: x <- array(1:20, dim = c(4,5)).
Arrays are specified by a vector of values and the dimensions of the matrix. Values are calculated top-down first, left-right second.
array(1:4, dim = c(2,2)) would return
1 32 4
and not
1 23 4
Negative indices are not allowed in index matrices. NA and zero values are allowed.
An important operation on arrays is the outer product. If a and b are two numeric arrays, their outer product is an array whose dimension vector is obtained by concatenating the two dimension vectors and whose data vector is achieved by forming all possible products of elements of the data vector of a with those of b. The outer product is calculated with the operator %o%:
ab <- a %o% b
Another way to achieve this is
ab <- outer(a, b, ‘*’)
In fact, any function can be applied on two arrays using the outer() function. Suppose we define a function f <- function(x, y) cos(y)/(1+x2). The function could be applied to two vectors x and y via z <- outer(x, y, f).
Consider the determinants of 2 by 2 matrices [a, b; c, d] where each entry is a non-negative integer from 0 to 9. The problem is to find the determinants of all possible matrices in this form and represent the frequency of which the value occurs with a high density plot.
Rephrased, find the probability distribution of the determinant if each digit is chosen independently and uniformly at random.
One clever way of doing this uses the outer(0 function twice.
d <- outer(0:9,0:9)fr <- table(outer(d, d, ‘-’))plot(fr, xlab = ‘Determinant’, ylab = ‘Frequency’)
The first line assigns d to this matrix:
The second line uses the outer() function again to calculate all possible determinants, and the last line plots it.
The function aperm(a, perm) can be used to permute an array a. The argument perm must be the permutation of the integers {1,..., k} where k is the number of subscripts in a. The result of the function is an array of the same size as a but with the old dimension given by perm[j] becoming the new j-th dimension.
An easy way to think about it is a generalization of transposition for matrices. If A is a matrix, then B is simply the transpose of A:
B <- aperm(A, c(2, 1))
In these special cases the function t() performs a transposition.
The operator %*% is used for matrix multiplication. If A and B are square matrices of the same size, A*B is the element-wise product of the two matrices. A %*% B is the dot product (matrix product).
If x is a vector, then x %*% A %*% x is a quadratic form.
crossprod() performs cross-products; thus, crossprod(X, y) is the same as the operation t(X) %*% y, but more efficient.
diag(v), where v is a vector, gives a diagonal matrix with elements of the vector as the diagonal entries. diag(M), where m is a matrix, gives the vector of the main diagonal entries of M (the same convention as in Matlab). diag(k), where k is a single numeric value, returns a k by k identity matrix.
Solving linear equations is the inverse of matrix multiplication. When
b <- A %*% x
with only A and b given, vector x is the solution of the linear equation system. This can be solved quickly in R with
solve(A, b)
The function eigen(Sm) calculates the eigenvalues and eigenvectors of a symmetric matrix Sm. The result is a list, with the first element named values and the second named vectors. ev <- eigen(Sm) assigns this list to ev.
ev$val is the vector of eigenvalues of Sm and ev$vec the matrix of corresponding eigenvectors.
For large matrices, it is better to avoid computing the eigenvectors if they are not needed by using the expression
evals <- eigen(Sm, only.values = TRUE)$values
The function svd(m) takes an arbitrary matrix argument, m, and calculates the singular value decomposition of m. This consists of a matrix of orthonormal columns U with the same column space as m, a second matrix of orthonormal columns V whose column space is the row space of m and a diagonal matrix of positive entries D such that
m = U %*% D %*% t(V)
det(m) can be used to calculate the determinant of a square matrix m.
The function lsfit() returns a list giving results of a least squares fitting procedure. An assignment like
ans <- lsfit(X, y)
gives results of a least squares fit where y is the vector of observations and X is the design matrix.
ls.diag() can be used for regression diagnostics.
A closely related function is qr().
b <- qr.coef(Xplus,y)fit <- qr.fitted(Xplus,y)res <- qr.resid(Xplus,y)
These compute the orthogonal projection of y onto the range of X in fit, the projection onto the orthogonal complement in res and the coefficient vector for the projection in b.
Matrices can be built up from other vectors and matrices with the functions cbind() and rbind().
cbind() forms matrices by binding matrices horizontally (column-wise), and rbind() binds matrices vertically (row-wise).
In the assignment X <- cbind(arg_1, arg_2, arg_3, ...) the arguments to cbind() must be either vectors of any length, or columns with the same column size (the same number of rows).
rbind() performs a corresponding operation for rows.
I hope this Introduction to R was able to give you a glimpse of R’s powerful features in linear algebra.