What does the term "broadcasting" mean in Pandas documentation?

Broadcasting on Pandas DataFrames with MultiIndex

Broadcasting is especially interesting with DataFrames which have a pandas.MultiIndex as I show you in the following example.

Pandas makes it possible to broadcast over the dimensions added via a multidimensional and even hierarchical index, and this is very powerfull, if you know how to use it. You don't need to code your loops and conditions. You can rely on what works already.

I filled two pandas.DataFrames, af and df with a pandas.MultiIndex on the 0-axis (the index) and 10 columns labeled with integeres refering for example to scenario data from a Monte-Carlo simulation.

The pandas.MultiIndexes of the af and df share some common levels in the names (I call them dimensions). Not all labels (newer pandas versions call them codes) need to be in the matching dimensions. In the example, the dimensions 'a' and 'c' are shared. In both frames the 'a'-dimensions has the entries (labels) ['A' and 'B'], whereas in the 'c' dimension the frames af and bf have the entries [0, 1, 2, 3] and [0, 1, 2] respectively.

Nonetheless, Broadcasting works fine. Which means in the following example, when multiplying the two frames, a group-wise multiplication for each group with matching entries in the matching dimensions is performed.

The following example shows broadcasting on multiplications, but it works for all binary operations between pandas.DataFrames on the left- and right-hand side.

Some observations

Note, that both frames can have additional dimensions. It is not necessary that one set of names is a subset of the other. In the example we have ['a', 'b', 'c'] and ['a', 'c', 'd'] for the af and bf frames respectively

The result spans up the whole space, as expected: ['a', 'b', 'c', 'd']

Since dimension 'c' does not have the entry (code) '3' in frame bf, whereas af has, the result fills the resulting block with NaNs.

Note, that pandas 1.0.3 has been used here. Broadcasting with more then one overlapping dimensions did not work with pandas version 0.23.4.

Broadcasting over the 0-axis and the 1-axis at the same time does also work. See the last two examples. For example, if you would like to multiply the af with only bf[0].to_frame(), the first scenario. But it will only be applied to the equally labeled columns (as broadcasting is intended).

Further Hints

If you want to multiply the af frame with a column vector (I need to apply some weights sometimes with additional dimensions), then you can easily implement it yourself. You can expand your dataframe to n = af.shape[1] columns and use then that one for multiplication. Have a look at numpy.tile on how to do it 'without' coding.

>>> af
Values    0    1    2    3    4    5    6    7    8    9
a b c
A a 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
  b 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
  c 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
B a 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
  b 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
  c 0   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    1   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    2   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
    3   2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0  2.0
>>> bf
Values    0    1    2    3    4    5    6    7    8    9
a c d
A 0 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
  1 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
  2 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
B 0 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
  1 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
  2 *   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
    #   3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0  3.0
>>> af * bf
Values       0    1    2    3    4    5    6    7    8    9
a c b d
A 0 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  1 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  2 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  3 a NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
    b NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
    c NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
B 0 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  1 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  2 a *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    b *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
    c *    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
      #    6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0  6.0
  3 a NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
    b NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
    c NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN  NaN
>>> af * bf[0] # Raises Error: ValueError: cannot join with no overlapping index names

   # Removed that part

>>> af * bf[0].to_frame()  # works consistently
             0   1   2   3   4   5   6   7   8   9
a c b d                                           
A 0 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  1 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  2 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  3 a NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
B 0 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  1 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  2 a *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c *    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
      #    6.0 NaN NaN NaN NaN NaN NaN NaN NaN NaN
  3 a NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
    b NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN
    c NaN  NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN

>>> cf = bf[0].to_frame() 
>>> cf.columns = [3]
>>> af * cf  # And as expected we can broadcast over the same column labels at the same time
            0   1   2    3   4   5   6   7   8   9
a c b d                                           
A 0 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  1 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  2 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  3 a NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN
    b NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN
    c NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN
B 0 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  1 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  2 a *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    b *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
    c *   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
      #   NaN NaN NaN  6.0 NaN NaN NaN NaN NaN NaN
  3 a NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN
    b NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN
    c NaN NaN NaN NaN  NaN NaN NaN NaN NaN NaN NaN

So the term broadcasting comes from numpy, simply put it explains the rules of the output that will result when you perform operations between n-dimensional arrays (could be panels, dataframes, series) or scalar values.

Broadcasting using a scalar value

So the simplest case is just multiplying by a scalar value:

In [4]:
s = pd.Series(np.arange(5))
s

Out[4]:
0    0
1    1
2    2
3    3
4    4
dtype: int32

