Working with Matrices

Overview:

  • Teaching: 15 mins
  • Exercises: 5 mins

Objectives:

  • Learn how to load and save variables
  • Techniques for isolating parts of matrices
  • Preallocation of memory for arrays

Different Syntaxes

MATLAB has had many version updates over the years (you should be able to check which version you are running as they are all named after the year of their release), and with different versions comes the inevitable change of syntaxes for commands. This is compounded by the fact that we have to write these notebooks in Octave syntax, which is usually identical to the latest MATLAB release but does deviate in places.

One place where MATLAB versions and Octave differ from each other is the use of the load and save commands you are about to see below. If the commands do not run correctly as you see them in this lesson, type help load or help save into the MATLAB command line to bring up the help for these commands, which will inform you of the correct syntax to use.

If you are still stuck (don't worry, MATLAB's "help" is very hard to interpret in places) just ask a demonstrator.

Loading and Saving Variables

MATLAB's GUI allows us to save variables in the workspace to .mat files to access later, and to load said files back into the workspace. These features can be used to save on computation time and/or cost, if the information stored in the variables is time consuming to produce.

As we might expect, we can save any variable in the workspace to a variable file using the save command. This command works differently to other function calls that you've seen so far. To use save, we type save <filename> v1 v2 ... where:

  • <filename> is the name of the file we want to save the variables to, input as a string!
  • v1 v2 ... is a list of all the variables in the workspace that we want to include in the saved file.

So for example...

In [1]:
mu0 = 4*pi*1e-7;
useful_vector = [1; 4; 9; 16; 25];
fun_matrix = magic(5); %the magic command creates a magic square and stores it in a matrix.

save "exampleSave" mu0 useful_vector fun_matrix

You should notice that, after executing the save command, a new file appears in the current directory panel of the GUI.

If we were to then start a new MATLAB session and want these variables back, we can then load them from this file. The load command has similar syntax to save, except we don't need to provide it with any variable names, only the filename (as a string).

In [2]:
clear; %remove the variables from the workspace to illustrate that we can re-load them!

load "exampleSave"

fprintf('To prove that the values really have reloaded after a clear:\n')
disp(mu0)
disp(useful_vector)
disp(fun_matrix)
To prove that the values really have reloaded after a clear:
 0.0000012566
    1
    4
    9
   16
   25
   17   24    1    8   15
   23    5    7   14   16
    4    6   13   20   22
   10   12   19   21    3
   11   18   25    2    9

Notice how the variables reappear in the current workspace with the same variable names as when we saved them. This leads to a warning: the load command will overwrite the value of variables that are already stored in the workspace if they share the same name!

Load/save via the GUI

MATLAB's GUI enables you to interact with the variables in your workspace and files in the current directory using the mouse and keyboard. As such, you can load and save using the GUI by highlighting the variables you want to save (or files you want to load) and right-clicking, then selecting the relevant option from the drop-down menu.

The load and save commands are much more useful when it comes to writing scripts though, so you should get into the hang of using them.

What's in an array?

Before we look at advanced techniques for isolating parts of arrays, we should first explore how we can access the individual entries in an array. The entries of an array can be accessed using indices to specify the position in the array of the value we want to extract. We need one index per dimension of the array, and indices range from 1 to the maximum length in that dimension of the array.

In [3]:
A = rand(3,5); %3-by-5 matrix with random entries
disp(A)

fprintf('The element at position (1,1) of the array is %.5f \n', A(1,1))
   0.963707   0.275114   0.010143   0.231564   0.226714
   0.173068   0.511327   0.663224   0.234112   0.457418
   0.200392   0.151542   0.795549   0.081569   0.598446
The element at position (1,1) of the array is 0.96371 

The syntax for accessing one entry in an array is to use the variable name followed by braces ( ) containing the indices. In this case, our array A is 2-dimensional (being a $3\times5$ array/matrix) and so we must use two indices to specify the value we want to find. Vectors are 1-dimensonal arrays, so we only need to use a single index to specify which entry we want to look at:

