Working definition (not final): a two dimensional array of scalars with one or more columns and one or more rows.
ex. (147258369) or (478) or (123) are 3x3, 1x3 and 3x1 matrices respectively. This is of the form rows x columns.
Generally, matrices are denoted as:
(𝑎𝑖𝑗)𝑚×𝑛 where 𝑖=1,2,,𝑚 and 𝑗=1,2,,𝑛. Indexing can also start with zero. In this form, the matrix is:
(𝑎11𝑎21𝑎31𝑎12𝑎22𝑎32𝑎13𝑎23𝑎33)

Diagonal matrix and Triangular matrix

For a diagonal 𝑖=𝑗. In a diagonal matrix the non diagonal elements are zero.
ex. (200010003)
the zero matrix (a matrix with only zeroes) is also a diagonal matrix.

A triangular matrix is a square matrix (same number of rows and columns) where elements above or below the main diagonal are all zeroes.

  • if the elements above the main diagonal are zeroes, it’s a lower triangular matrix. ex. (435020003)
  • if the elements below the main diagonal are zeroes, it’s an upper triangular matrix. ex. (500820783)

Identity matrix

A diagonal matrix whose all diagonal entries are 1.
ex.

𝐼2=(1001)𝐼3=(100010001)

Matrix algebra

Two matrices are said to be equal if their dimensions (rows x columns) are equal and all corresponding elements are equal.
ex.

𝐴=(1212)𝐵=(1212)𝑎𝑖𝑗=𝑏𝑖𝑗𝑖𝑗

Thus the two are equal.

Addition and subtraction

Addition or subtraction is only possible when two matrices have the same size or dimensions. Both operations yield a matrix of the same size as the operand matrices. Matrices of different sizes cannot be added or subtracted.
𝑐𝑖𝑗=𝑎𝑖𝑗+𝑏𝑖𝑗

𝐴+𝐵=𝐵+𝐴 and 𝐴+(𝐵+𝐶)=(𝐴+𝐵)+𝐶=𝐴+𝐵+𝐶

Scalar multiplication

Matrices can be multiplied by a scalar (a constant). The constant just gets multiplied to each element of the matrix individually, so:

𝐴=(𝑎11𝑎21𝑎𝑚1𝑎12𝑎22𝑎𝑚2𝑎1𝑛𝑎2𝑛𝑎𝑚𝑛)𝑚×𝑛𝛼𝐴=(𝛼𝑎11𝛼𝑎21𝛼𝑎𝑚1𝛼𝑎12𝛼𝑎22𝛼𝑎𝑚2𝛼𝑎1𝑛𝛼𝑎2𝑛𝛼𝑎𝑚𝑛)𝑚×𝑛

Matrix multiplication

Product of two matrices is another matrix. The necessary condition for matrix multiplication is that the number of columns in the first matrix must be equal to the number of rows of the second matrix. That is, if two matrices of dimensions m x n and p x q are being multiplied, it must be true that n = p, and the resultant matrix would have dimensions m x q.
ex.

𝐴=(𝑎11𝑎21𝑎12𝑎22𝑎13𝑎23)𝐵=(𝑏11𝑏21𝑏31𝑏12𝑏22𝑏32)𝐶=𝐴𝐵=(𝑐11𝑐21𝑐12𝑐22)𝑐11=(𝑎11,𝑎12,𝑎13)(𝑏11𝑏12𝑏13)=𝑎11𝑏11+𝑎12𝑏21+𝑎13𝑏31

generally, if we start the indexing from 1 and 𝑗 is the number of columns in the first matrix, or the number of rows in the second matrix:

𝑐𝑖𝑗=𝑗𝑘=1𝑎𝑖𝑘𝑏𝑘𝑗
if you’re doing this by hand (why?) I have found this to be a nice aid:

𝑐𝑖𝑗=(𝑖throw of A)(𝑗thcolumn of B)

which is just a dot product of vectors.

Some rules

  1. 𝐴𝐼=𝐼𝐴=𝐴
  2. 𝐴(𝐵𝐶)=(𝐴𝐵)𝐶=𝐴𝐵𝐶
  3. 𝐴(𝐵+𝐶)=𝐴𝐵+𝐴𝐶
  4. (𝐴+𝐵)𝐶=𝐴𝐶+𝐵𝐶
    some common pitfalls:
  • 𝐴𝐵 is not generally equal to 𝐵𝐴 as 𝐵𝐴 may not be conformable (what is conformability)?
  • If 𝐴𝐵=0 neither A nor B may necessarily be equal to 0 (zero matrix).
  • If 𝐴𝐵=𝐴𝐶, 𝐵 is not necessarily equal to 𝐶.

