{
"cells": [
{
"cell_type": "markdown",
"metadata": {},
"source": [
"# Broadcasting\n",
"\n",
"The fact that vectors and matrices are just special cases of arrays, and that arrays can have any number of dimensions and any shape, raises an obvious question: how do arrays of different shapes and numbers of dimensions interact?\n",
"\n",
"The answer is that in situations where there is a principled way for arrays of different shapes to interact with one another, numpy will allow operations like adding or multiplying the arrays via a process called **broadcasting.**\n",
"\n",
"Broadcasting is actually something that you've already been exposed to implicitly. When working with vectors, for example, we talked about how, if we try to do math with two vectors of the same length, numpy will apply our mathematical operation element-wise, matching the first entry in the first vector to the first entry in the second vector, the second entry in the first vector to the second entry in the second vector, etc:\n"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"import numpy as np\n",
"\n",
"v1 = np.array([1, 2, 3])\n",
"v2 = np.array([4, 5, 6])\n",
"v1 + v2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Broadcasting](img/2.3.41-broadcasting_0.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Or, if one of the two vectors is of length one (or we are working with a regular scaler number that isn't a vector), numpy will *broadcast* the operation by applying the operation between the single number and each entry and the longer vector:"
]
},
{
"cell_type": "code",
"execution_count": 2,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([2, 3, 4])"
]
},
"execution_count": 2,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"v1 + 1\n"
]
},
{
"cell_type": "code",
"execution_count": 3,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([2, 3, 4])"
]
},
"execution_count": 3,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"v1 + np.array([1])\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Broadcasting](img/2.3.41-broadcasting_1.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"And if we try to do math with two vectors when the lengths aren't the same and one of the vectors is not of length one the case, we get a `ValueError: operand could not be broadcast together` error. For example, numpy could not do `np.array([1, 2, 3]) + np.array([1, 2])`. \n",
"\n",
"And indeed, this is basically the first rule of broadcasting: it only works when the length of the arrays along the dimension over which you are broadcasting have the same length, or where one is of length 1.\n",
"\n",
"## Broadcasting Rules\n",
"\n",
"So what happens if we're dealing with arrays with more than one dimension? And what do we do if our arrays have different numbers of dimensions -- e.g. can I make a vector interact with a matrix, or a matrix interact with a three-dimensional array? \n",
"\n",
"The answer is that numpy addresses these situations by looking at the shapes of the two arrays (e.g. `my_array.shape`) -- starting with the rightmost dimension -- to see if matched the dimensions have compatible lengths (e.g. are the same or one is length 1). If so, broadcasting will occur accordingly, and if not you'll get a `ValueError: operands could not be broadcast together` error. \n",
"\n",
"OK, that was a lot of terms, so let's be concrete: suppose have a vector of length 3 and a matrix that is 2 x 3:"
]
},
{
"cell_type": "code",
"execution_count": 4,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[1, 2, 3],\n",
" [4, 5, 6]])"
]
},
"execution_count": 4,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"my_vector = np.array([1, 2, 3])\n",
"my_matrix = np.array([[1, 2, 3], [4, 5, 6]])\n",
"my_matrix\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Now suppose we wanted to add these together. Could we do that? To answer that question, numpy would start by comparing the shapes of the two arrays: `(3,)` and `(2, 3)`. Starting at the rightmost entry, it would then try and match these up, so it would match `3` to `3`. Then it would ask: are these the same, or is one equal to 1? Here the answer is yes, so numpy would *broadcast* the operation by applying it repeatedly along each row (the extra dimension in the matrix):"
]
},
{
"cell_type": "code",
"execution_count": 5,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[2, 4, 6],\n",
" [5, 7, 9]])"
]
},
"execution_count": 5,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"my_vector + my_matrix\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Broadcasting](img/2.3.41-broadcasting_2.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But what if my matrix had been 3x2 instead of 2x3? Well, then numpy would have matched `3` (the vector length) to the rightmost entry in `my_matrix2.shape` (`2`), found they were different and that neither was 1, so it would just raise an error:\n",
"\n",
"```python\n",
"my_matrix2 = np.array([[1, 2], [3, 4], [5, 6]])\n",
"my_vector + my_matrix2\n",
"\n",
"---------------------------------------------------------------------------\n",
"ValueError Traceback (most recent call last)\n",
"/var/folders/tj/s8f2_ks15h315z5thvtnhz8r0000gp/T/ipykernel_16753/2980522362.py in \n",
" 1 my_matrix2 = np.array([[1, 2],[ 3, 4], [5, 6]])\n",
"----> 2 my_vector + my_matrix2\n",
"\n",
"ValueError: operands could not be broadcast together with shapes (3,) (3,2) \n",
"```"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"## A Common Gotcha: Narrow Matrices v. 1-Dimensional Vectors\n",
"\n",
"And now a common issue people run into related to broadcasting: in numpy, there is a distinction between a 1-dimensional vector (the data structure we used throughout Week 2), and a 2-dimensional matrix with only 1 row or 1 column.\n",
"\n",
"To illustrate, let's start by creating a simple vector and getting its `.shape`:"
]
},