In [4]:
v = rand(5,1); %column vector of length 5
disp(v)

fprintf('The element at position (3) of the vector is %.5f \n', v(3))
   0.22528
   0.13074
   0.66423
   0.27137
   0.69646
The element at position (3) of the vector is 0.66423 

Now that we can access parts of an array, we can also assign them values and change the entries in the array directly. This is done by isolating an element of an array, then using the = operation to assign it a new value.

In [5]:
A = rand(3,5);
some_number_I_picked_for_an_example = 1e-1;
disp(A)

%change the (2,3) element of A to be -1
A(2,3) = -1;
%change the (3,5) element of A to be the value stored in the variable "some_number_I_picked_for_an_example"
A(3,5) = some_number_I_picked_for_an_example;

fprintf('A now looks like:\n')
disp(A)
   0.37684   0.66633   0.99741   0.23419   0.79856
   0.81510   0.86648   0.84529   0.40810   0.25220
   0.38280   0.78711   0.16511   0.11030   0.79739
A now looks like:
   0.37684   0.66633   0.99741   0.23419   0.79856
   0.81510   0.86648  -1.00000   0.40810   0.25220
   0.38280   0.78711   0.16511   0.11030   0.10000

Isolating Parts of Arrays

In the previous lesson we dealt with defining arrays and performing operations on them. Here we look at more advanced techniques for working with arrays; in particular how we can isolate parts of an array and the in-built functions available to us when performing calculations with them.

In-built isolation functions

There are some functions in MATLAB that allow us to extract parts of matrices quickly. diag is one of these functions (yes, the same diag as in the previous lesson); when we give it a matrix argument it returns a vector that contains the diagonal entries of the matrix. There are also the functions triu and tril which extract the upper and lower triangular parts of a matrix (respectively).

Isolation via "Slicing"

Of course, we may not only be interested in the diagonal or triangular parts of an array, but instead might be looking for a "sub-block" of a matrix or some important components of a vector. In these cases, we can use a technique called index slicing to aid us. This technique allows us to extract multiple entries at once, and also allows us to assign them at once too!

To "slice" an array, we use a colon (:) when we try to access the values stored in an array.

  • If we want all the entries along one dimension of the array, we just put a colon where we would normally put the index.
  • If we want a subset of the entries along one dimension of the array; then either side of a colon we type the start and end indices of the subset we want to extract.
In [6]:
A = rand(5,5);
disp(A)

fprintf('Extract the first row of A:\n')
A_first_row = A(1,:); %(1,:) means "take row 1, and all the columns"
disp(A_first_row)

fprintf('Extract the 3rd column of A:\n')
A_third_col = A(:,3); %(:,3) means "take all the rows, and column 3"
disp(A_third_col)

fprintf('Extract the 3-by-3 submatrix of A formed by the centre 9 entries\n')
A_centre_block = A(2:4,2:4); %(2:4,2:4) means "take rows 2-4, and columns 2-4"
disp(A_centre_block)
   0.04865632   0.75185393   0.94361009   0.00054386   0.77646286
   0.35478160   0.53371004   0.15519662   0.73459041   0.10342921
   0.78194677   0.42711720   0.94747232   0.17820877   0.89772608
   0.28874029   0.83627165   0.45426995   0.88602619   0.82219367
   0.84870334   0.72218514   0.59486890   0.19154256   0.16308917
Extract the first row of A:
   0.04865632   0.75185393   0.94361009   0.00054386   0.77646286
Extract the 3rd column of A:
   0.94361
   0.15520
   0.94747
   0.45427
   0.59487
Extract the 3-by-3 submatrix of A formed by the centre 9 entries
   0.53371   0.15520   0.73459
   0.42712   0.94747   0.17821
   0.83627   0.45427   0.88603

We can assign values to subsets of arrays by using =. You must assign an array of values of the same shape/size as the subset you are extracting, or provide a single scalar value. If you provide the latter, every value you extract will be set to that scalar value. Otherwise, you will simply insert the new array into the subset that you extracted.

