Julia: Tutorial & Code-Collection¶

Source: https://github.com/markomlikota/CodingSoftware

MIT License, © Marko Mlikota, https://markomlikota.github.io

Matrices & Arrays¶

  • Like vectors, matrices allow us to store many elements (objects) in a single object.
  • Unlike vectors, matrices order these elements along two dimensions (rather than just one).
  • Arrays generalize matrices by storing elements along 3 or more dimensions.
  • Lots of the discussion about vectors applies analogously to matrices and arrays.
In [1]:
# Let's install the new packages needed in this section:
Pkg.add("SparseArrays")
   Resolving package versions...
  No Changes to `/opt/conda/julia/environments/v1.10/Project.toml`
  No Changes to `/opt/conda/julia/environments/v1.10/Manifest.toml`

Creation¶

In [2]:
# Create a 3 x 2 matrix:

mA = [1 2; 3 4; 5 6]
Out[2]:
3×2 Matrix{Int64}:
 1  2
 3  4
 5  6
In [3]:
# 3 x 2 matrix of zeros or ones or any other number:
zeros(3,2)
ones(3,2)
repeat([2],3,2) # or:
fill(2, 3,2)

# Note that some are matrices of integers, some are matrices of floats.
Out[3]:
3×2 Matrix{Int64}:
 2  2
 2  2
 2  2
In [4]:
# 3x2 matrix of trues or falses:

trues(3,2)
falses(3,2)
Out[4]:
3×2 BitMatrix:
 0  0
 0  0
 0  0
In [5]:
# 3x2 matrix of NaNs:
NaN*ones(3,2) # or:
repeat([NaN],3,2) # or: 
fill(NaN, 3,2)
Out[5]:
3×2 Matrix{Float64}:
 NaN  NaN
 NaN  NaN
 NaN  NaN
In [6]:
# 3x2 matrix of strings:
["a" "b" "c"; "d" "e" "f"]
repeat(["rum"],3,2)
Out[6]:
3×2 Matrix{String}:
 "rum"  "rum"
 "rum"  "rum"
 "rum"  "rum"
In [7]:
# Create matrix out of several vectors:

va = [1,2,3]
vb = [3,3,4]

mA = [va vb]
Out[7]:
3×2 Matrix{Int64}:
 1  3
 2  3
 3  4
In [8]:
# Create matrix by repeating a single vector or a single matrix:

va = [1,2,3]
repeat(va,2,3) # or, more precisely:
repeat(va,outer=(2,3))
repeat(va,inner=(2,3))

mA = [1 2; 3 4]
repeat(mA,2,3) # or, more precisely:
repeat(mA,outer=(2,3))
repeat(mA,inner=(2,3))
Out[8]:
4×6 Matrix{Int64}:
 1  1  1  2  2  2
 1  1  1  2  2  2
 3  3  3  4  4  4
 3  3  3  4  4  4
In [9]:
# We can also create/fill-in a matrix by comprehension (see section on for-loops):

mA = [jj*exp(ii) for ii in 1:5, jj=1:3]

mA = ones(5,2)
[mA[ii,2] = exp(ii) for ii in 1:5]
# (see also discussion of indexing below)
Out[9]:
5-element Vector{Float64}:
   2.718281828459045
   7.38905609893065
  20.085536923187668
  54.598150033144236
 148.4131591025766
In [10]:
# Diagonal matrix:
using LinearAlgebra
vx = [1, 2, 3]
diagm(vx) 

# 3x3 Identity matrix:
Matrix(I,3,3)
Out[10]:
3×3 Matrix{Bool}:
 1  0  0
 0  1  0
 0  0  1
In [11]:
# Analogously as with vectors, matrices can have different types, 
# inherited by the types of their elements.
# We can have matrices of integers, floats, logicals, strings, complex and rational numbers, 
# as well as matrices of functions, and all these types can be mixed in a single matrix,
# turning the matrix into type "Any":
mA = [3 true; "string" exp]
# We can also have matrices of vectors or matrices of matrices.


# 4x5 matrix of 3-element-vector of ones:
mva = [ones(3) for ii=1:4, jj=1:5] 

# 4x5 matrix of 3x2 matrices of ones:
mma = [ones(3,2) for ii=1:4, jj=1:5] 

# 4x5 matrix of empty float-vectors:
mva = [Vector{Float64}(undef,1) for ii=1:4, jj=1:5] 

# 4x5 matrix of empty float-matrices:
mma = [Matrix{Float64}(undef,1,1) for ii=1:4, jj=1:5] 

# To accommodate underlying vectors/matrices of other types,
# instead of "Float64", type "Int" or "Complex{Float64}" or "Rational{Int64}" or "Bool" or "String".
# To accommodate underlying vectors/matrices of mixed type, instead of "Float64", type "Any".
Out[11]:
4×5 Matrix{Matrix{Float64}}:
 [7.13366e-313;;]  [7.10235e-313;;]  …  [2.03804e-312;;]  [6.94538e-310;;]
 [7.06753e-313;;]  [7.09956e-313;;]     [2.05205e-312;;]  [5.0e-324;;]
 [7.10235e-313;;]  [2.04193e-312;;]     [2.0428e-312;;]   [2.0237e-320;;]
 [7.10781e-313;;]  [2.04553e-312;;]     [6.94538e-310;;]  [6.94538e-310;;]