In [5]:    
s * 10

Out[5]:
0     0
1    10
2    20
3    30
4    40
dtype: int32

and we get the same expected results with a dataframe:

In [6]:    
df = pd.DataFrame({'a':np.random.randn(4), 'b':np.random.randn(4)})
df

Out[6]:
          a         b
0  0.216920  0.652193
1  0.968969  0.033369
2  0.637784  0.856836
3 -2.303556  0.426238

In [7]:    
df * 10

Out[7]:
           a         b
0   2.169204  6.521925
1   9.689690  0.333695
2   6.377839  8.568362
3 -23.035557  4.262381

So what is technically happening here is that the scalar value has been broadcasted along the same dimensions of the Series and DataFrame above.

Broadcasting using a 1-D array

Say we have a 2-D dataframe of shape 4 x 3 (4 rows x 3 columns) we can perform an operation along the x-axis by using a 1-D Series that is the same length as the row-length:

In [8]:
df = pd.DataFrame({'a':np.random.randn(4), 'b':np.random.randn(4), 'c':np.random.randn(4)})
df

Out[8]:
          a         b         c
0  0.122073 -1.178127 -1.531254
1  0.011346 -0.747583 -1.967079
2 -0.019716 -0.235676  1.419547
3  0.215847  1.112350  0.659432

In [26]:    
df.iloc[0]

Out[26]:
a    0.122073
b   -1.178127
c   -1.531254
Name: 0, dtype: float64

In [27]:    
df + df.iloc[0]

Out[27]:
          a         b         c
0  0.244146 -2.356254 -3.062507
1  0.133419 -1.925710 -3.498333
2  0.102357 -1.413803 -0.111707
3  0.337920 -0.065777 -0.871822

the above looks funny at first until you understand what is happening, I took the first row of values and added this row-wise to the df, it can be visualised using this pic (sourced from scipy):

enter image description here

The general rule is this:

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.

So if I tried to add a 1-D array that didn't match in length, say one with 4 elements, unlike numpy which will raise a ValueError, in Pandas you'll get a df full of NaN values:

In [30]:
df + pd.Series(np.arange(4))

Out[30]:
    a   b   c   0   1   2   3
0 NaN NaN NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN NaN NaN
2 NaN NaN NaN NaN NaN NaN NaN
3 NaN NaN NaN NaN NaN NaN NaN

Now some of the great things about pandas is that it will try to align using existing column names and row labels, this can get in the way of trying to perform a fancier broadcasting like this:

enter image description here

In [55]:
df[['a']] + df.iloc[0]

Out[55]:
          a   b   c
0  0.244146 NaN NaN
1  0.133419 NaN NaN
2  0.102357 NaN NaN
3  0.337920 NaN NaN

In the above I use double subscripting to force the shape to be (4,1) but we see a problem when trying to broadcast using the first row as the column alignment only aligns on the first column. To get the same form of broadcasting to occur like the diagram above shows we have to decompose to numpy arrays which then become anonymous data:

In [56]:
df[['a']].values + df.iloc[0].values

Out[56]:
array([[ 0.24414608, -1.05605392, -1.4091805 ],
       [ 0.13341899, -1.166781  , -1.51990758],
       [ 0.10235701, -1.19784299, -1.55096957],
       [ 0.33792013, -0.96227987, -1.31540645]])

It's also possible to broadcast in 3-dimensions but I don't go near that stuff often but the numpy, scipy and pandas book have examples that show how that works.

Generally speaking the thing to remember is that aside from scalar values which are simple, for n-D arrays the minor/trailing axes length must match or one of them must be 1.

Update

it seems that the above now leads to ValueError: Unable to coerce to Series, length must be 1: given 3 in latest version of pandas 0.20.2

so you have to call .values on the df first:

In[42]:
df[['a']].values + df.iloc[0].values

Out[42]: 
array([[ 0.244146, -1.056054, -1.409181],
       [ 0.133419, -1.166781, -1.519908],
       [ 0.102357, -1.197843, -1.55097 ],
       [ 0.33792 , -0.96228 , -1.315407]])

To restore this back to the original df we can construct a df from the np array and pass the original columns in the args to the constructor:

In[43]:
pd.DataFrame(df[['a']].values + df.iloc[0].values, columns=df.columns)

Out[43]: 
          a         b         c
0  0.244146 -1.056054 -1.409181
1  0.133419 -1.166781 -1.519908
2  0.102357 -1.197843 -1.550970
3  0.337920 -0.962280 -1.315407