In [7]:
fprintf('Set the centre 3-by-3 block of A to be 1 \n')
A(2:4,2:4) = ones(3,3); %"take rows 2-4 and columns 2-4 and set them to be a 3-by-3 matrix of ones"
disp(A)

fprintf('Set every value in the 2nd column of A to be 4.5 \n')
A(:,2) = 4.5; %"take all the rows and the second column and set them to be 4.5"
disp(A)
Set the centre 3-by-3 block of A to be 1 
   0.04865632   0.75185393   0.94361009   0.00054386   0.77646286
   0.35478160   1.00000000   1.00000000   1.00000000   0.10342921
   0.78194677   1.00000000   1.00000000   1.00000000   0.89772608
   0.28874029   1.00000000   1.00000000   1.00000000   0.82219367
   0.84870334   0.72218514   0.59486890   0.19154256   0.16308917
Set every value in the 2nd column of A to be 4.5 
   0.04865632   4.50000000   0.94361009   0.00054386   0.77646286
   0.35478160   4.50000000   1.00000000   1.00000000   0.10342921
   0.78194677   4.50000000   1.00000000   1.00000000   0.89772608
   0.28874029   4.50000000   1.00000000   1.00000000   0.82219367
   0.84870334   4.50000000   0.59486890   0.19154256   0.16308917

"But wait!", I hear you cry,
"What if I want to extract the final entry of an array, but don't know the size of the array?"

For this you can use the keyword end when slicing. MATLAB interprets this as "the value of the last index in this dimension of the array" when you try to extract values this way.

In [8]:
fprintf('Extract the final entry of the 3rd column of A: \n')
disp(A(end,3))

fprintf('Extract everything up to the penultimate value of row 4 of A: \n')
disp(A(4,1:end-1))
Extract the final entry of the 3rd column of A: 
 0.59487
Extract everything up to the penultimate value of row 4 of A: 
   0.28874   4.50000   1.00000   1.00000

Index Error

Notice that if you try to access part of an array that doesn't exist, for example index 5 of a vector of length 4, or index 0 of anything, you will get an "(array index) out of bounds" error:

In [9]:
v = ones(1,6); %length 6 row vector
v(7)
error: v(7): out of bound 6

What is : doing?

You might notice that if you type 1:5 into the command line and press execute, it returns a vector:

In [10]:
1:5
ans =
   1   2   3   4   5

Indeed, : is itself a matrix construction function! It has the general syntax of start:step:end, where:

  • start is the first value we want to start at,
  • step is the incriment we apply to each value,
  • end is the value that should not be exceeded.

If we only include one colon, then step defaults to 1 and we just have start:end.

With the information above in mind, you can quickly realise that MATLAB will slice when you put vectors where you would normally place indices when extracting parts of an array. For example, we could extract all the even-index-elements of a vector:

In [11]:
v = rand(1,10); %random row vector of length 10
disp(v)
even_indexes = 2:2:length(v);

fprintf('Even-indexed values of v: \n')
disp(v(even_indexes))
 Columns 1 through 7:

   0.039308   0.312711   0.575496   0.881480   0.279258   0.691240   0.397761

 Columns 8 through 10:

   0.989777   0.199859   0.677365
Even-indexed values of v: 
   0.31271   0.88148   0.69124   0.98978   0.67736

Or we could pick out a selection of points from a matrix that we so happened to want:

In [12]:
A = rand(5,5);
disp(A)

row_inds = [2 4 5];
col_inds = [1 4];

fprintf('Our selection of indices gives us the values: \n')
disp(A(row_inds, col_inds))
   0.386832   0.069482   0.474598   0.023672   0.865176
   0.179804   0.513471   0.237184   0.218649   0.676577
   0.607962   0.668264   0.736099   0.787649   0.375383
   0.135531   0.016820   0.124934   0.379952   0.135691
   0.085000   0.216957   0.229473   0.616758   0.857327
Our selection of indices gives us the values: 
   0.179804   0.218649
   0.135531   0.379952
   0.085000   0.616758

Notice how even though our slices don't correspond to a continuous (or connected) subsection of our array, the extracted values still come out as a single "block".

