Skip to content

    Chapter 13, Basic

    Broadcasting

    Adding two arrays of the same size is easy. Broadcasting is what happens when the sizes are different, and NumPy still finds a way to do the calculation.

    Code for this chapter: básico/cap13_broadcasting.py

    The idea: stretch, without copying

    Think of the smaller array being stretched to match the larger one. NumPy does not create copies of the data: it reuses memory as if it were repeated. A row added to a grid applies to all rows, and a column applies to all columns:

    básico/cap13_broadcasting.pylinhas 10 a 18
    import numpy as np
    
    grade = np.ones((3, 4))
    linha = np.array([1, 2, 3, 4])
    print(grade + linha)
    
    coluna = np.array([[10], [20], [30]])
    print(grade + coluna)
    print((np.arange(3).reshape(3, 1) + np.arange(4)).shape)
    
    Output
    [[2. 3. 4. 5.]
     [2. 3. 4. 5.]
     [2. 3. 4. 5.]]
    [[11. 11. 11. 11.]
     [21. 21. 21. 21.]
     [31. 31. 31. 31.]]
    (3, 4)
    

    The last example is the most powerful: a column (3, 1) added to a row (4,) produces a grid (3, 4), with all combinations.

    The rule

    Compare the shapes from right to left, one dimension at a time. Two dimensions are compatible if they are equal or if one of them is 1. If all dimensions pass this test, the shapes combine. Missing dimensions on the left are treated as 1 by NumPy. You can check the rule by code:

    básico/cap13_broadcasting.pylinhas 23 a 33
    def compativel(a, b):
        try:
            return np.broadcast_shapes(a, b)
        except ValueError:
            return None
    
    
    print(compativel((3, 4), (4,)))
    print(compativel((3, 1), (1, 4)))
    print(compativel((5, 1, 3), (3,)))
    print(compativel((3, 4), (3,)))
    
    Output
    (3, 4)
    (3, 4)
    (5, 1, 3)
    None
    

    The last case fails: (3, 4) and (3,) align the 4 with the 3 (from right to left), which are not equal and neither is 1. The actual error has this message:

    básico/cap13_broadcasting.pylinhas 35 a 38
    try:
        np.ones((3, 4)) + np.ones(3)
    except ValueError as erro:
        print(erro)
    
    Output
    operands could not be broadcast together with shapes (3,4) (3,) 
    

    Real uses

    Centering each column (subtracting the mean of each) and normalizing each row (dividing by the sum of the row) are the two most common operations, and both depend on broadcasting. The keepdims=True keeps the reduced axis with size 1, which is exactly what broadcasting needs:

    básico/cap13_broadcasting.pylinhas 43 a 45
    notas = np.array([[7.0, 8.0, 9.0], [4.0, 6.0, 8.0]])
    print(notas - notas.mean(axis=0))
    print(notas / notas.sum(axis=1, keepdims=True))
    
    Output
    [[ 1.5  1.   0.5]
     [-1.5 -1.  -0.5]]
    [[0.29166667 0.33333333 0.375     ]
     [0.22222222 0.33333333 0.44444444]]
    

    The subtraction uses a mean per column (shape (3,)). The division needs a sum per row in column shape, (2, 1), and that’s why the keepdims=True is necessary.

    Another use is to calculate all differences between pairs at once, without a double loop:

    básico/cap13_broadcasting.pylinhas 47 a 48
    indices = np.arange(4)
    print(indices[:, None] - indices)
    
    Output
    [[ 0 -1 -2 -3]
     [ 1  0 -1 -2]
     [ 2  1  0 -1]
     [ 3  2  1  0]]
    

    Memory is not copied

    Broadcasting is a virtualstretching. You can see this in the strides: a step of 0 bytes means "repeat the same data". And the result is read-only, because writing to elements that are actually the same would be ambiguous:

    básico/cap13_broadcasting.pylinhas 53 a 54
    esticado = np.broadcast_to(np.array([1, 2, 3]), (1000, 3))
    print(esticado.shape, esticado.strides, esticado.flags["WRITEABLE"])
    
    Output
    (1000, 3) (0, 8) False
    

    A thousand rows "exist", but take up the space of one. That’s why broadcasting is fast and economical. The caution comes from the other side: the result of the calculation (the grid (3, 4) of the example) is a real array, and at large scales it can blow up memory (chapter 31).

    It’s not "any smaller array will do"

    The typical error is thinking that if an array is smaller, it always combines. What matters is each dimension. Test the shape with np.broadcast_shapes before running a large calculation, or read the error message: it shows the two shapes side by side.

    Exercise 1

    Standardize the columns

    Write padronizar_colunas(x) that subtracts the mean of each column and divides by its standard deviation (the z-score). After standardizing, each column has a mean of 0 and a standard deviation of 1.