Exploring Matrices in Python

If you need to carry out any serious work with matrices in Python then your best option will usually be to use the NumPy package. NumPy provides an industrial strength array class called ndarray (n-dimensional array) which can be used to represent a matrix, and the usual arithmetic operators can be used. Additionally, you can use the @ operator to multiply two matrices together.

However, if you are trying to learn matrix arithmetic then NumPy isn't going to help much as matrix operations aren't very intuitive, especially matrix multiplication which is rather convoluted. The best way to learn is probably to start with a sheet of paper and a pen, working through some examples, before moving on to writing code from scratch to perform the various arithmetic functions.

In this post I will write a very quick whistlestop tour of matrix arithmetic using NumPy before moving on to the main purpose of this post: writing a simple matrix class from scratch.

Matrix Arithmetic

Before getting into writing code I'll give a brief overview of the various arithmetic operations that can be applied to matrices.

Matrix Addition and Subtraction

To add or subtract two matrices both must have the same shape (ie. have the same number of rows and columns) and the resulting matrix will also be of the same shape. Each value is simply the corresponding value in the first matrix plus or minus the corresponding value in the second.

Scalar Addition and Subtraction

Any matrix can have a scalar (single value) added or subtracted. The result is the same shape and with the scalar added to or subtracted from each value in the original matrix.

Scalar Multiplication

Scalar multiplication works in the same way as addition and subtraction, with each value in the result being the corresponding value in the original matrix multiplied by the scalar.

Matrix Multiplication

This is where things get tricky. To be able to multiply two matrices together the second must have the same number of rows as the first has columns. The resulting matrix will have the same number of rows as the first, and the same number of columns as the second.

A textual description of matrix multiplication is going to be wordy and confusing so let's go with a diagram instead:

Even that might be a bit confusing so let's split it into four stages, each highlighted in turn.

  • The value in row 0*, column 0 of the result is the dot product (sum of the products of corresponding values) of row 0 of the first matrix and column 0 of the second.

  • The value in row 1, column 0 of the result is the dot product of row 1 of the first matrix and column 0 of the second.

  • The value in row 0, column 1 of the result is the dot product of row 0 of the first matrix and column 1 of the second.

  • The value in row 1, column 1 of the result is the dot product of row 1 of the first matrix and column 1 of the second.

* Matrix row and column indexes have traditionally started at 1 but when implemented in software we'll go with zero-based indexes.

The pattern is clear - for a given row/column in the result calculate the dot product of the same row in the first matrix and the same column in the second.

The Project

This project consists of three Python source code files which you can download as a zip file from the Downloads page, or clone/download from Github if you prefer.

  • numpymatrices.py
  • matrix.py
  • main.py

Source Code Links

ZIP File
GitHub

Let's start with a very brief introduction to using matrices with NumPy.

numpymatrices.py

import numpy


def main():

    print("------------------")
    print("| codedrome.com  |")
    print("| NumPy Matrices |")
    print("------------------\n")

    m1 = numpy.array([[1,2,3],[4,5,6]])
    m2 = numpy.array([[7,10],[8,11],[9,12]])
    m3 = numpy.array([[9,8,7],[6,5,4]])

    print("m1\n=========\n{}".format(m1))
    print()
    print("m2\n=========\n{}".format(m2))
    print()
    print("m3\n=========\n{}".format(m3))
    print()

    added = m1 + m3
    print("m1 + m3\n============\n{}".format(added))
    print()

    addedscalar = m1 + 7
    print("m1 + 7\n============\n{}".format(addedscalar))
    print()

    multipliedscalar = m1 * 3
    print("m1 * 3\n============\n{}".format(multipliedscalar))
    print()

    multiplied = m1 @ m2
    print("m1 @ m2\n===========\n{}".format(multiplied))
    print()


main()

Firstly we import numpy (you'll need to install it with pip if you haven't already) and then within the single main function create three matrices: the arguments are lists of lists, the inner lists being the rows of the matrices. These are then printed just to show their contents.

Then we create four more matrices by performing various arithmetic operations on the original three:

  • Matrix addition, adding one matrix to another
  • Scalar addition, adding a single value to a matrix
  • Scalar multiplication, multiplying a matrix by a single number
  • Matrix multiplication, multiplying two matrices

Let's now run this code.

Running numpymatrices.py

python3.7 numpymatrices.py

The output is

Program Output

------------------
| codedrome.com  |
| NumPy Matrices |
------------------

m1
=========
[[1 2 3]
 [4 5 6]]

m2
=========
[[ 7 10]
 [ 8 11]
 [ 9 12]]

m3
=========
[[9 8 7]
 [6 5 4]]

m1 + m3
============
[[10 10 10]
 [10 10 10]]

m1 + 7
============
[[ 8  9 10]
 [11 12 13]]

m1 * 3
============
[[ 3  6  9]
 [12 15 18]]

m1 @ m2
===========
[[ 50  68]
 [122 167]]

Here we see seven matrices: the original three followed by the results of the various arithmetic operations. NumPy is both efficient and easy to use but as I mentioned above it is worthwhile coding an implementation of the basic matrix operations from scratch just to get your head round the topic, so let's do just that.