Transpose of a matrix

If a matrix is given as:

𝐴={𝑎𝑖𝑗}𝑚×𝑛:𝑖=1,2,,𝑚;𝑗=1,2,,𝑛𝐴𝑇={𝑎𝑗𝑖}𝑛×𝑚:𝑖=1,2,,𝑛;𝑗=1,2,,𝑚

Some common rules:

  • (𝐴+𝐵)𝑇=𝐴𝑇+𝐵𝑇
  • (𝐴𝐵)𝑇=𝐵𝑇𝐴𝑇
  • (𝑘𝐴)𝑇=𝑘𝐴𝑇
  • (𝐴𝑇)𝑇=𝐴

Determinant of a matrix; determinant as a volume

Every square matrix has a determinant which is a scalar number. For a simple 2x2 matrix,

𝐴=(𝑎𝑐𝑏𝑑)det(𝐴)=𝑎𝑑𝑏𝑐

If the determinant is non-zero, we say that the matrix is regular and the inverse exists. i.e. the matrix is invertible.
We can consider a determinant as a mapping from a set of n vectors spanning an object in 𝑛. The determinant is the signed volume of an n-dimensional parallelpiped formed by columns of the matrix A.
For n=2 the columns of the matrix form a parallelogram. If two vectors b and g form the columns of the matrix 𝐴=(𝒃,𝒈), the absolute value of the determinant of A is the area of the parallelogram with the vertices 0, b, g, b+g as shown in the figure below. In particular, if b and g are linearly dependent we can’t even have a parallelogram since 𝒃=𝜆𝒈.

Sign: The sign of the determinant signals the orientation of the spanning vectors 𝒃,𝒈 with respect to the standard basis (𝒆1,𝒆2). Flipping the order to g,b instead of b,g swaps the columns of A and reverse the orientation of the shaded area.
The intuition here is that in 2 dimensions, the determinant represents the area spanned by two vectors. This intuition expands to higher dimensions too.
In 3 we can consider three vectors spanning the edge of a parallelpiped, i.e. a solid with faces that are parallel parallelograms. The absolute value of the determinant of a 3x3 matrix is the volume of the solid.
Thus, the determinant acts as a function that measures the signed volume formed by column vectors composed in a matrix.

I asked claude about the sign change since I was confused on what it means, as “direction of the basis vectors” wasn’t that intuitive.
Claude on sign change:
The sign tells you whether the linear transformation preserves or flips orientation — think of it as: does it keep your basis vectors in the same rotational arrangement, or does it mirror them?
Concretely in 2D: take the standard basis e₁=(1,0), e₂=(0,1). Going from e₁ to e₂ is a counterclockwise turn. Now apply a transformation A to both vectors, getting Ae₁ and Ae₂ — these become the new columns of A. Ask: going from Ae₁ to Ae₂, is it still counterclockwise, or has it flipped to clockwise?

  • det(A) > 0: orientation preserved — Ae₁ to Ae₂ is still counterclockwise, same rotational sense as the original basis. Rotations, scaling (by positive factors), shears — all orientation-preserving.
  • det(A) < 0: orientation flipped — Ae₁ to Ae₂ is now clockwise. This happens with reflections (e.g., flipping across an axis) — the “handedness” of the space gets mirrored.

Trace of a matrix

The trace of a square matrix is defined as:
tr(𝐴)𝑛𝑖=1𝑎𝑖𝑖
i.e. it’s the sum of the diagonal elements.

Inverse of a matrix

The inverse of a scalar is the reciprocal or division by 1 by the scalar, ex. inverse of k = 7 is 1/k = 1/7.
In matrices division is not defined because there may be 𝐴𝐵=𝐴𝐶 even when B and C are not equal.
Instead, the inverse of a square matrix is defined as the unique matrix 𝐴1 where:

𝐴𝐴1=𝐴1𝐴=𝐼

The inverse only exists when the matrix is non singular, or the determinant is non-zero.
Some properties:

  • (𝐴𝐵)1=𝐵1𝐴1
  • ((𝐴)1)1=𝐴
  • (𝐴𝑇)1=(𝐴1)𝑇
  • (𝑘𝐴)1=1𝑘𝐴1

Calculating the inverse

To compute the inverse 𝐴1 of 𝐴𝑛×𝑛 we need to find a matrix X that satisfies 𝐴𝑋=𝐼. Then we can say that 𝑋=𝐴1.
Practically, with a pen and paper we can first create the augmented matrix [𝐴|𝐼𝑛] and perform operations on it to bring it to reduced row-echelon form. Then we can get the inverse on the right hand side of the equation system. Recall that this is called Gaussian elimination.
ex.