In [12]:
# Sparse Matrices:

# When working with large matrices with mostly zero elements,
# your code will likely be more efficient if you declare the matrices as sparse:

using SparseArrays
mA = spzeros(20,20) 

# In my experience, performing operations with sparse matrices is faster than
# using knowledge on the structure of sparse matrices to break up operations into several chunks
# (e.g. performing operations for each block of a block-diagonal matrix).
Out[12]:
20×20 SparseMatrixCSC{Float64, Int64} with 0 stored entries:
⎡⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎤
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎢⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎥
⎣⠀⠀⠀⠀⠀⠀⠀⠀⠀⠀⎦

Indexing, Basic Modifications & Element-Identification¶

In [13]:
mA = [1 2; 3 4; 5 6]
Out[13]:
3×2 Matrix{Int64}:
 1  2
 3  4
 5  6
In [14]:
# Dimensions of a matrix:

size(mA)[1] # number of rows
size(mA) # number of columns
size(mA) # both
Out[14]:
(3, 2)
In [15]:
# We can index (access elements of) matrices analogously as with vectors:

mA[1,2] # element (1,2)
mA[end,2] # last element in second column
mA[1:2,2] # first two elements in second column
mA[:,2] # entire second column
mA[3,:] # entire third row


# Again, we can use logical vectors for indexing, 
# and we can apply the function "getindex":

mA[[true,true,false],2]

getindex(mA,2,1)
Out[15]:
3
In [16]:
# Using indexing, we can modify parts of a matrix, 
# analogously as done for vectors:

# e.g. to set element (1,2) of mA to 5, we type
mA[1,2] = 5
# to set elements (1,2) and (2,2) of mA to [2,3], we type
mA[1:2,2] = [2,3]
# to set both elements (1,2) and (2,2) of mA to 5, we type
mA[1:2,2] .= 5
# to multiple both elements (1,2) and (2,2) of mA by 3, we type
mA[1:2,2] .*= 3
Out[16]:
2-element view(::Matrix{Int64}, 1:2, 2) with eltype Int64:
 15
 15
In [17]:
# In principle, we can apply the commands "sort" and "filter" also to matrices.
# However, they do not preserve the matrix-structure, but return a vector.

# The findall function does preserve the matrix-structure;
# it returns the 2-dimensional indices of elements that satisfy the stated condition:

findall(x-> sqrt(x)>2,mA)
Out[17]:
4-element Vector{CartesianIndex{2}}:
 CartesianIndex(3, 1)
 CartesianIndex(1, 2)
 CartesianIndex(2, 2)
 CartesianIndex(3, 2)
In [18]:
# We can delete rows and columns of a matrix 
# by indexing and saving the result to a (new) object
# (instead of using "filter", which is the preferred way for vectors):

includeTheseRows = [2,3]
mB = mA[includeTheseRows,:]
Out[18]:
2×2 Matrix{Int64}:
 3  15
 5   6

Try Exercises 5.a.i - 5.a.ii



Algebraic and Logical Operations¶

In [19]:
# All the comments above on performing elementary algebraic or logical operations on vectors
# also apply for matrices;

# we can add, subtract or multiply matrices 
# using the same signs as used for operations with scalars: +, -, *;

# we can multiply or divide matrices by scalars using the same signs: *, /;

# to add/subract a scalar to/from a matrix, we must use element-wise addition/subtraction: .+, .-;

# we can perform element-wise multiplication and division using .* and ./,
# and we can take element-wise powers using .^;

# we conduct element-wise comparisons using .==, .!==, .>, .>=, .<, .<=,
# which can be negated and combined.
In [20]:
# Of course, algebraic operations that mix matrices and vectors work analogously, e.g.:

mA = [1 2; 3 4; 5 6]
vb = [1, 2, 3]
vc = [5, 6]

vb' * mA - vc'
Out[20]:
1×2 adjoint(::Vector{Int64}) with eltype Int64:
 17  22
In [21]:
# Transpose:
mA'
Out[21]:
2×3 adjoint(::Matrix{Int64}) with eltype Int64:
 1  3  5
 2  4  6
In [22]:
# Kronecker product:
mB = [1 1; 0 0]
kron(mA,mB)
Out[22]:
6×4 Matrix{Int64}:
 1  1  2  2
 0  0  0  0
 3  3  4  4
 0  0  0  0
 5  5  6  6
 0  0  0  0
In [23]:
# Rank:
using LinearAlgebra
rank(mA)
Out[23]:
2
In [24]:
# Operations for square matrices:
using LinearAlgebra

mA = [1 2 3; 4 1 6; 7 8 1]

inv(mA) # inverse

det(mA) # determinant

tr(mA) # trace

eigvals(mA) # eigenvalues
eigvecs(mA) # eigenvectors (in each column)
eigen(mA) # both
Out[24]:
Eigen{Float64, Float64, Matrix{Float64}, Vector{Float64}}
values:
3-element Vector{Float64}:
 -6.214612641961068
 -1.5540265964847833
 10.768639238445843
vectors:
3×3 Matrix{Float64}:
 -0.175709  -0.766257  -0.344989
 -0.570057   0.587185  -0.589753
  0.802596   0.26089   -0.730188
In [25]:
# Solving systems of linear equations:

mA = [1 2 3; 4 1 6; 7 8 1]
vb = [1 2 2]'
mA\vb # solution to A*x = b, i.e. gives A^(-1) b
Out[25]:
3×1 Matrix{Float64}:
 0.14423076923076922
 0.09615384615384615
 0.22115384615384615

Functions Applied to Matrices¶

In [26]:
# Matrices to Vectors & Reverse:

mA = [1 2 3; 4 1 6; 7 8 1]

vec(mA) # vectorize by column

# other direction: turn vector into matrix:
vx = collect(1:12)
reshape(vx,4,3) # 4x3 matrix
Out[26]:
4×3 Matrix{Int64}:
 1  5   9
 2  6  10
 3  7  11
 4  8  12
In [27]:
# Many functions can be applied to matrix-arguments:

maximum(mA) # largest element
minimum(mA) # smallest element

# function "replace"; e.g. replace NaN-elements in mA by zero:
mA = [1 2; 3 NaN]
replace!(mA, NaN => 0) # or:
mA[isnan.(mA)] .= 0
Out[27]:
0-element view(::Vector{Float64}, Int64[]) with eltype Float64
In [28]:
diag(mA) # diagonal of matrix mA

tril(mA) # lower triangular matrix
triu(mA) # upper triangular matrix
Out[28]:
2×2 Matrix{Float64}:
 1.0  2.0
 0.0  0.0
In [29]:
using Distributions
cov(mA) # variance-covariance matrix of columns of mA
cor(mA) # correlation matrix
Out[29]:
2×2 Matrix{Float64}:
  1.0  -1.0
 -1.0   1.0
In [30]:
# We saw how a function with scalar-arguments 
# can be applied to each element of a vector 
# by appending the function with a dot.
# Applying functions to each element of a matrix works analogously:

mA = [1 2; 3 4]
sqrt.(mA)

# The functions "map", "reduce", "mapreduce" and "replace" also work analogously
# (see section on vectors).
Out[30]:
2×2 Matrix{Float64}:
 1.0      1.41421
 1.73205  2.0
In [31]:
# Many functions designed for vector-arguments 
# can be applied to columns or rows of a matrix
# simply by specifying the option "dims":

sum(mA,dims=1) # sum across rows (first dimension) (for each column)
sum(mA,dims=2) # sum across columns (second dimension) (for each row)

# Similarly:
cumsum(mA,dims=1)
maximum(mA,dims=1)
minimum(mA,dims=1)

using Distributions
mean(mA, dims=1) 
var(mA,dims=1) 
std(mA,dims=2)
Out[31]:
2×1 Matrix{Float64}:
 0.7071067811865476
 0.7071067811865476
In [32]:
# If this option is not available, functions for vector-arguments
# can be applied to columns/rows of a matrix using "map": e.g.

map(jj->sum(mA[:,jj]),1:size(mA)[2])
Out[32]:
2-element Vector{Int64}:
 4
 6
In [33]:
# Recall the discussion on "passing by reference" 
# (the differences between " a = b ", " a = copy(b) " and " a = deepcopy(b) ").
# They apply not only to vectors (and vectors of vectors/matrices), 
# but also matrices (and matrices of vectors/matrices).

Arrays¶

In [34]:
# Vectors store objects along one dimension.
# Matrices store objects along two dimensions.
# We can store objects along more than two dimensions with arrays.
In [35]:
# Their creation is analogous to that of matrices: e.g.

aD = zeros(3,2,4) # 3 x 2 x 4 array of zeros
trues(3,2,4) # of trues
repeat([2],3,2,4) # of twos; equivalently:
fill(2, 3,2,4)
repeat(["rum"],3,2,4) # of strings

# We can also create an array by repeating vector or matrix,
# we can create/fill-in an array by comprehension,
# and we can create arrays of vectors/matrices/arrays.
Out[35]:
3×2×4 Array{String, 3}:
[:, :, 1] =
 "rum"  "rum"
 "rum"  "rum"
 "rum"  "rum"

[:, :, 2] =
 "rum"  "rum"
 "rum"  "rum"
 "rum"  "rum"

[:, :, 3] =
 "rum"  "rum"
 "rum"  "rum"
 "rum"  "rum"

[:, :, 4] =
 "rum"  "rum"
 "rum"  "rum"
 "rum"  "rum"
In [36]:
# Show dimension of array aD:
size(aD)
Out[36]:
(3, 2, 4)
In [37]:
# Arrays are indexed analogously to matrices.

# Also, functions are applied to arrays analogously as to matrices.
# Though standard algebraic operations are defined only for
# scalars, vectors and matrices,
#  we can apply them to vectors and matrices indexed from arrays.

Try Exercises 5.b.i - 5.b.iv