3.3. Array Broadcasting

3.3.1. Rationale

Vector
Broadcasting
Matrix Multiplication

3.3.2. Broadcasting Rules

  1. Operations between multiple array objects are first checked for proper shape match

  2. Mathematical operators (+, -, *, /, exp, log, ...) apply element by element, on values

  3. Reduction operations (mean, std, skew, kurt, sum, prod, ...) apply to whole array, unless an axis is specified

  4. Missing values propagate, unless explicitly ignored (nanmean, nansum, ...)

3.3.3. Addition

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a + a
# array([[ 2,  4,  6],
#        [ 8, 10, 12]])

a + b
# array([[ 5,  7,  9],
#        [11, 13, 15]])

a + c
# array([[2, 4, 6],
#        [5, 7, 9]])

a + d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.4. Subtraction

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a - a
# array([[0, 0, 0],
#        [0, 0, 0]])

a - b
# array([[-3, -3, -3],
#        [-3, -3, -3]])

a - c
# array([[0, 0, 0],
#        [3, 3, 3]])

a - d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.5. Division

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a / a
# array([[1., 1., 1.],
#        [1., 1., 1.]])

a / b
# array([[0.25      , 0.4       , 0.5       ],
#        [0.57142857, 0.625     , 0.66666667]])

a / c
# array([[1. , 1. , 1. ],
#        [4. , 2.5, 2. ]])

a / d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.6. True Division

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a // a
# array([[1, 1, 1],
#        [1, 1, 1]])

a // b
# array([[0, 0, 0],
#        [0, 0, 0]])

a // c
# array([[1, 1, 1],
#        [4, 2, 2]])

a // d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.7. Modulo

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a % a
# array([[0, 0, 0],
#        [0, 0, 0]])

a % b
# array([[1, 2, 3],
#        [4, 5, 6]])

a % c
# array([[0, 0, 0],
#        [0, 1, 0]])

a % d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.8. Power

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a ** a
# array([[    1,     4,    27],
#        [  256,  3125, 46656]])

a ** b
# array([[       1,       32,      729],
#        [   16384,   390625, 10077696]])

a ** c
# array([[  1,   4,  27],
#        [  4,  25, 216]])

a ** d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.9. Root

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a ** (1/a)
# array([[1.        , 1.41421356, 1.44224957],
#        [1.41421356, 1.37972966, 1.34800615]])

a ** (1/b)
# array([[1.        , 1.14869835, 1.20093696],
#        [1.21901365, 1.22284454, 1.22028494]])

a ** (1/c)
# array([[1.        , 1.41421356, 1.44224957],
#        [4.        , 2.23606798, 1.81712059]])

a ** (1/d)
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.10. Array Multiplication

  • Multiplication * remains elementwise and does not correspond to matrix multiplication.

import numpy as np


a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([[4, 5, 6], [7, 8, 9]])
c = np.array([1, 2, 3])
d = np.array([4, 5])

a * a
# array([[ 1,  4,  9],
#        [16, 25, 36]])

a * b
# array([[ 4, 10, 18],
#        [28, 40, 54]])

a * c
# array([[ 1,  4,  9],
#        [ 4, 10, 18]])

a * d
# Traceback (most recent call last):
# ValueError: operands could not be broadcast together with shapes (2,3) (2,)

3.3.11. Matrix Multiplication

../_images/arithmetic-matmul.gif
../_images/arithmetic-matmul.jpg
import numpy as np


a = np.array([[1, 2, 3],
              [4, 5, 6]])

b = np.array([[1, 2],
              [3, 4],
              [5, 6]])

a @ b
# array([[22, 28],
#        [49, 64]])
import numpy as np


a = np.array([[1, 2, 3],
              [4, 5, 6]])

b = np.array([[4, 5, 6],
              [7, 8, 9]])

a @ b
# Traceback (most recent call last):
# ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 2 is different from 3)

3.3.12. Dot

  • np.dot()

  • If either a or b is 0-D (scalar), it is equivalent to multiply and using numpy.multiply(a, b) or a * b is preferred.

  • If both a and b are 1-D arrays, it is inner product of vectors (without complex conjugation).

  • If both a and b are 2-D arrays, it is matrix multiplication, but using matmul or a @ b is preferred.

  • If a is an N-D array and b is a 1-D array, it is a sum product over the last axis of a and b.

  • If a is an N-D array and b is an M-D array (where M>=2), it is a sum product over the last axis of a and the second-to-last axis of b: dot(a, b)[i,j,k,m] = sum(a[i,j,:] * b[k,:,m])

import numpy as np


a = np.array([1, 2, 3], float)
b = np.array([0, 1, 1], float)

np.dot(a, b)
# 5.0
import numpy as np


a = np.array([[0, 1], [2, 3]], float)
b = np.array([2, 3], float)
c = np.array([[1, 1], [4, 0]], float)

