NumPy - broadcasting (advanced)

https://numpy.org/devdocs/user/theory.broadcasting.html
Array Broadcasting in Numpy

https://numpy.org/doc/stable/user/basics.broadcasting.html

INTRODUCTION

The term broadcasting describes how numpy treats arrays with different shapes during arithmetic operations. Subject to certain constraints, the smaller array is “broadcast” across the larger array so that they have compatible shapes. Broadcasting provides a means of vectorizing array operations so that looping occurs in C instead of Python.

The Broadcasting Rule:
In order to broadcast, the size of the trailing axes for both arrays in an operation must either be the same size or one of them must be one.


A = np.array([[11, 12, 13], [21, 22, 23], [31, 32, 33]]) # A.shape is (3,3)
# array([[11, 12, 13],
#        [21, 22, 23],
#        [31, 32, 33]])

B = np.array([1, 2, 3])   # B.shape is (3,)

3 * B   # simple broacasting
# the number 3 acts as np.array([3, 3, 3])
# +--+--+--+   +--+--+--+   +--+--+--+
# | 3|  |  | * | 1| 2| 3| = | 3| 6| 9|
# +--+--+--+   +--+--+--+   +--+--+--+

A * B   # multiplication with broadcasting (elementwise)
# B acts as np.array([[1, 2, 3]] * 3)
# array([[1, 2, 3],
#        [1, 2, 3],
#        [1, 2, 3]])
# Result:
# array([[11, 24, 39],
#        [21, 44, 69],
#        [31, 64, 99]])
# +--+--+--+   +--+--+--+   +--+--+--+
# |11|12|13|   | 1| 2| 3|   |12|24|39|
# +--+--+--+   +--+--+--+   +--+--+--+
# |21|22|23| * |  |  |  | = |21|44|69|
# +--+--+--+   +--+--+--+   +--+--+--+
# |31|32|33|   |  |  |  |   |31|64|99|
# +--+--+--+   +--+--+--+   +--+--+--+

A + B   # addition with broadcasting (elementwise)
# Result:
# array([[12, 14, 16],
#        [22, 24, 26],
#        [32, 34, 36]])
# +--+--+--+   +--+--+--+   +--+--+--+
# |11|12|13|   | 1| 2| 3|   |12|14|16|
# +--+--+--+   +--+--+--+   +--+--+--+
# |21|22|23| + |  |  |  | = |22|24|26|
# +--+--+--+   +--+--+--+   +--+--+--+
# |31|32|33|   |  |  |  |   |31|34|36|
# +--+--+--+   +--+--+--+   +--+--+--+

A - B   # substraction with broadcasting (elementwise)
# Result:
# array([[10, 10, 10],
#        [20, 20, 20],
#        [30, 30, 30]])
# +--+--+--+   +--+--+--+   +--+--+--+
# |11|12|13|   | 1| 2| 3|   |10|10|10|
# +--+--+--+   +--+--+--+   +--+--+--+
# |21|22|23| - |  |  |  | = |20|20|20|
# +--+--+--+   +--+--+--+   +--+--+--+
# |31|32|33|   |  |  |  |   |30|30|30|
# +--+--+--+   +--+--+--+   +--+--+--+

# numpy.newaxis is used to increase the dimension of the existing array
# by one more dimension, when used once.
# numpy.newaxis is an alias for ‘None’.

C = B[:,np.newaxis]   # C.shape is (3, 1)
# array([[1],
#        [2],
#        [3]])
np.may_share_memory(B, C)   # True

D = B[np.newaxis,:]   # D.shape is (1, 3)
# array([[1, 2, 3]])
np.may_share_memory(B, D)   # True

A * B[:, np.newaxis]
# B acts as np.array([[1, 2, 3],] * 3).transpose()
# array([[1, 1, 1],
#        [2, 2, 2],
#        [3, 3, 3]])
# Result:
# array([[11, 12, 13],
#        [42, 44, 46],
#        [93, 96, 99]])
# +--+--+--+   +--+--+--+   +--+--+--+
# |11|12|13|   | 1|  |  |   |11|12|13|
# +--+--+--+   +--+--+--+   +--+--+--+
# |21|22|23| * | 2|  |  | = |42|44|46|
# +--+--+--+   +--+--+--+   +--+--+--+
# |31|32|33|   | 3|  |  |   |93|96|99|
# +--+--+--+   +--+--+--+   +--+--+--+

A = np.array([10, 20, 30])
B = np.array([1, 2, 3])

A[:, np.newaxis] * B   # double broadcasting
# A acts as np.array([[10, 20, 30],] * 3).transpose()
# B acts as np.array([[1, 2, 3]] * 3)
# Result:
# array([[10, 20, 30],
#        [20, 40, 60],
#        [30, 60, 90]])
# +--+--+--+   +--+--+--+   +--+--+--+
# |10|  |  |   | 1| 2| 3|   |10|20|30|
# +--+--+--+   +--+--+--+   +--+--+--+
# |20|  |  | * |  |  |  | = |20|40|60|
# +--+--+--+   +--+--+--+   +--+--+--+
# |30|  |  |   |  |  |  |   |30|60|90|
# +--+--+--+   +--+--+--+   +--+--+--+

In some cases, broadcasting stretches both arrays to form an output array larger than either of the initial arrays.