{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "\n", "\n", "\n", "\n", "| - | - | - |\n", "|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|-----------------------------------------------------------------------------------------------------|\n", "| [Exercise 11 (rows and columns)](<#Exercise-11-(rows-and-columns)>) | [Exercise 12 (row and column vectors)](<#Exercise-12-(row-and-column-vectors)>) | [Exercise 13 (diamond)](<#Exercise-13-(diamond)>) |\n", "| [Exercise 14 (vector lengths)](<#Exercise-14-(vector-lengths)>) | [Exercise 15 (vector angles)](<#Exercise-15-(vector-angles)>) | [Exercise 16 (multiplication table revisited)](<#Exercise-16-(multiplication-table-revisited)>) |\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# NumPy\n", "\n", "[NumPy](https://docs.scipy.org/doc/numpy/) is a Python library for handling multi-dimensional arrays. It contains both the data structures needed for the storing and accessing arrays, and operations and functions for computation using these arrays. Although the arrays are usually used for storing numbers, other type of data can be stored as well, such as strings. Unlike lists in core Python, NumPy's fundamental data structure, the array, must have the same data type for all its elements. The homogeneity of arrays allows highly optimized functions that use arrays as their inputs and outputs.\n", "\n", "There are several uses for high-dimensional arrays in data analysis. For instance, they can be used to:\n", "\n", "* store matrices, solve systems of linear equations, find eigenvalues/vectors, find matrix decompositions, and solve other problems familiar from linear algebra\n", "* store multi-dimensional measurement data. For example, an element `a[i,j]` in a 2-dimensional array might store the temperature $t_{ij}$ measured at coordinates i, j on a 2-dimension surface.\n", "* images and videos can be represented as NumPy arrays:\n", "\n", " * a gray-scale image can be represented as a two dimensional array\n", " * a color image can be represented as a three dimensional image, the third dimension contains the color components red, green, and blue\n", " * a color video can be represented as a four dimensional array\n", "* a 2-dimensional table might store a sequence of *samples*, and each sample might be divided into *features*. For example, we could measure the weather conditions once per day, and the conditions could include the temperature, direction and speed of wind, and the amount of rain. Then we would have one sample per day, and the features would be the temperature, wind, and rain. In the standard representation of this kind of tabular data, the rows corresponds to samples and the columns correspond to features. We see more of this kind of data in the chapters on Pandas and Scikit-learn.\n", "\n", "In this chapter we will go through:\n", "\n", "* Creation of arrays\n", "* Array types and attributes\n", "* Accessing arrays with indexing and slicing\n", "* Reshaping of arrays\n", "* Combining and splitting arrays\n", "* Fast operations on arrays\n", "* Aggregations of arrays\n", "* Rules of binary array operations\n", "* Matrix operations familiar from linear algebra" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We start by importing the NumPy library, and we use the standard abbreviation `np` for it." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Creation of arrays\n", "There are several ways of creating NumPy arrays. One way is to give a (nested) list as a parameter to the `array` constructor:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.array([1,2,3]) # one dimensional array" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that leaving out the brackets from the above expression, i.e. calling `np.array(1,2,3)` will result in an error." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Two dimensional array can be given by listing the rows of the array:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.array([[1,2,3], [4,5,6]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly, three dimensional array can be described as a list of lists of lists:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.array([[[1,2], [3,4]], [[5,6], [7,8]]])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There are some helper functions to create common types of arrays:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.zeros((3,4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "To specify that elements are `int`s instead of `float`s, use the parameter `dtype`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.zeros((3,4), dtype=int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Similarly `ones` initializes all elements to one, `full` initializes all elements to a specified value, and `empty` leaves the elements uninitialized:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.ones((2,3))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.full((2,3), fill_value=7)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.empty((2,4))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `eye` function creates the identity matrix, that is, a matrix with elements on the diagonal are set to one, and non-diagonal elements are set to zero:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.eye(5, dtype=int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The `arange` function works like the `range` function, but produces an array instead of a list." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.arange(0,10,2)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For non-integer ranges it is better to use `linspace`:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.linspace(0, np.pi, 5) # Evenly spaced range with 5 elements" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "With `linspace` one does not have to compute the length of the step, but instead one specifies the wanted number of elements. By default, the endpoint is included in the result, unlike with `arange`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Arrays with random elements\n", "\n", "To test our programs we might use real data as input. However, real data is not always available, and it may take time to gather. We could instead generate random numbers to use as substitute. They can be generated really easily with NumPy, and can be sampled from several different distributions, of which we mention below only a few. Random data can simulate real data better than, for example, ranges or constant arrays. Sometimes we also need random numbers in our programs to choose a subset of real data (sampling). NumPy can easily produce arrays of wanted shape filled with random numbers. Below are few examples." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.random.random((3,4)) # Elements are uniformly distributed from half-open interval [0.0,1.0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.random.normal(0, 1, (3,4)) # Elements are normally distributed with mean 0 and standard deviation 1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.random.randint(-2, 10, (3,4)) # Elements are uniformly distributed integers from the half-open interval [-2,10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Sometimes it is useful to be able to recreate exactly the same data in every run of our program. For example, if there is a bug in our program, which manifests itself only with certain input, then to debug our program it needs to behave deterministically. We can create random numbers deterministically, if we always start from the same starting point. This starting point is usually an integer, and we call it a *seed*. Example of use:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.random.seed(0)\n", "print(np.random.randint(0, 100, 10))\n", "print(np.random.normal(0, 1, 10))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "If you run the above cell multiple times, it will always give the same numbers, unlike the earlier examples. Try rerunning them now!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "The call to `np.random.seed` initializes the *global* random number generator. The calls `np.random.random`, `np.random.normal`, etc all use this global random number generator. It is however possible to create new random number generators, and use those to sample random numbers from a distribution. Example on usage:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "new_generator = np.random.RandomState(seed=123) # RandomState is a class, so we give the seed to its constructor\n", "new_generator.randint(0, 100, 10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "You will see these used later in the materials and in the exercises, just so we can agree what the random input data is. How else could we agree whether result is correct or not, if we can't agree what the input is!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Array types and attributes\n", "\n", "An array has several attributes: `ndim` tells the number of dimensions, `shape` tells the size in each dimension, `size` tells the number of elements, and `dtype` tells the element type. Let's create a helper function to explore these attributes:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "def info(name, a):\n", " print(f\"{name} has dim {a.ndim}, shape {a.shape}, size {a.size}, and dtype {a.dtype}:\")\n", " print(a)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b=np.array([[1,2,3], [4,5,6]])\n", "info(\"b\", b)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "c=np.array([b, b]) # Creates a 3-dimensional array\n", "info(\"c\", c)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d=np.array([[1,2,3,4]]) # a row vector\n", "info(\"d\", d)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note above how Python printed the three dimensional array. The general rules of printing an n-dimensional array as a nested list are:\n", "\n", "* the last dimension is printed from left to right,\n", "* the second-to-last is printed from top to bottom,\n", "* the rest are also printed from top to bottom, with each slice separated from the next by an empty line." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Indexing, slicing and reshaping" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Indexing\n", "One dimensional array behaves like the list in Python:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a=np.array([1,4,2,7,9,5])\n", "print(a[1])\n", "print(a[-2])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "For multi-dimensional array the index is a comma separated tuple instead of a single integer:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b=np.array([[1,2,3], [4,5,6]])\n", "print(b)\n", "print(b[1,2]) # row index 1, column index 2\n", "print(b[0,-1]) # row index 0, column index -1" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# As with lists, modification through indexing is possible\n", "b[0,0] = 10\n", "print(b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Note that if you give only a single index to a multi-dimensional array, it indexes the first dimension of the array, that is the rows. For example:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(b[0]) # First row\n", "print(b[1]) # Second row" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Slicing\n", "Slicing works similarly to lists, but now we can have slices in different dimensions:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(a)\n", "print(a[1:3])\n", "print(a[::-1]) # Reverses the array" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(b)\n", "print(b[:,0])\n", "print(b[0,:])\n", "print(b[:,1:])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We can even assign to a slice:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "b[:,1:] = 7\n", "print(b)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "A common idiom is to extract rows or columns from an array:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print(b[:,0]) # First column\n", "print(b[1,:]) # Second row" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Reshaping\n", "\n", "When an array is reshaped, its number of elements stays the same, but they are reinterpreted to have a different shape. An example of this is to interpret a one dimensional array as two dimension array:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "a=np.arange(9)\n", "anew=a.reshape(3,3)\n", "info(\"anew\", anew)\n", "info(\"a\", a)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "d=np.arange(4) # 1d array\n", "dr=d.reshape(1,4) # row vector\n", "dc=d.reshape(4,1) # column vector\n", "info(\"d\", d)\n", "info(\"dr\", dr)\n", "info(\"dc\", dc)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "