{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "# Lecture 6 - NumPy for Array Operations" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[![View notebook on Github](https://img.shields.io/static/v1.svg?logo=github&label=Repo&message=View%20On%20Github&color=lightgrey)](https://github.com/avakanski/Fall-2024-Applied-Data-Science-with-Python/blob/main/docs/Lectures/Theme_2-Data_Engineering/Lecture_6-NumPy/Lecture_6-NumPy.ipynb)\n", "[![Open In Collab](https://colab.research.google.com/assets/colab-badge.svg)](https://colab.research.google.com/github/avakanski/Fall-2024-Applied-Data-Science-with-Python/blob/main/docs/Lectures/Theme_2-Data_Engineering/Lecture_6-NumPy/Lecture_6-NumPy.ipynb) " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "- [6.1 Introduction to NumPy](#6.1-introduction-to-numpy)\n", "- [6.2 Array Construction and Indexing](#6.2-array-construction-and-indexing)\n", "- [6.3 Array Math](#6.3-array-math)\n", "- [6.4 Broadcasting](#6.4-broadcasting)\n", "- [6.5 Random Number Generators](#6.5-random-number-generators)\n", "- [6.6 Reshaping NumPy Arrays](#6.6-reshaping-numpy-arrays)\n", "- [6.7 Linear Algebra with NumPy](#6.7-linear-algebra-with-numpy)\n", "- [Appendix](#appendix)\n", "- [References](#references)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.1 Introduction to NumPy " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy (short for Numerical Python) was created in 2005, and since then, the NumPy library has evolved into an essential library for scientific computing in Python. It has become a building block of many other scientific libraries, such as SciPy, Scikit-learn, Pandas, and others.\n", "\n", "NumPy provides a convenient Python interface for working with multi-dimensional array data structures. The NumPy array data structure is also called `ndarray`, which is short for *n*-dimensional array. \n", "\n", "In addition to being mostly implemented in C and using Python as a \"glue language,\" the main reason why NumPy is so efficient for numerical computations is that NumPy arrays use contiguous blocks of memory that can be efficiently cached by the CPU. In contrast, Python lists are arrays of pointers to objects in random locations in memory, which cannot be easily cached and come with a more expensive memory look-up. In addition, NumPy arrays have a fixed size and are homogeneous, which means that all elements in an array must have the same type. Homogenous `ndarray` objects have the advantage that NumPy can carry out operations using efficient C code and avoid expensive type checks and related resource-consuming Python operations. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### N-dimensional Arrays" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy is built around [`ndarrays`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.ndarray.html) objects, which are multi-dimensional array data structures. Intuitively, we can think of a one-dimensional NumPy array as a vector of elements -- you may think of it as a fixed-size Python list where all elements share the same type. Similarly, we can think of a two-dimensional array as a data structure to represent a matrix (or a Python list of lists). NumPy arrays can have up to 32 dimensions. For instance, RGB images have 3 dimensions, corresponding to pixels width and height, and the three color channels. Similarly, an RGB video has 4 dimensions, corresponding to pixels width and height, color channel, and frame number.\n", "\n", "In this next example, we will call the `array` function first to create an one-dimensional NumPy array, and afterward, a two-dimensional NumPy array, consisting of two rows and three columns (from a list of lists)." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1., 2., 3.])" ] }, "execution_count": 1, "metadata": {}, "output_type": "execute_result" } ], "source": [ "import numpy as np\n", "\n", "ary1d = [1., 2., 3.]\n", "np.array(ary1d)" ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [4, 5, 6]])" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "lst = [[1, 2, 3], \n", " [4, 5, 6]]\n", "\n", "ary2d = np.array(lst)\n", "ary2d\n", "\n", "# rows x columns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "By default, NumPy infers the type of the array upon construction. Since in the second example we passed Python integers to the 2D array, the `ndarray` object `ary2d` should be of type `int32`, which we can confirm by accessing the `dtype` attribute." ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('int32')" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary2d.dtype" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we want to construct NumPy arrays of different types, we can pass an argument for the `dtype` parameter of the array using the `astype` method (for example `np.int64` to create 64-bit arrays). For a full list of supported data types, please refer to the official [NumPy documentation](https://docs.scipy.org/doc/numpy/user/basics.types.html). " ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [4, 5, 6]], dtype=int64)" ] }, "execution_count": 4, "metadata": {}, "output_type": "execute_result" } ], "source": [ "int64_ary = ary2d.astype(np.int64)\n", "int64_ary" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1., 2., 3.],\n", " [4., 5., 6.]], dtype=float32)" ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "float32_ary = ary2d.astype(np.float32)\n", "float32_ary" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "dtype('float32')" ] }, "execution_count": 6, "metadata": {}, "output_type": "execute_result" } ], "source": [ "float32_ary.dtype" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To return the number of elements in an array, we can use the `size` attribute, as shown below." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "6" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary2d.size" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And the number of dimensions of our array can be obtained via the `ndim` attribute." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 8, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary2d.ndim" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we are interested in the number of elements along each array dimension (in the context of NumPy arrays, we may also refer to the array dimensions as *axes*), we can access the `shape` attribute as shown below." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(2, 3)" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary2d.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `shape` of arrays is a tuple. In the code example above, the two-dimensional `ary2d` object has two *rows* and *three* columns, `(2, 3)`.\n", "\n", "The `shape` of a one-dimensional array only contains a single value." ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(3,)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.array([1., 2., 3.]).shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.2 Array Construction and Indexing " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "This section provides several useful functions to construct arrays, e.g., containing only ones or zeros." ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 1, 1, 1],\n", " [1, 1, 1, 1],\n", " [1, 1, 1, 1]])" ] }, "execution_count": 11, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.ones((3, 4), dtype=int)" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0., 0., 0.],\n", " [0., 0., 0.],\n", " [0., 0., 0.]])" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.zeros((3, 3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can use these functions to create arrays with arbitrary values, e.g., we can create an array containing the values 99 as follows." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[99., 99., 99.],\n", " [99., 99., 99.],\n", " [99., 99., 99.]])" ] }, "execution_count": 13, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.zeros((3, 3)) + 99" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Creating arrays of ones or zeros can also be useful as placeholder arrays, in cases where we do not want to use the initial values for computations right away. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy also has functions to create identity matrices and diagonal matrices as `ndarrays`. " ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1., 0., 0.],\n", " [0., 1., 0.],\n", " [0., 0., 1.]])" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.eye(3)" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 0, 0],\n", " [0, 2, 0],\n", " [0, 0, 3]])" ] }, "execution_count": 15, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.diag((1, 2, 3))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Two other very useful functions for creating sequences of numbers within a specified range are `arange` and `linspace`. NumPy's `arange` function follows the same syntax as Python's `range` function. If two arguments are provided, the first argument represents the start value and the second argument defines the stop value of a half-open interval." ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([4, 5, 6, 7, 8, 9])" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.arange(4, 10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we only provide a single argument, the default start value of 0 is assumed." ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0, 1, 2, 3, 4])" ] }, "execution_count": 17, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.arange(5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similar to Python's `range`, a third argument can be provided to define the *step* (the default step size is 1). For example, we can obtain an array of all values between 1 and 11 with 0.1 step as follows." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 1. , 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7, 1.8, 1.9, 2. ,\n", " 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3. , 3.1,\n", " 3.2, 3.3, 3.4, 3.5, 3.6, 3.7, 3.8, 3.9, 4. , 4.1, 4.2,\n", " 4.3, 4.4, 4.5, 4.6, 4.7, 4.8, 4.9, 5. , 5.1, 5.2, 5.3,\n", " 5.4, 5.5, 5.6, 5.7, 5.8, 5.9, 6. , 6.1, 6.2, 6.3, 6.4,\n", " 6.5, 6.6, 6.7, 6.8, 6.9, 7. , 7.1, 7.2, 7.3, 7.4, 7.5,\n", " 7.6, 7.7, 7.8, 7.9, 8. , 8.1, 8.2, 8.3, 8.4, 8.5, 8.6,\n", " 8.7, 8.8, 8.9, 9. , 9.1, 9.2, 9.3, 9.4, 9.5, 9.6, 9.7,\n", " 9.8, 9.9, 10. , 10.1, 10.2, 10.3, 10.4, 10.5, 10.6, 10.7, 10.8,\n", " 10.9])" ] }, "execution_count": 18, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary3 = np.arange(1., 11., 0.1)\n", "ary3" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note the shape of the above array." ] }, { "cell_type": "code", "execution_count": 19, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(100,)" ] }, "execution_count": 19, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.shape(ary3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `linspace` function is especially useful if we want to create a number of evenly spaced values in a specified interval. In the next cell, we created a array of 5 values evenly spaced between 6 and 25." ] }, { "cell_type": "code", "execution_count": 20, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 6. , 10.75, 15.5 , 20.25, 25. ])" ] }, "execution_count": 20, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.linspace(6., 25., num=5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Array Indexing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Simple NumPy indexing and slicing work similar to Python lists, as in the following examples." ] }, { "cell_type": "code", "execution_count": 21, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "3" ] }, "execution_count": 21, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([1, 2, 3, 4])\n", "ary[2]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Also, the same Python semantics apply to slicing operations. The following example shows how to fetch the first three elements in `ary`." ] }, { "cell_type": "code", "execution_count": 22, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3])" ] }, "execution_count": 22, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[0:3] " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we work with arrays that have more than one dimension or axis, we separate our indexing or slicing operations by commas as shown in the following examples." ] }, { "cell_type": "code", "execution_count": 23, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "2" ] }, "execution_count": 23, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "ary[0, -2] # first row, second from last element" ] }, { "cell_type": "code", "execution_count": 24, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "6" ] }, "execution_count": 24, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[-1, -1] # lower right" ] }, { "cell_type": "code", "execution_count": 25, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "5" ] }, "execution_count": 25, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[1, 1] # first row, second column" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "code", "execution_count": 26, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 4])" ] }, "execution_count": 26, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[:, 0] # entire first column" ] }, { "cell_type": "code", "execution_count": 27, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2],\n", " [4, 5]])" ] }, "execution_count": 27, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[:, :2] # first two columns" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.3 Array Math " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One of the core features of NumPy that makes working with `ndarray` so efficient and convenient is **vectorization**. NumPy provides vectorized functions for performing element-wise operations implicitly via so-called *ufuncs* which is short for \"universal functions\".\n", "\n", "There are more than 60 ufuncs available in NumPy. *ufuncs* are implemented in compiled C code and are very fast and efficient compared to Python. \n", "\n", "To provide an example of a simple ufunc for element-wise addition, consider the following example, where we add a scalar 1 to each element in a nested Python list." ] }, { "cell_type": "code", "execution_count": 28, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[2, 3, 4], [5, 6, 7]]" ] }, "execution_count": 28, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Element-wise addition in Python\n", "lst = [[1, 2, 3], \n", " [4, 5, 6]] # 2d array\n", "\n", "for row_idx, row_val in enumerate(lst):\n", " for col_idx, col_val in enumerate(row_val):\n", " lst[row_idx][col_idx] += 1\n", "lst" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can accomplish the same using NumPy's ufunc for element-wise scalar addition as shown below." ] }, { "cell_type": "code", "execution_count": 29, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[2, 3, 4],\n", " [5, 6, 7]])" ] }, "execution_count": 29, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Element-wise addition in NumPy\n", "ary = np.array([[1, 2, 3], [4, 5, 6]])\n", "ary = np.add(ary, 1) \n", "ary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For basic arithmetic operations we can use `add`, `subtract`, `divide`, `multiply`, `power`, and `exp` (exponential). However, NumPy uses operator overloading, and therefore, we can directly use mathematical operators (`+`, `-`, `/`, `*`, and `**`)." ] }, { "cell_type": "code", "execution_count": 30, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[3, 4, 5],\n", " [6, 7, 8]])" ] }, "execution_count": 30, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.add(ary, 1)" ] }, { "cell_type": "code", "execution_count": 31, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[3, 4, 5],\n", " [6, 7, 8]])" ] }, "execution_count": 31, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary + 1" ] }, { "cell_type": "code", "execution_count": 32, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 4, 9, 16],\n", " [25, 36, 49]], dtype=int32)" ] }, "execution_count": 32, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.power(ary, 2)" ] }, { "cell_type": "code", "execution_count": 33, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 4, 9, 16],\n", " [25, 36, 49]])" ] }, "execution_count": 33, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary**2" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy also has implementations for other math operations, such as `sqrt` (square root), `log` (natural logarithm), and `log10` (base-10 logarithm)." ] }, { "cell_type": "code", "execution_count": 34, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1.41421356, 1.73205081, 2. ],\n", " [2.23606798, 2.44948974, 2.64575131]])" ] }, "execution_count": 34, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.sqrt(ary)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Often, we want to compute the sum or product of array elements along a given axis. For this purpose, we can use the `reduce` operation. By default, `reduce` applies an operation along the first axis (`axis=0`). In the case of a two-dimensional array, we can think of the first axis as the rows of a matrix. Thus, adding up elements along rows yields the column sums of that matrix as shown below." ] }, { "cell_type": "code", "execution_count": 35, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([5, 7, 9])" ] }, "execution_count": 35, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3], \n", " [4, 5, 6]]) # rolling over the 1st axis, axis 0\n", "\n", "np.add.reduce(ary, axis=0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To compute the row sums of the array above, we can specify `axis=1`." ] }, { "cell_type": "code", "execution_count": 36, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 6, 15])" ] }, "execution_count": 36, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.add.reduce(ary, axis=1) # row sums" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy also provides functions for specific operations such as `product` and `sum`. For example, `sum(axis=0)` is equivalent to `add.reduce`." ] }, { "cell_type": "code", "execution_count": 37, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([5, 7, 9])" ] }, "execution_count": 37, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary.sum(axis=0) # column sums" ] }, { "cell_type": "code", "execution_count": 38, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([ 6, 15])" ] }, "execution_count": 38, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary.sum(axis=1) # row sums" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note also that `product` and `sum` both compute the product or sum of the entire array if we do not specify an axis." ] }, { "cell_type": "code", "execution_count": 39, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "21" ] }, "execution_count": 39, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary.sum()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Other useful *ufuncs* are:\n", " \n", "- `np.mean` (computes arithmetic mean, i.e., average)\n", "- `np.std` (computes the standard deviation)\n", "- `np.var` (computes variance)\n", "- `np.sort` (sorts an array)\n", "- `np.argsort` (returns indices that would sort an array)\n", "- `np.min` (returns the minimum value of an array)\n", "- `np.max` (returns the maximum value of an array)\n", "- `np.argmin` (returns the index of the minimum value)\n", "- `np.argmax` (returns the index of the maximum value)\n", "- `np.array_equal` (checks if two arrays have the same shape and elements)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.4 Broadcasting " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Broadcasting** allows to perform vectorized operations between two arrays even if their dimensions do not match. " ] }, { "cell_type": "code", "execution_count": 40, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([2, 3, 4])" ] }, "execution_count": 40, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary1 = np.array([1, 2, 3])\n", "ary1 + 1" ] }, { "cell_type": "code", "execution_count": 41, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([2, 3, 4])" ] }, "execution_count": 41, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# this is equivalent to:\n", "ary1 + np.array([1, 1, 1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For example, we can add a one-dimensional to a two-dimensional array, where NumPy creates an implicit multidimensional grid from the one-dimensional array `ary1`." ] }, { "cell_type": "code", "execution_count": 42, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 5, 7, 9],\n", " [ 8, 10, 12]])" ] }, "execution_count": 42, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary2 = np.array([[4, 5, 6], \n", " [7, 8, 9]])\n", "\n", "ary2 + ary1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using broadcasting in Python requires compatibility between the sizes of the arrays. For instance, if we try to add two arrays of sizes (3,3) and (2,2), Python will return an error." ] }, { "cell_type": "code", "execution_count": 43, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "operands could not be broadcast together with shapes (3,3) (2,2) ", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[43], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m np\u001b[38;5;241m.\u001b[39mones((\u001b[38;5;241m3\u001b[39m,\u001b[38;5;241m3\u001b[39m)) \u001b[38;5;241m+\u001b[39m np\u001b[38;5;241m.\u001b[39mones((\u001b[38;5;241m2\u001b[39m,\u001b[38;5;241m2\u001b[39m))\n", "\u001b[1;31mValueError\u001b[0m: operands could not be broadcast together with shapes (3,3) (2,2) " ] } ], "source": [ "np.ones((3,3)) + np.ones((2,2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Advanced Indexing -- Memory Views and Copies" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the previous sections, we used basic indexing and slicing routines. It is important to note that basic integer-based indexing and slicing create so-called *views* of NumPy arrays in the memory. Working with views can be highly desirable since it avoids making unnecessary copies of arrays to save memory resources. To illustrate the concept of memory views, let's look at a simple example where we access the first row in an array, assign it to a variable, and modify that variable." ] }, { "cell_type": "code", "execution_count": 44, "metadata": {}, "outputs": [], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "first_row = ary[0, :]" ] }, { "cell_type": "code", "execution_count": 45, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3])" ] }, "execution_count": 45, "metadata": {}, "output_type": "execute_result" } ], "source": [ "first_row" ] }, { "cell_type": "code", "execution_count": 46, "metadata": {}, "outputs": [], "source": [ "first_row += 99" ] }, { "cell_type": "code", "execution_count": 47, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([100, 101, 102])" ] }, "execution_count": 47, "metadata": {}, "output_type": "execute_result" } ], "source": [ "first_row" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As expected, `first_row` was modified, now containing the original values in the first row incremented by 99." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note, however, that the original array was modified as well. The reason for this is that `ary[0, :]` created a view of the first row in `ary`, and its elements were then incremented by 99. " ] }, { "cell_type": "code", "execution_count": 48, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[100, 101, 102],\n", " [ 4, 5, 6]])" ] }, "execution_count": 48, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The same concept applies to slicing operations." ] }, { "cell_type": "code", "execution_count": 49, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 1, 2, 102],\n", " [ 4, 5, 105]])" ] }, "execution_count": 49, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "last_col = ary[:, 2]\n", "last_col += 99\n", "ary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Therefore, it is important to be aware that indexing and slicing create views, which can speed up our code by avoiding to create unnecessary copies in memory. However, in certain scenarios we want to create a copy of an array; we can do this via the `copy` method as shown below." ] }, { "cell_type": "code", "execution_count": 50, "metadata": {}, "outputs": [], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "first_row = ary[0].copy()\n", "first_row += 99" ] }, { "cell_type": "code", "execution_count": 51, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([100, 101, 102])" ] }, "execution_count": 51, "metadata": {}, "output_type": "execute_result" } ], "source": [ "first_row" ] }, { "cell_type": "code", "execution_count": 52, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [4, 5, 6]])" ] }, "execution_count": 52, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "On the other hand, slicing a list in Python creates a copy, and it does not change the original list." ] }, { "cell_type": "code", "execution_count": 53, "metadata": {}, "outputs": [], "source": [ "list1 = [[1, 2, 3], [4, 5, 6]]" ] }, { "cell_type": "code", "execution_count": 54, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[1, 2, 3]" ] }, "execution_count": 54, "metadata": {}, "output_type": "execute_result" } ], "source": [ "first_row_list = list1[0]\n", "first_row_list" ] }, { "cell_type": "code", "execution_count": 55, "metadata": {}, "outputs": [ { "ename": "TypeError", "evalue": "can only concatenate list (not \"int\") to list", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mTypeError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[55], line 2\u001b[0m\n\u001b[0;32m 1\u001b[0m \u001b[38;5;66;03m# cannot broadcast\u001b[39;00m\n\u001b[1;32m----> 2\u001b[0m first_row_list \u001b[38;5;241m+\u001b[39m \u001b[38;5;241m99\u001b[39m\n", "\u001b[1;31mTypeError\u001b[0m: can only concatenate list (not \"int\") to list" ] } ], "source": [ "# cannot broadcast\n", "first_row_list + 99" ] }, { "cell_type": "code", "execution_count": 56, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[1, 2, 3, 99]" ] }, "execution_count": 56, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# note that 99 is concatenated\n", "first_row_list + [99]" ] }, { "cell_type": "code", "execution_count": 57, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "[[1, 2, 3], [4, 5, 6]]" ] }, "execution_count": 57, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# the original list is not changed\n", "list1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Fancy Indexing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In addition to basic single-integer indexing and slicing operations, NumPy supports advanced indexing routines called *fancy indexing*. Via fancy indexing, we can use tuple or list objects of non-contiguous integer indices to return desired array elements. Since fancy indexing can be performed with non-contiguous sequences, it cannot return a view -- a contiguous slice from memory. Thus, fancy indexing always returns a copy of an array: it is important to keep that in mind. The following code snippets show some fancy indexing examples." ] }, { "cell_type": "code", "execution_count": 58, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 3],\n", " [4, 6]])" ] }, "execution_count": 58, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "ary[:, [0, 2]] # first and and last column" ] }, { "cell_type": "code", "execution_count": 59, "metadata": {}, "outputs": [], "source": [ "this_is_a_copy = ary[:, [0, 2]]\n", "this_is_a_copy += 99" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that the values in `this_is_a_copy` were incremented as expected." ] }, { "cell_type": "code", "execution_count": 60, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[100, 102],\n", " [103, 105]])" ] }, "execution_count": 60, "metadata": {}, "output_type": "execute_result" } ], "source": [ "this_is_a_copy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, the contents of the original array remain unaffected." ] }, { "cell_type": "code", "execution_count": 61, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [4, 5, 6]])" ] }, "execution_count": 61, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Boolean Masks for Indexing" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also use Boolean masks for indexing, that is, arrays of `True` and `False` values. Consider the following example, where we return all values in the array that are greater than 3." ] }, { "cell_type": "code", "execution_count": 62, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[False, False, False],\n", " [ True, True, True]])" ] }, "execution_count": 62, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3],\n", " [4, 5, 6]])\n", "\n", "greater3_mask = ary > 3\n", "greater3_mask" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using these masks, we can select elements given our desired criteria." ] }, { "cell_type": "code", "execution_count": 63, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([4, 5, 6])" ] }, "execution_count": 63, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[greater3_mask]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or, we can also write this as follows." ] }, { "cell_type": "code", "execution_count": 64, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([4, 5, 6])" ] }, "execution_count": 64, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[ary>3]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also chain different selection criteria using the logical *and* operator `&` or the logical *or* operator `|`. The example below demonstrates how we can select array elements that are greater than 3 and divisible by 2." ] }, { "cell_type": "code", "execution_count": 65, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[False, False, False],\n", " [ True, False, True]])" ] }, "execution_count": 65, "metadata": {}, "output_type": "execute_result" } ], "source": [ "(ary > 3) & (ary % 2 == 0)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similar to the previous example, we can use this boolean array as a mask for selecting the respective elements from the array." ] }, { "cell_type": "code", "execution_count": 66, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([4, 6])" ] }, "execution_count": 66, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[(ary > 3) & (ary % 2 == 0)]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And, for example, to negate a condition, we can use the `~` operator:" ] }, { "cell_type": "code", "execution_count": 67, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([2, 3, 4])" ] }, "execution_count": 67, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary[~((ary < 2) | (ary > 4) )]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A related, useful function to assign values to specific elements in an array is the `np.where` function. In the example below, we assign a 1 to all values in the array that are greater than 2, and 0 otherwise. \n", "\n", "The general syntax is `np.where(condition, x, y)`, and the returned elements are `x` if the condition is `True` and `y` if the condition is `False`." ] }, { "cell_type": "code", "execution_count": 68, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0, 0, 1, 1, 1])" ] }, "execution_count": 68, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([1, 2, 3, 4, 5])\n", "\n", "np.where(ary > 2, 1, 0)" ] }, { "cell_type": "code", "execution_count": 69, "metadata": { "tags": [] }, "outputs": [ { "data": { "text/plain": [ "array(['Freezing', 'Above Freezing', 'Above Freezing', 'Above Freezing',\n", " 'Freezing'], dtype=' 3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.5 Random Number Generators " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In machine learning and data science, we often need to generate arrays of random numbers, such as the initial values of the model parameters. NumPy has a `random` subpackage to create random numbers and samples from a variety of distributions. \n", "\n", "Let's start with drawing a random sample of three numbers in the range [0,1] from a uniform distribution via `random.rand`." ] }, { "cell_type": "code", "execution_count": 72, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.22422424, 0.70761147, 0.37458586])" ] }, "execution_count": 72, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.rand(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we make another draw, we will obtain a different random sample. " ] }, { "cell_type": "code", "execution_count": 73, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.827369 , 0.80704915, 0.56825233])" ] }, "execution_count": 73, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.rand(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Numpy also allows to use a fixed *random seed* for the random number generator, and in that case, the random sample will be same at each draw. " ] }, { "cell_type": "code", "execution_count": 74, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.69646919, 0.28613933, 0.22685145])" ] }, "execution_count": 74, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.seed(seed=123)\n", "\n", "np.random.rand(3)" ] }, { "cell_type": "code", "execution_count": 75, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.69646919, 0.28613933, 0.22685145])" ] }, "execution_count": 75, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.seed(seed=123)\n", "\n", "np.random.rand(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And, of course, if we change the random seed value, the generated random numbers will be also different." ] }, { "cell_type": "code", "execution_count": 76, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.11505457, 0.60906654, 0.13339096])" ] }, "execution_count": 76, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.seed(seed=43)\n", "\n", "np.random.rand(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Using a random seed is highly recommended in practical applications and in research projects, since it ensures that our results are reproducible, since using the same seed will create the same random numbers. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also create multi-dimensional arrays of random numbers, if needed." ] }, { "cell_type": "code", "execution_count": 77, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[0.24058962, 0.32713906, 0.85913749, 0.66609021, 0.54116221],\n", " [0.02901382, 0.7337483 , 0.39495002, 0.80204712, 0.25442113],\n", " [0.05688494, 0.86664864, 0.221029 , 0.40498945, 0.31609647],\n", " [0.0766627 , 0.84322469, 0.84893915, 0.97146509, 0.38537691],\n", " [0.95448813, 0.44575836, 0.66972465, 0.08250005, 0.89709858]])" ] }, "execution_count": 77, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.rand(5,5)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "And, we can draw random numbers from other distributions. For instance, `random.randn` returns random numbers from a normal, i.e., Gaussian, distribution, with mean 0, and variance 1. " ] }, { "cell_type": "code", "execution_count": 78, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([-1.04683899, -0.88961759, 0.01404054, -0.16082969])" ] }, "execution_count": 78, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.random.randn(4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Or, for instance if we needed to generate 6 random values from a normal distribution with mean -3 and standard deviation 10, we can write as follows." ] }, { "cell_type": "code", "execution_count": 79, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([19.30359649, -6.99115719, -2.45555437, 5.84181815, -4.07980561,\n", " 2.55606984])" ] }, "execution_count": 79, "metadata": {}, "output_type": "execute_result" } ], "source": [ "-3 + 10*np.random.randn(6)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The class `RandomState` is another way to create and manage random number generators in `NumPy`. `RandomState` objects maintain their own state independently of other objects in our code, and this allows for the generation of random numbers without affecting other parts of the program. " ] }, { "cell_type": "code", "execution_count": 80, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([0.69646919, 0.28613933, 0.22685145])" ] }, "execution_count": 80, "metadata": {}, "output_type": "execute_result" } ], "source": [ "rng2 = np.random.RandomState(seed=123)\n", "rng2.rand(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In the above code, `np.random.RandomState()` constructed a random number generator that we named `rng2`. When we call `rng2` to draw random numbers, it uses the specified seed (`seed=123`) to create random numbers in a controlled way. \n", "\n", "Note however that the seed 123 will be applied only to the `rng2` object, and it does not affect other variables in our code that use `np.random`. For instance, we can apply a different random seed to all other `np.random` functions in our code, e.g., by `np.random.seed(seed=11)`. Therefore, `rng2` will maintain its own local random state based on the random seed 123, which is independent of the other instances that are controlled by the global random number generator with a random seed 11." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.6 Reshaping NumPy Arrays " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In practice, we often run into situations where existing arrays do not have the *right* shape to perform certain computations. As you might remember from the beginning of this article, the size of NumPy arrays is fixed. Fortunately, this does not mean that we have to create new arrays and copy values from the old array to the new one if we want arrays of different shapes. That is, the size is fixed, but the shape is not. NumPy provides a `reshape` method that allows to obtain a view of an array with a different shape. \n", "\n", "For example, we can reshape a one-dimensional array into a two-dimensional one using `reshape` as follows." ] }, { "cell_type": "code", "execution_count": 81, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [4, 5, 6]])" ] }, "execution_count": 81, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary1d = np.array([1, 2, 3, 4, 5, 6])\n", "ary2d_view = ary1d.reshape(2, 3)\n", "ary2d_view" ] }, { "cell_type": "code", "execution_count": 82, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "True" ] }, "execution_count": 82, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.may_share_memory(ary2d_view, ary1d)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `True` value returned from `np.may_share_memory` indicates that the reshape operation returns a memory view, not a copy." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When using reshape, we need to make sure that the reshaped array has the same number of elements as the original one. Otherwise, we will obtain an error message." ] }, { "cell_type": "code", "execution_count": 83, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "cannot reshape array of size 6 into shape (3,4)", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[83], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m ary2d_view1 \u001b[38;5;241m=\u001b[39m ary1d\u001b[38;5;241m.\u001b[39mreshape(\u001b[38;5;241m3\u001b[39m, \u001b[38;5;241m4\u001b[39m)\n", "\u001b[1;31mValueError\u001b[0m: cannot reshape array of size 6 into shape (3,4)" ] } ], "source": [ "ary2d_view1 = ary1d.reshape(3, 4)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "However, we do not need to specify the number of elements in each axis. NumPy can figure out how many elements to put along an axis if only one axis is unspecified, by using the placeholder `-1`." ] }, { "cell_type": "code", "execution_count": 84, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2],\n", " [3, 4],\n", " [5, 6]])" ] }, "execution_count": 84, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary1d.reshape(-1, 2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can also use `reshape` to flatten an array." ] }, { "cell_type": "code", "execution_count": 85, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3, 4, 5, 6])" ] }, "execution_count": 85, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[[1, 2, 3],\n", " [4, 5, 6]]])\n", "\n", "ary.reshape(-1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Other methods for flattening arrays exist, namely `flatten`, which creates a copy of the array, and `ravel`, which creates a memory view." ] }, { "cell_type": "code", "execution_count": 86, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3, 4, 5, 6])" ] }, "execution_count": 86, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary.flatten()" ] }, { "cell_type": "code", "execution_count": 87, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3, 4, 5, 6])" ] }, "execution_count": 87, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary.ravel()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sometimes, we are interested in merging different arrays. Unfortunately, there is no efficient way to do this without creating a new array, since NumPy arrays have a fixed size. While combining arrays should be avoided if possible for reasons of computational efficiency, it is sometimes necessary. To combine two or more array objects, we can use NumPy's `concatenate` function as shown in the following examples." ] }, { "cell_type": "code", "execution_count": 88, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [1, 2, 3]])" ] }, "execution_count": 88, "metadata": {}, "output_type": "execute_result" } ], "source": [ "ary = np.array([[1, 2, 3]])\n", "\n", "# stack along the first axis (here: rows)\n", "np.concatenate((ary, ary), axis=0)" ] }, { "cell_type": "code", "execution_count": 89, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3, 1, 2, 3]])" ] }, "execution_count": 89, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# stack along the second axis (here: columns)\n", "np.concatenate((ary, ary), axis=1)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Two related functions are `vstack` and `hstack` that stand for vertically or horizontally stacking arrays, respectively. " ] }, { "cell_type": "code", "execution_count": 90, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3],\n", " [1, 2, 3]])" ] }, "execution_count": 90, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.vstack((ary, ary))" ] }, { "cell_type": "code", "execution_count": 91, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 2, 3, 1, 2, 3]])" ] }, "execution_count": 91, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.hstack((ary, ary))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## 6.7 Linear Algebra with NumPy " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can think of one-dimensional NumPy arrays as data structures that represent row vectors." ] }, { "cell_type": "code", "execution_count": 92, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3])" ] }, "execution_count": 92, "metadata": {}, "output_type": "execute_result" } ], "source": [ "row_vector = np.array([1, 2, 3])\n", "row_vector" ] }, { "cell_type": "code", "execution_count": 93, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(3,)" ] }, "execution_count": 93, "metadata": {}, "output_type": "execute_result" } ], "source": [ "row_vector.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, we can use two-dimensional arrays to create column vectors." ] }, { "cell_type": "code", "execution_count": 94, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1],\n", " [2],\n", " [3]])" ] }, "execution_count": 94, "metadata": {}, "output_type": "execute_result" } ], "source": [ "column_vector = np.array([1, 2, 3]).reshape(-1, 1)\n", "column_vector" ] }, { "cell_type": "code", "execution_count": 95, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "(3, 1)" ] }, "execution_count": 95, "metadata": {}, "output_type": "execute_result" } ], "source": [ "column_vector.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Matrix Multiplication in NumPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To perform matrix multiplication between matrices, we know that number of columns of the left matrix must match the number of rows of the matrix to the right. In NumPy, we can perform matrix multiplication via the `matmul` function." ] }, { "cell_type": "code", "execution_count": 96, "metadata": {}, "outputs": [], "source": [ "matrix = np.array([[1, 2, 3], \n", " [4, 5, 6]])" ] }, { "cell_type": "code", "execution_count": 97, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[14],\n", " [32]])" ] }, "execution_count": 97, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.matmul(matrix, column_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "Figure source: Reference [1]." ] }, { "cell_type": "markdown", "metadata": { "tags": [] }, "source": [ "Here is another example." ] }, { "cell_type": "code", "execution_count": 98, "metadata": {}, "outputs": [], "source": [ "A = np.array([[1, 2, 3], \n", " [4, 5, 6]])\n", "\n", "B = np.array([[7, 8], \n", " [9, 10], \n", " [11, 12]])" ] }, { "cell_type": "code", "execution_count": 99, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 58, 64],\n", " [139, 154]])" ] }, "execution_count": 99, "metadata": {}, "output_type": "execute_result" } ], "source": [ "C = np.matmul(A, B)\n", "\n", "C" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Alternatively, we can also use the `@` operator for matrix multiplication, which is more concise." ] }, { "cell_type": "code", "execution_count": 100, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[ 58, 64],\n", " [139, 154]])" ] }, "execution_count": 100, "metadata": {}, "output_type": "execute_result" } ], "source": [ "D = A @ B\n", "\n", "D" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that we the `*` operator in NumPy performs element-wise multiplication (Hadamard product), rather than matrix multiplication. " ] }, { "cell_type": "code", "execution_count": 101, "metadata": {}, "outputs": [], "source": [ "E = np.array([[1, 1], \n", " [2, 2]])\n", "\n", "F = np.array([[3, 3], \n", " [4, 4]])" ] }, { "cell_type": "code", "execution_count": 102, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[3, 3],\n", " [8, 8]])" ] }, "execution_count": 102, "metadata": {}, "output_type": "execute_result" } ], "source": [ "E * F" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If we attempt to perform element-wise multiplication of the arrays A and B with the `*` operator, we get an error message, due to mismatch of their dimensions. " ] }, { "cell_type": "code", "execution_count": 103, "metadata": {}, "outputs": [ { "ename": "ValueError", "evalue": "operands could not be broadcast together with shapes (2,3) (3,2) ", "output_type": "error", "traceback": [ "\u001b[1;31m---------------------------------------------------------------------------\u001b[0m", "\u001b[1;31mValueError\u001b[0m Traceback (most recent call last)", "Cell \u001b[1;32mIn[103], line 1\u001b[0m\n\u001b[1;32m----> 1\u001b[0m A \u001b[38;5;241m*\u001b[39m B\n", "\u001b[1;31mValueError\u001b[0m: operands could not be broadcast together with shapes (2,3) (3,2) " ] } ], "source": [ "A * B" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "When multiplying matrices and vectors, NumPy can be quite forgiving if the dimensions of matrices and one-dimensional arrays do not match exactly -- thanks to broadcasting. The following example of multiplying the above matrix with a row-vector yields the same result as the multiplication of the matrix with the column vector, except that it returns a one-dimensional array instead of a two-dimensional array." ] }, { "cell_type": "code", "execution_count": 104, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([14, 32])" ] }, "execution_count": 104, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.matmul(matrix, row_vector)" ] }, { "cell_type": "code", "execution_count": 105, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[14],\n", " [32]])" ] }, "execution_count": 105, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# compare to:\n", "np.matmul(matrix, column_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Dot-product in NumPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy has a special `dot` function that calculates the dot-product for one-dimensional arrays, i.e., the sum of products of corresponding elements." ] }, { "cell_type": "code", "execution_count": 106, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([1, 2, 3])" ] }, "execution_count": 106, "metadata": {}, "output_type": "execute_result" } ], "source": [ "row_vector" ] }, { "cell_type": "code", "execution_count": 107, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "14" ] }, "execution_count": 107, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.dot(row_vector, row_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note in the next cell that the dot product of a `row_vector` and a `column_vector` is an array, differently from the dot product of a `row_vector` and a `row_vector` in the previous cell which is a scalar." ] }, { "cell_type": "code", "execution_count": 108, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([14])" ] }, "execution_count": 108, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.dot(row_vector, column_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, we can compute the dot-product between two vectors using `matmul` or the `@` operator ." ] }, { "cell_type": "code", "execution_count": 109, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "14" ] }, "execution_count": 109, "metadata": {}, "output_type": "execute_result" } ], "source": [ "row_vector @ row_vector" ] }, { "cell_type": "code", "execution_count": 110, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "14" ] }, "execution_count": 110, "metadata": {}, "output_type": "execute_result" } ], "source": [ "np.matmul(row_vector, row_vector)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Matrix Transpose in NumPy" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "NumPy also has a handy `transpose` method to transpose matrices." ] }, { "cell_type": "code", "execution_count": 111, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 4],\n", " [2, 5],\n", " [3, 6]])" ] }, "execution_count": 111, "metadata": {}, "output_type": "execute_result" } ], "source": [ "matrix = np.array([[1, 2, 3], \n", " [4, 5, 6]])\n", "\n", "matrix.transpose()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is also a shorthand notation for `transpose` simply as `T`." ] }, { "cell_type": "code", "execution_count": 112, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "array([[1, 4],\n", " [2, 5],\n", " [3, 6]])" ] }, "execution_count": 112, "metadata": {}, "output_type": "execute_result" } ], "source": [ "matrix.T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "While this section demonstrates some of the basic linear algebra operations carried out on NumPy arrays that we use in practice, you can find additional functions in the documentation of NumPy's submodule for linear algebra: [`numpy.linalg` documentation](https://docs.scipy.org/doc/numpy/reference/routines.linalg.html). If you want to perform a particular linear algebra routine that is not implemented in NumPy, it is also worth consulting the [`scipy.linalg` documentation](https://docs.scipy.org/doc/scipy/reference/linalg.html) -- SciPy is a library for scientific computing built on top of NumPy." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "One last note is that there is also a special [`matrix`](https://docs.scipy.org/doc/numpy/reference/generated/numpy.matrix.html) data type in NumPy. NumPy `matrix` objects are analogous to NumPy arrays but are restricted to two dimensions. Also, matrices define certain operations differently than arrays; for instance, the `*` operator performs matrix multiplication instead of element-wise multiplication. However, NumPy `matrix` is less popular in the science community compared to the more general array data structure. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Appendix " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**The material in the Appendix is not required for quizzes and assignments.**" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Motivation for Using NumPy: It is Fast!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let's compare computing a vector dot product in Python (using lists) and compare it with NumPy's dot-product function. Mathematically, the dot product between two vectors $\\mathbf{x}$ and $\\mathbf{w}$ can be written as follows:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "$$\n", "z = \\sum_i x_i w_i = x_1 \\times w_1 + x_2 \\times w_2 + ... + x_n \\times w_n = \\mathbf{x}^\\top \\mathbf{w} \n", "$$" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "First, the Python implementation using a for-loop." ] }, { "cell_type": "code", "execution_count": 113, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "32.0\n" ] } ], "source": [ "def python_forloop_list_approach(x, w):\n", " z = 0.\n", " for i in range(len(x)):\n", " z += x[i] * w[i]\n", " return z\n", "\n", "\n", "a = [1., 2., 3.]\n", "b = [4., 5., 6.]\n", "\n", "print(python_forloop_list_approach(a, b))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Let us compute the runtime for two larger (1000-element) vectors using IPython's `%timeit` magic function." ] }, { "cell_type": "code", "execution_count": 114, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "124 µs ± 10.5 µs per loop (mean ± std. dev. of 7 runs, 10,000 loops each)\n" ] } ], "source": [ "large_a = list(range(1000))\n", "large_b = list(range(1000))\n", "\n", "\n", "%timeit python_forloop_list_approach(large_a, large_b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Next, we use the `dot` function/method implemented in NumPy to compute the dot product between two vectors and run `%timeit` afterwards." ] }, { "cell_type": "code", "execution_count": 115, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "32.0\n" ] } ], "source": [ "def numpy_dotproduct_approach(x, w):\n", " # same as np.dot(x, w)\n", " # and same as x @ w\n", " return x.dot(w)\n", " \n", "\n", "a = np.array([1., 2., 3.])\n", "b = np.array([4., 5., 6.])\n", "\n", "print(numpy_dotproduct_approach(a, b))" ] }, { "cell_type": "code", "execution_count": 116, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "1.73 µs ± 324 ns per loop (mean ± std. dev. of 7 runs, 100,000 loops each)\n" ] } ], "source": [ "large_a = np.arange(1000)\n", "large_b = np.arange(1000)\n", "\n", "%timeit numpy_dotproduct_approach(large_a, large_b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "As we can see, replacing the for-loop with NumPy's dot function makes the computation of the vector dot product approximately 100 times faster." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## References \n", "\n", "1. Scientific Computing in Python: Introduction to NumPy and Matplotlib, by Sebastian Raschka, available at: [https://sebastianraschka.com/blog/2020/numpy-intro.html](https://sebastianraschka.com/blog/2020/numpy-intro.html)." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "[BACK TO TOP](#top)" ] } ], "metadata": { "kernelspec": { "display_name": "Python 3 (ipykernel)", "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.11.5" } }, "nbformat": 4, "nbformat_minor": 4 }