np.dot(b, a)
# array([ 6., 11.])

np.dot(a, b)
# array([ 3., 13.])

np.dot(a, c)
# array([[ 4., 0.],
#        [ 14., 2.]])

np.dot(c, a)
# array([[ 2., 4.],
#        [ 0., 4.]])

3.3.13. Assignments

Code 3.49. Solution
"""
* Assignment: Numpy Broadcasting Arithmetic
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. For given: `a: np.ndarray`, `b: np.ndarray`, `c: np.ndarray`
    2. Calculate square root of each element in `a` and `b`
    3. Calculate second power (square) of each element in `c`
    4. Add elements from `a` to `b`
    5. Multiply the result by `c`
    6. Run doctests - all must succeed

Polish:
    1. Dla danych: `a: np.ndarray`, `b: np.ndarray`, `c: np.ndarray`
    2. Oblicz pierwiastek kwadratowy każdego z elementu w `a` i `b`
    3. Oblicz drugą potęgę (kwadrat) każdego z elementu w `c`
    4. Dodaj elementy z `a` do `b`
    5. Przemnóż wynik przez `c`
    6. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> type(result)
    <class 'numpy.ndarray'>

    >>> result
    array([[ 1.41421356,  2.73205081],
           [45.254834  ,  0.        ]])
"""

import numpy as np


a = np.array([[0, 1], [2, 3]], float)
b = np.array([2, 3], float)
c = np.array([[1, 1], [4, 0]], float)

result = ...


Code 3.50. Solution
"""
* Assignment: Numpy Broadcasting Type Cast
* Complexity: easy
* Lines of code: 2 lines
* Time: 3 min

English:
    1. For given: `a: np.ndarray`, `b: np.ndarray` (see below)
    2. Add `a` and `b`
    3. Add `b` and `a`
    4. What happened?
    5. Run doctests - all must succeed

Polish:
    1. Dla danych: `a: np.ndarray`, `b: np.ndarray` (patrz sekcja input)
    2. Dodaj `a` i `b`
    3. Dodaj `b` i `a`
    4. Co się stało?
    5. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> type(ab)
    <class 'numpy.ndarray'>
    >>> type(ba)
    <class 'numpy.ndarray'>
    >>> ab
    array([[5, 1],
           [2, 3]])
    >>> ba
    array([[5, 1],
           [2, 3]])
"""

import numpy as np


a = np.array([[1, 0], [0, 1]])
b = [[4, 1], [2, 2]]


Code 3.51. Solution
"""
* Assignment: Numpy Broadcasting Matmul
* Complexity: easy
* Lines of code: 4 lines
* Time: 3 min

English:
    1. For given: `a: np.ndarray`, `b: np.ndarray` (see below)
    2. Multiply `a` and `b` using scalar multiplication
    3. Multiply `a` and `b` using matrix multiplication
    4. Multiply `b` and `a` using scalar multiplication
    5. Multiply `b` and `a` using matrix multiplication
    6. Discuss results
    7. Run doctests - all must succeed

Polish:
    1. Dla danych: `a: np.ndarray`, `b: np.ndarray` (patrz sekcja input)
    2. Przemnóż `a` i `b` używając mnożenia skalarnego
    3. Przemnóż `a` i `b` używając mnożenia macierzowego
    4. Przemnóż `b` i `a` używając mnożenia skalarnego
    5. Przemnóż `b` i `a` używając mnożenia macierzowego
    6. Omów wyniki
    7. Uruchom doctesty - wszystkie muszą się powieść

Tests:
    >>> import sys; sys.tracebacklimit = 0

    >>> mul_ab(a, b)  # doctest: +NORMALIZE_WHITESPACE
    Traceback (most recent call last):
    ValueError: operands could not be broadcast together with shapes (4,4) (4,2)

    >>> matmul_ab(a, b)
    array([[ 9,  2],
           [ 7,  3],
           [21,  8],
           [28,  8]])

    >>> mul_ba(b, a)  # doctest: +NORMALIZE_WHITESPACE
    Traceback (most recent call last):
    ValueError: operands could not be broadcast together with shapes (4,2) (4,4)

    >>> matmul_ba(b, a)
    Traceback (most recent call last):
    ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0, with gufunc signature (n?,k),(k,m?)->(n?,m?) (size 4 is different from 2)
"""

import numpy as np


a = np.array([[1, 0, 1, 0],
              [0, 1, 1, 0],
              [3, 2, 1, 0],
              [4, 1, 2, 0]])

b = np.array([
    [4, 1],
    [2, 2],
    [5, 1],
    [2, 3]])


def mul_ab(a, b):
    return ...


def matmul_ab(a, b):
    return ...


def mul_ba(b, a):
    return ...


def matmul_ba(b, a):
    return ...