Preallocation of arrays

What do you expect to happen if the following code is run?

In [13]:
vec = ones(1,7);
vec(8) = 4;

You would be forgiven for thinking that we would get an error - after all, index 8 doesn't exist in a length 7 vector. However this is an example of a MATLAB quirk; if you assign a value to an index (or range of indices via slicing) that don't exist in an array, MATLAB will automatically extend the array and then assign the values in the extended array to those that you specified.

Initially this may seem like a really useful feature... but it is not. Every time you use this feature to add a value to an array, MATLAB has to create a temporary array of the new size, copy the values from your old array into it, then assign the new values, then delete the old array and reassign the new array to the variable name you chose! This can lead to increased computation time, and is sometimes called the "growing array problem". At small array sizes, this is not a major issue. But for matrices $\approx 10\times10$ in size, it starts to become a major issue.

We can circumnavigate this issue by preallocating memory for our arrays. What this means is that you should always initalise an array of the correct size before using slicing to assign values to it's elements. Typically one can use the zeros function to do this, but ones or eye may be a better choice if the matrix you want to construct looks similar to them.

Below are some examples of "bad" and "good" creation of a 4-by-4 matrix:

In [14]:
clear A
%bad - assign each element without preallocation
tic;
A(1,1) = 1; A(1,2) = 1; A(3,1) = 1; A(1,4) = 1;
A(2,1) = 1; A(2,2) = 1; A(2,3) = 1; A(2,4) = 1;
A(3,1) = 1; A(3,2) = 1; A(3,3) = 1; A(3,4) = 1;
A(4,1) = 1; A(4,2) = 1; A(4,3) = 1; A(4,4) = 1;
t = toc;
fprintf('Bad method took %.5f seconds \n', t)
%good - assign each element with preallocation
clear A
tic;
A = zeros(4,4);
A(1,1) = 1; A(1,2) = 1; A(3,1) = 1; A(1,4) = 1;
A(2,1) = 1; A(2,2) = 1; A(2,3) = 1; A(2,4) = 1;
A(3,1) = 1; A(3,2) = 1; A(3,3) = 1; A(3,4) = 1;
A(4,1) = 1; A(4,2) = 1; A(4,3) = 1; A(4,4) = 1;
t = toc;
fprintf('Good method took %.5f seconds \n', t)
Bad method took 6.05058 seconds 
Good method took 6.05378 seconds 

Slicing

Use load to read in the data provided in duck_population.mat, which you can download here. This file contains one variable duck_populations which is a $3\times 7$ array of integer values. The columns of the array correspond to the populations of individual species of duck, whilst the rows of the array correspond to the population of males, females, and ducklings ($<1$ year) across all species. The array can be visualised as a table:

Species 1 Species 2 Species 3 Species 4 Species 5 Species 6 Species 7
# Males 38 33 39 46 39 43 17
# Females 38 26 35 30 8 35 28
# Ducklings 12 4 10 9 14 2 22

The $3\times 7$ array duck_population contains the numerical values of the table, in this arrangement.

For the following tasks, the MATLAB functions sum, mean, std, max and min may be useful: use the help feature to explore how they work.

  1. A researcher cross-checking the data informs you that there were multiple errors upon data entry, and asks you to rectify them by creating a new array pop_corrected. Create the new array, and try to make the changes in as few commands as possible. The changes to be made are:
    • Species 5's female population has been recorded as 8, where it should be 28.
    • A number of species 6's male ducks are actually under 1 year old, and so qualify as ducklings. Reduce the number of species 6 males by 17, and increase the number of ducklings accordingly.
    • Species 1's population recordings are actually from the survey conducted last year, rather than this year's recent data. The readings should be 45 males, 43 females, and 3 ducklings.
    • Species 2 and 3 have each other's data; correct this!
  2. Save the new array to a file with the same name, pop_corrected.
  3. Find the average and standard deviation of males, females, and ducklings - displaying them to the screen in pairs to 2 decimal places.
  4. For each of males, females and ducklings, find the species with the greatest and least population recorded.