𝐴=(111012200)[𝐴|𝐼3]=(111012200100010001)

Implementation

see https://numpy.org/doc/stable/reference/routines.linalg.html
using numpy:

import numpy as np
 
# arrays inside an array
matA = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
matB = np.array([[2, 3, 4], [5, 6, 7], [8, 9, 10]])
print(matA.shape)
print(matB.shape)
print(np.add(matA, matB))
print(np.subtract(matA, matB))
print(np.dot(matA, matB))
# a determinant of 0 means that the matrix is singular.
print(np.linalg.det(matA))
# this causes an error, as for a singular matrix the inverse cannot be found
# print(np.linalg.inv(matA))
matC = np.array([[1, -1], [2, 3]])
# non zero determinant ==> non singular.
print(np.linalg.det(matC))
print(np.linalg.inv(matC))

my own naive implementations that I did a while ago for understanding:

class Matrix:
    """ This class models a two dimensional matrix """
 
    def __init__(self, rows: int, columns: int):
        """ Initialise a R x C matrix with zeroes """
        self.matrix = []
        self.rows = rows
        self.cols = columns
        self.dimensions = rows, columns
        for i in range(rows):
            row = []
            for j in range(columns):
                row.append(0)
            self.matrix.append(row)
 
    def __repr__(self):
        """ Prints the matrix """
        string = ""
        for row in self.matrix:
            string += f"{row}\n"
        return string.strip()
 
    def user_set(self):
        """ Takes user input for each element of the matrix """
        self.matrix = []
        for i in range(self.rows):
            row = []
            for j in range(self.cols):
                user_input = int(input(f"Element [{i}][{j}]: "))
                row.append(user_input)
            self.matrix.append(row)
    
    def __add__(self, another: Matrix):
        """ Adds two matrices together """
        if self.dimensions != another.dimensions:
            raise ArithmeticError("The two matrices don't have the same dimensions!")
        final_matrix = []
        for i in range(self.rows):
            row = []
            for j in range(self.cols):
                row.append(self.matrix[i][j] + another.matrix[i][j])
            final_matrix.append(row)
 
        new_matrix: Matrix = Matrix(self.rows, self.cols)
        new_matrix.from_list(final_matrix)
        return new_matrix
 
    def __sub__(self, another: Matrix):
        """ Subtracts the second matrix from the first """
        if self.dimensions != another.dimensions:
            raise ArithmeticError("The two matrices don't have the same dimensions!")
        final_matrix = []
        for i in range(self.rows):
            row = []
            for j in range(self.cols):
                row.append(self.matrix[i][j] - another.matrix[i][j])
            final_matrix.append(row)
 
        new_matrix: Matrix = Matrix(self.rows, self.cols)
        new_matrix.from_list(final_matrix)
        return new_matrix
 
    def __mul__(self, another: Matrix | float):
        """ Multiplies two matrices together, or a matrix and a constant together """
        if type(another) is Matrix:
            if self.cols != another.rows:
                raise ArithmeticError("For matrix multiplication, the number of columns in the first matrix must be equal to the number of rows in the second matrix.")
            final_matrix = []
            for i in range(self.rows):
                new_row = []
                for j in range(another.cols): 
                    # final_matrix[i][j] = 0
                    element = 0
                    for k in range(self.cols):
                        element += self.matrix[i][k] * another.matrix[k][j]
                    new_row.append(element)
                final_matrix.append(new_row)
 
            new_matrix: Matrix = Matrix(self.rows, another.cols)
 
        else:
            final_matrix = []
            for row in self.matrix:
                new_row = []
                for element in row:
                    new_element = another * element
                    new_row.append(new_element)
                final_matrix.append(new_row)
            new_matrix: Matrix = Matrix(self.rows, self.cols)
 
        new_matrix.from_list(final_matrix)
        return new_matrix
 
    def transpose(self):
        """ Returns the transpose of a matrix """
        transpose = []
        for i in range(self.cols):
            new_row = []
            for j in range(self.rows):
                new_row.append(self.matrix[j][i])
            transpose.append(new_row)
 
        new_matrix: Matrix = Matrix(self.cols, self.rows)
        new_matrix.from_list(transpose)
        return new_matrix
 
    def from_list(self, my_list: list):
        """ Returns a Matrix from a list """
        self.matrix = []
        for row in my_list:
            new_row = []
            for element in row:
                new_row.append(element)
            self.matrix.append(row)
        return self.matrix
 
if __name__ == "__main__":
    new = Matrix(3, 3)
    new.from_list([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
    another = Matrix(2, 3)
    another.from_list([[3, 2, 1], [2, 3, 6]])
    # traversing column wise?
    print(another)