matrix.py

class Matrix:

    def __init__(self, entries=None, rowcount=0, columncount=0):

        """
        Create a Matrix object either with the supplied values
        or with the specified number of rows and columns of 0 values.
        """

        if entries is not None:

            self.entries = entries
            self.rowcount = len(entries)
            self.columncount = len(entries[0])

        elif rowcount > 0 and columncount > 0:

            self.rowcount = rowcount
            self.columncount = columncount

            self.entries = []
            for row in range(0, self.rowcount):
                self.entries.append([0] * self.columncount)

    def __str__(self):

        """
        Returns the matrix formatted with square brackets
        and rows on separate lines.
        """

        brackets = {"top_left": "\u23A1",
                    "middle_left": "\u23A2",
                    "bottom_left": "\u23A3",
                    "top_right": "\u23A4",
                    "middle_right": "\u23A5",
                    "bottom_right": "\u23A6"}

        string = []

        for index, row in enumerate(self.entries):

            if index == 0:
                string.append(brackets["top_left"])
            elif index == self.rowcount - 1:
                string.append(brackets["bottom_left"])
            else:
                string.append(brackets["middle_left"])

            for column in row:
                string.append("{:4}".format(column))

            if index == 0:
                string.append(brackets["top_right"])
            elif index == self.rowcount - 1:
                string.append(brackets["bottom_right"])
            else:
                string.append(brackets["middle_right"])

            string.append("\n")

        return "".join(string)

    def __add__(self, other):

        """
        Matrix addition returns a matrix the same shape as the two being added
        with each value the sum of the corresponding values.
        """

        if self.addable(other):

            result = Matrix(rowcount=self.rowcount, columncount=self.columncount)

            for row in range(0, self.rowcount):
                for column in range(0, self.columncount):
                    result.entries[row][column] = self.entries[row][column] + other.entries[row][column]

            return result

        else:

            raise ValueError("Matrices must have the same shape to be added")

    def __sub__(self, other):

        """
        Matrix subtraction returns a matrix the same shape as the two being added
        with each value being the first value minus its corresponding second value.
        """

        if self.subtractable(other):

            result = Matrix(rowcount=self.rowcount, columncount=self.columncount)

            for row in range(0, self.rowcount):
                for column in range(0, self.columncount):
                    result.entries[row][column] = self.entries[row][column] - other.entries[row][column]

            return result

        else:

            raise ValueError("Matrices must have the same shape to be subtracted")

    def __mul__(self, scalar):

        """
        Scalar multiplication returns a matrix the same shape as the original,
        each value being multiplied by the scalar.
        """

        result = Matrix(rowcount=self.rowcount, columncount=self.columncount)

        for row in range(0, self.rowcount):
            for column in range(0, self.columncount):
                result.entries[row][column] = self.entries[row][column] * scalar

        return result

    def __matmul__(self, other):

        """
        The result of matrix multiplication is a matrix with the same number
        of rows as the first matrix and the same number of columns as the second.
        Each value is the dot product of the corresponding row and column.
        """

        if self.multipliable(other):

            result = Matrix(rowcount=self.rowcount, columncount=other.columncount)

            for row in range(0, result.rowcount):
                for column in range(result.columncount):
                    result.entries[row][column] = self.__dot_product(other, row, column)

            return result

        else:

            raise ValueError("Matrices not of the right shapes to be multiplied")

    def addable(self, other):

        """
        To be addable matrices must each have the same number of rows and columns.
        """

        return self.rowcount == other.rowcount and self.columncount == other.columncount

    def subtractable(self, other):

        """
        To be subtractable matrices must each have the same number of rows and columns.
        """

        return self.rowcount == other.rowcount and self.columncount == other.columncount

    def multipliable(self, other):

        """
        To be multipliable the number of rows in the second matrix must
        be the same as the number of columns in the first matrix.
        """

        return self.columncount == other.rowcount

    def __dot_product(self, other, self_row, other_column):

        """
        Calculate the sum of the products of the corresponding values
        in the specified row and column.
        """

        dot_product = 0

        for i in range(0, self.columncount):

            dot_product += (self.entries[self_row][i] * other.entries[i][other_column])

        return dot_product

The code above is a simple implementation of "toy" matrix class purely for educational purposes. Don't even think about using it in the real world!

The __init__ method allows us to create a matrix in two ways, either with a list of values(or entries in matrix jargon), or with row and column counts. If you use the former the rowcount and columncount properties are set from the supplied list, and if you use the latter you'll get an array with all entries initialised to 0.

The __str__ has only one point of (minor) interest - it uses a few Unicode character values to print out segments of square brackets, as you'll see in a moment.

Next we have a few special methods (aka "magic" or "dunder" methods). Python maps these to the appropriate operator symbol, and the ones I'll use are:

  • __add__ maps to + (matrix addition)
  • __sub__ maps to - (matrix subtraction)
  • __mul__ maps to * (scalar multiplication)
  • __matmul__ maps to @ (matrix multiplication)

I haven't implemented scalar addition or subtraction but you could do this if you wish. You'll need to check the type of the other argument, and if it is a numeric value carry out scalar operations.