Solution

Help with help

At first glance, MATLAB's help screen can be a bit of an information overload, however it's all in how you read the information that is provided.

Let's look at std as an example:

In [15]:
help std
perl: warning: Setting locale failed.
perl: warning: Please check that your locale settings:
    LANGUAGE = "en_GB",
    LC_ALL = (unset),
    LC_TIME = "C",
    LC_NUMERIC = "C",
    LANG = "en_GB.UTF-8"
    are supported and installed on your system.
perl: warning: Falling back to the standard locale ("C").
'std' is a function from the file /snap/octave/current/usr/share/octave/5.1.0/m/statistics/std.m
 -- std (X)
 -- std (X, OPT)
 -- std (X, OPT, DIM)
     Compute the standard deviation of the elements of the vector X.
     The standard deviation is defined as
          std (X) = sqrt ( 1/(N-1) SUM_i (X(i) - mean(X))^2 )
     where N is the number of elements of the X vector.
     If X is a matrix, compute the standard deviation for each column
     and return them in a row vector.
     The argument OPT determines the type of normalization to use.
     Valid values are
     0:
          normalize with N-1, provides the square root of the best
          unbiased estimator of the variance [default]
     1:
          normalize with N, this provides the square root of the second
          moment around the mean
     If the optional argument DIM is given, operate along this
     dimension.
     See also: var, bounds, mad, range, iqr, mean, median.
Additional help for built-in functions and operators is
available in the online version of the manual.  Use the command
'doc <topic>' to search the manual index.
Help and information about Octave is also available on the WWW
at https://www.octave.org and via the help@octave.org
mailing list.

If you are using Octave, you may get a warning about your location settings - this is fine, ignore this. In addition, the names of the variables may differ from those below depending on your Octave/MATLAB version, but will still have the same roles. In particular, MATLAB's help changes format between versions, so you may get different printouts when you switch versions. All the information referred to below will still be there, just potentially in a different layout.

You should begin reading the help from where you see the list of std printed, with various arguments in brackets after them. In this case, we have 3 of them:

-- std(X)
-- std(X, OPT)
-- std(X, OPT, DIM)

These are the 3 ways in which we can use the function std. As the variable X appears in every function call, we always need to provide a value of X to std. The variables OPT and DIM however, are optional arguments.

Below this list of possible ways to use the function, we get a small description of what std does, depending on the type of variable that X is. We see that "if X is a matrix, compute the standard deviation for each column and return them as a row vector" is what std will do by default for a matrix X. For a vector X, it will just compute the standard deviation of the entries.

Under this initial description is some information about the optional arguments, OPT and DIM. OPT lets us choose how we want the standard deviation calculated, telling us we can either provide the value 0 or for OPT. Providing 0 will give us the square root of the best unbiased estimator of the varience, and is the default option; providing 1 will give the square root of the second moment about the mean. The optional argument DIM can be used to change the dimension along which we take standard deviations - if the array X we put in is 2-dimensional we have two choices: dimension 1 (columns) or dimension 2 (rows). The default option is along the columns, so DIM=1.
NOTE: This may seem counter-intuitive; seeing as MATLAB's arrays are index by (row, column). However "operating along the rows" has the interpretation of "finding data by moving from row 1 to row 2 to row 3 to ... to row end" rather than "fix the row number, and look across the columns for data".

The default values for the optional arguments mean that executing the command

std(X)

is the same as executing

std(X, 0, 2)

If instead we wanted to find the standard deviation along the rows of the array X, we would use the command

std(X, 0, 1)

or if we wanted the standard deviation of the columns, but the second moment about the mean, we would use

std(X, 1)

NOTE: There's no need to include the optional arguments that come after the final optional argument you want to change from it's default.

Key Points:

  • We can load and save variables to and from the workspace.
  • We can use in built functions to isolate parts of a matrix, or use index slicing.
  • Detailed information on functions can be accessed using the help feature.
  • Many of MATLAB's functions work column-wise on matrices, and can have their default options changed to suit our needs.