
{
"cell_type": "code",
"execution_count": 6,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([1, 2, 3])"
]
},
"execution_count": 6,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"my_vector = np.array([1, 2, 3])\n",
"my_vector"
]
},
{
"cell_type": "code",
"execution_count": 7,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3,)"
]
},
"execution_count": 7,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"my_vector.shape"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see, numpy only reports the size of our vector in one dimension (the trailing comma is included so you know that it's a list with one entry, not a weirdly formatted 3). One value is given because our data is one-dimensional. "
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But if we create a matrix with three rows and one column (note I'm passing a list within a list to `np.array`), we get a data structure that *looks* similar, but is actually different in an important way, as evident from the output of `.shape`:"
]
},
{
"cell_type": "code",
"execution_count": 8,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[1],\n",
" [2],\n",
" [3]])"
]
},
"execution_count": 8,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"skinny_matrix = np.array([[1], [2], [3]])\n",
"skinny_matrix"
]
},
{
"cell_type": "code",
"execution_count": 9,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3, 1)"
]
},
"execution_count": 9,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"skinny_matrix.shape"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"As we can see, this matrix is two-dimensional, as evidenced by the fact the `.shape` is reporting two numbers. \n",
"\n",
"This distinction is important because these differences in the shape of our arrays impact how numpy broadcasts operations. For example, let's suppose that we wanted to add the contents of `skinny_matrix` to the contents of `my_vector`. Intuitively, these both have three entries, so we would think that they would add up element-wise, just the same way they would if we added up `my_vector` with itself:"
]
},
{
"cell_type": "code",
"execution_count": 10,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([2, 4, 6])"
]
},
"execution_count": 10,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"my_vector + my_vector\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"But that's not what happens:"
]
},
{
"cell_type": "code",
"execution_count": 11,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[2, 3, 4],\n",
" [3, 4, 5],\n",
" [4, 5, 6]])"
]
},
"execution_count": 11,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"skinny_matrix + my_vector\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Why? Because the shape of `skinny_matrix` is `(3, 1)`, when numpy looks at the length of the rightmost dimension, it finds the value `1`, so instead of matching up the elements one to one, it applies the addition operation between `my_vector` and each row, generating a three by three matrix."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"![Broadcasting](img/2.3.41-broadcasting_3.png)"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"\n",
"To avoid this type of behavior, we just need to reshape `skinny_matrix` so that it is actually one-dimensional:"
]
},
{
"cell_type": "code",
"execution_count": 12,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([1, 2, 3])"
]
},
"execution_count": 12,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"now_a_vector = skinny_matrix.reshape(3)\n",
"now_a_vector"
]
},
{
"cell_type": "code",
"execution_count": 13,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3,)"
]
},
"execution_count": 13,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"now_a_vector.shape"
]
},
{
"cell_type": "code",
"execution_count": 14,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([2, 4, 6])"
]
},
"execution_count": 14,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"now_a_vector + my_vector\n"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"Or we could use `.reshape` to make our original one-dimensional vector a matrix with the same shape as our `skinny_matrix`:"
]
},
{
"cell_type": "code",
"execution_count": 15,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[1],\n",
" [2],\n",
" [3]])"
]
},
"execution_count": 15,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"now_a_matrix = my_vector.reshape((3, 1))\n",
"now_a_matrix"
]
},
{
"cell_type": "code",
"execution_count": 16,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"(3, 1)"
]
},
"execution_count": 16,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"now_a_matrix.shape"
]
},
{
"cell_type": "code",
"execution_count": 17,
"metadata": {},
"outputs": [
{
"data": {
"text/plain": [
"array([[2],\n",
" [4],\n",
" [6]])"
]
},
"execution_count": 17,
"metadata": {},
"output_type": "execute_result"
}
],
"source": [
"skinny_matrix + now_a_matrix\n"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Python 3.10.6 ('base')",
"language": "python",
"name": "python3"
},
"language_info": {
"codemirror_mode": {
"name": "ipython",
"version": 3
},
"file_extension": ".py",
"mimetype": "text/x-python",
"name": "python",
"nbconvert_exporter": "python",
"pygments_lexer": "ipython3",
"version": "3.10.6"
},
"orig_nbformat": 4,
"vscode": {
"interpreter": {
"hash": "718fed28bf9f8c7851519acf2fb923cd655120b36de3b67253eeb0428bd33d2d"
}
}
},
"nbformat": 4,
"nbformat_minor": 2
}