__add__ & __sub__

Firstly we need to check the two matrices are addable/subtractable. If so we create a new matrix of the same shape before using nested loops to calculate the entries in the result.

__mul__

This is for scalar arithmetic so we create a new matrix the same shape as the original, and then calculate the values in nested loops.

__matmul__

Again we use the same approach: create a matrix for the result and then populate it within nested loops. The extra complexity in matrix multiplication is farmed out to the __dot_product function which I'll describe in a moment.

addable, subtractable and multipliable

These methods simply check whether the various operations can be carried out between the two specific matrices.

__dot_product

This method takes two matrices (if you include self), and the indexes of the first's row and the second's column. It then calculates the dot product of these by initialising the result to 0 and then adding the products of the corresponding entries within a loop.

That's the Matrix class completed so we now need to try it out.

main.py

import matrix


def main():

    print("----------------------")
    print("| codedrome.com      |")
    print("| Exploring Matrices |")
    print("----------------------\n")

    addition()

    subtraction()

    # scalar_multiplication()

    # matrix_multiplication()


def addition():

    print("Addition\n--------")

    m1 = matrix.Matrix(entries=[[1,2,3],[4,5,6]])
    m2 = matrix.Matrix(entries=[[2,3,4],[3,4,5]])
    m3 = matrix.Matrix(entries=[[2,3],[3,4,5]])

    print("m1 and m2 addable? {}".format(m1.addable(m2)))
    print("m1 and m3 addable? {}\n".format(m1.addable(m3)))

    m1_plus_m2 = m1 + m2

    print(m1)
    print(m2)
    print(m1_plus_m2)


def subtraction():

    print("Subtraction\n-----------")

    m4 = matrix.Matrix(entries=[[9,8,7],[8,7,6]])
    m5 = matrix.Matrix(entries=[[1,2,3],[3,4,5]])
    m6 = matrix.Matrix(entries=[[8,7],[6,5,4]])

    print("m4 and m5 subtractable? {}".format(m4.subtractable(m5)))
    print("m4 and m6 subtractable? {}".format(m4.subtractable(m6)))

    m4_minus_m5 = m4 - m5

    print(m4)
    print(m5)
    print(m4_minus_m5)


def scalar_multiplication():

    print("Scalar multiplication\n---------------------")

    m7 = matrix.Matrix(entries=[[2,4,6],[8,10,12]])

    m8 = m7 * 3

    print(m7)
    print(m8)


def matrix_multiplication():

    print("Matrix multiplication\n---------------------")

    m9 = matrix.Matrix(entries=[[1,2,3],[4,5,6]])
    m10 = matrix.Matrix(entries=[[7,10],[8,11],[9,12]])

    print("m9 and m10 multipliable? {}".format(m9.multipliable(m10)))

    print(m9)
    print(m10)

    m9_x_m10 = m9 @ m10

    print(m9_x_m10)


main()

The main.py file contains four functions to create Matrix objects, call the various methods and print the results. The calls to the first two in main are uncommented so let's run them.

Running main.py

python3.7 main.py

The output is

Program Output - addition and subtraction

----------------------
| codedrome.com      |
| Exploring Matrices |
----------------------

Addition
--------
m1 and m2 addable? True
m1 and m3 addable? False

⎡   1   2   3⎤
⎣   4   5   6⎦

⎡   2   3   4⎤
⎣   3   4   5⎦

⎡   3   5   7⎤
⎣   7   9  11⎦

Subtraction
-----------
m4 and m5 subtractable? True
m4 and m6 subtractable? False
⎡   9   8   7⎤
⎣   8   7   6⎦

⎡   1   2   3⎤
⎣   3   4   5⎦

⎡   8   6   4⎤
⎣   5   3   1⎦

In each case two of the matrices are addable or subtractable, and the others are not. We then see the original matrices and the results of the addition and subtraction. Here you get to see the various Unicode square bracket symbols.

Comment out the first two function calls in main and, uncomment scalar_multiplication() and run the program again.

Program Output - scalar multiplication

----------------------
| codedrome.com      |
| Exploring Matrices |
----------------------

Scalar multiplication
---------------------
⎡   2   4   6⎤
⎣   8  10  12⎦

⎡   6  12  18⎤
⎣  24  30  36⎦

As you can see each of the values has been multiplied by the scalar 3.

Now uncomment matrix_multiplication() in main and run again.

Program Output - matrix multiplication

----------------------
| codedrome.com      |
| Exploring Matrices |
----------------------

Matrix multiplication
---------------------
m9 and m10 multipliable? True
⎡   1   2   3⎤
⎣   4   5   6⎦

⎡   7  10⎤
⎢   8  11⎥
⎣   9  12⎦

⎡  50  68⎤
⎣ 122 167⎦

The matrices being multiplied are those show in the example near the top of the page, so you can follow the multiplication process through if you wish.

Thinking of Starting Your Own Blog?

If you are thinking of starting your own blog or looking to migrate to a better host check out BlueHost. Free domain name for 1 year, free SSL certificate and 1-Click WordPress Install.