{
"nbformat": 4,
"nbformat_minor": 0,
"metadata": {
"kernelspec": {
"display_name": "Python 3",
"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.8.5"
},
"colab": {
"name": "Week2_1_python.ipynb",
"provenance": [],
"collapsed_sections": []
}
},
"cells": [
{
"cell_type": "markdown",
"metadata": {
"id": "l7LRbQ1qeHqS"
},
"source": [
"# Week 2 Coding Lecture 1: For Loops\n",
"In this lecture, we will discuss loops: A programming concept that allows you to repeate a block of code many times. \n",
"\n",
"Suppose that we want to make the row vector $x = \\begin{pmatrix} 0 & 1 & 2 & 3 & 4 \\end{pmatrix}$ as an array in python. We know at least two very easy ways to do so. We could write it out like this: "
]
},
{
"cell_type": "code",
"metadata": {
"id": "HWfbQJApeHqa",
"outputId": "b7966dcf-7fe7-4c9b-e36f-ade4727df983"
},
"source": [
"import numpy as np\n",
"x = np.array([[0, 1, 2, 3, 4]])\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0 1 2 3 4]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "0un4dU4peHqc"
},
"source": [
"or we could use the `arange` function like this: "
]
},
{
"cell_type": "code",
"metadata": {
"id": "CVqpBSZTeHqc",
"outputId": "676d2790-43c9-4d02-bea1-5d8dfc056aa7"
},
"source": [
"x = np.arange(0, 5)\n",
"x = np.reshape(x, (1, -1))\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0 1 2 3 4]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "NtP46uhPeHqd"
},
"source": [
"(Note that the reshape command is necessary because we wanted a $1\\times 5$ row vector instead of a 1D array.) "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "APV9_zDBeHqd"
},
"source": [
"But suppose that we did not know about the `arange` function and did not want to type out the whole vector. (I will mostly stick to short vectors in these notes because they don't fill the entire page when printed, but you can imagine why we wouldn't want to type out the entire vector if there were 1,000 or 1,000,000 entries instead of 5.) Another approach would be to make an \"empty vector\" x and then fill in the correct entries. By \"empty vector\", I mean a vector of the appropriate size but without the correct values. \n",
"\n",
"There are a few commands in the `numpy` package to construct arrays. Two of the most useful are `zeros` and `ones`. The syntax for these functions is fairly straightforward. For instance, `np.zeros((m, n))` produces a 2D array with `m` rows and `n` columns where all of the entries are zero. `np.zeros(m)` produces a 1D array with `m` entries, all of which are zero. "
]
},
{
"cell_type": "code",
"metadata": {
"id": "rXLk6D6ieHqd",
"outputId": "5624c1ee-495d-4091-ddaf-3473e75d1db5"
},
"source": [
"print(np.zeros((3, 4)))"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 0. 0. 0.]\n",
" [0. 0. 0. 0.]\n",
" [0. 0. 0. 0.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "P7kfvzR8eHqe",
"outputId": "b26f0bf9-d81b-4dfc-a9d7-4e76742a8ea2"
},
"source": [
"print(np.zeros(5))"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[0. 0. 0. 0. 0.]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "3QaEPrVOeHqe"
},
"source": [
"The `ones` command works exactly the same way, but it produces an array whose entries are all ones. "
]
},
{
"cell_type": "code",
"metadata": {
"id": "-vMslTPXeHqe",
"outputId": "bd14f9c5-be74-49fd-88f6-be47532ca65e"
},
"source": [
"print(np.ones((3, 4)))"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. 1. 1. 1.]\n",
" [1. 1. 1. 1.]\n",
" [1. 1. 1. 1.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "DVBEFnWueHqf",
"outputId": "128f0a79-9d09-4e94-ebf6-665d94068586"
},
"source": [
"print(np.ones(5))"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[1. 1. 1. 1. 1.]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "7rpgndmVeHqg"
},
"source": [
"If we want to make an empty vector of the same size as `x`, we can therefore use "
]
},
{
"cell_type": "code",
"metadata": {
"id": "pqybVigfeHqg",
"outputId": "b186d94b-64a3-4e99-fb29-32d7a7f2c49e"
},
"source": [
"x = np.zeros((1, 5))\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 0. 0. 0. 0.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "-60HPCxceHqh"
},
"source": [
"Now that we have an empty vector to work with, we can fill in the entries we want. \n",
"\n",
"**Side Note:** I will call this an \"empty\" vector in this class, but that name is not quite right. There is another command in the `numpy` package called `empty` that creates an array of a given size without filling in any values. The entries of the resulting array will be determined by whatever happened to be in memory before. The `empty` function is faster than either `zeros` or `ones`, and so it is often used in highly optimized code. However, it tends to cause problems that are a nightmare to debug. I do not recommend that you use it here. \n",
"\n",
"In particular, we can use "
]
},
{
"cell_type": "code",
"metadata": {
"id": "i18iafyTeHqh",
"outputId": "b5d94863-ef96-4599-d881-9f4ee24ab819"
},
"source": [
"x[0, 0] = 0\n",
"x[0, 1] = 1\n",
"x[0, 2] = 2\n",
"x[0, 3] = 3\n",
"x[0, 4] = 4\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 1. 2. 3. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "b6AgEbH-eHqi"
},
"source": [
"We have now created exactly the same vector as in the previous section, albeit with much more typing. The step where we make an empty vector is called *initialization* and we say that we *initialized* the vector `x`. This tells python to set aside a block of memory large enough to hold a $1\\times 5$ vector. \n",
"\n",
"It's worth noting that this initialization step is not always necessary in other languages (particularly MATLAB), but it is required here. The reason is that we can't change the size of a numpy array once it is created, so we have to make sure that we have an array of the correct size. (There is a significantly worse alternative: Instead of setting each entry of `x`, we could repeatedly use the `append` function to add entries to the end of `x`. Note that this involves repeatedly copying the array.) "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "dzFY81TIeHqi"
},
"source": [
"As you might imagine, code like that in the above cell is not very practical. This is already a tedious amount of typing with only 5 entries and would be much worse with 1,000 or 1,000,000. In addition, it is easy to make mistakes with this much code and harder to hunt them down if we find out later that we made the wrong vector. It is also tedious to make changes to this code. If we later decide that each entry of `x` should be twice as big, we would have to change 5 lines of code (or 1,000 or 1,000,000 if our vector were larger). \n",
"\n",
"However, these five lines are almost exactly the same. This suggests that we should be able to avoid most of this typing. If we could tell python the pattern for one of these lines, then we could hopefully just tell python to repeat the pattern five times. \n",
"\n",
"This is exactly what a `for` loop is for. If you want to repeat the same block of code several times, you can use the python `for` keyword. For example, to repeat some code five times, we could write the code"
]
},
{
"cell_type": "code",
"metadata": {
"id": "xMXqnAZieHql"
},
"source": [
"for k in range(5):\n",
" # Some code to be repeated\n",
" pass"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "nOcYBQ58eHql"
},
"source": [
"(The word pass is not part of the syntax, but I had to include it because for loops are not allowed to only have comments in them. The keyword `pass` just means \"do nothing\".) In general, the first line should say \n",
"\n",
"`for variable_name in range(number_of_steps):\n",
" other code`\n",
" \n",
"Note that the words `for` and `in` are required, as is the `:` at the end of the first line. It is also very important to indent the code after the loop. This indentation is what tells python which lines of code are supposed to be repeated. Other languages like MATLAB or C use other keywords or symbols to show the end of a block of code, but python uses levels of indentation. This can take a little getting used to. PyCharm will handle the indentation for you, but it is important to be consistent. If one line of code is indented by 1 space and another line is indented by 2 spaces, the code will either not run or not do what you want. Moreover, different operating systems and text editors handle tabs differently, so a tab may or may not be the equivalent of some number of spaces. I recommend that you go into PyCharm settings, then in the menu go to Editor->Code Style->Python and make sure that \"Use tab character\" is *un*checked. This will make PyCharm automatically convert tabs into spaces. \n",
"\n",
"The word `for` is a reserved word that tells python this is a for loop. The variable `k` is called a *loop counter* or a *loop index* or a *loop variable*, depending on context. You can think of it as keeping track of what step you are on. When you run this code, it will behave exactly as if you had instead typed "
]
},
{
"cell_type": "code",
"metadata": {
"id": "pFyd6h7TeHqo"
},
"source": [
"k = 0\n",
"# Some code to be repeated\n",
"k = 1\n",
"# Some code to be repeated\n",
"k = 2\n",
"# Some code to be repeated\n",
"k = 3\n",
"# Some code to be repeated\n",
"k = 4\n",
"# Some code to be repeated"
],
"execution_count": null,
"outputs": []
},
{
"cell_type": "markdown",
"metadata": {
"id": "CuL2EqRaeHqp"
},
"source": [
"As you can see, the code is repeated five times and the variable `k` tells us which step we are on (counting from 0, jut like indices). For a particularly silly example, the following code prints out the number 8 ten times: "
]
},
{
"cell_type": "code",
"metadata": {
"id": "sAwWBvBSeHqq",
"outputId": "a84d91dc-555e-4724-9d96-65ca484b2911"
},
"source": [
"for k in range(10):\n",
" print(8)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"8\n",
"8\n",
"8\n",
"8\n",
"8\n",
"8\n",
"8\n",
"8\n",
"8\n",
"8\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1A7lyGW5eHqq"
},
"source": [
"We can also use the loop variable in the code to be repeated. This is useful for making the code do slightly different things each time. As another example, the following code prints out the first four perfect squares: "
]
},
{
"cell_type": "code",
"metadata": {
"id": "Kf4-PZFheHqq",
"outputId": "7a8ea9c5-3a6c-46b7-bc2f-bbf85d724d5a"
},
"source": [
"for k in range(4):\n",
" print(k ** 2)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"0\n",
"1\n",
"4\n",
"9\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "YTkfZMzDeHqr"
},
"source": [
"There is nothing special about the name `k`. You can call your loop index whatever you want. I will usually stick to things like `i`, `j`, `k`, `m` or `n` because those are common index variables in mathematics, but some people prefer more descriptive names like `counter` or `index` or `loop_var`. This is really just a matter of taste. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Myx4MBIgeHqr"
},
"source": [
"Getting back to our previous example, after initializing `x` we want to repeat the code `x[0, some_number] = some_number` over and over again. The value of `some_number` starts at 0, then increases to 1, then 2, then 3 and then 4. This is exactly what our loop counter did. We can rewrite our original code as "
]
},
{
"cell_type": "code",
"metadata": {
"id": "hCMjEMm7eHqr",
"outputId": "30e9030a-91f8-4c73-d14e-892a584c23b1"
},
"source": [
"x = np.zeros((1, 5))\n",
"for j in range(5):\n",
" x[0, j] = j\n",
" \n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 1. 2. 3. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "iGanzUcUeHqs"
},
"source": [
"Remember, what python is really doing when we run this is "
]
},
{
"cell_type": "code",
"metadata": {
"id": "_qCZ6PYteHqs",
"outputId": "adc25c3d-a796-494a-ad6d-25b018e612e8"
},
"source": [
"x = np.zeros((1, 5))\n",
"j = 0\n",
"x[0, j] = j\n",
"j = 1\n",
"x[0, j] = j\n",
"j = 2\n",
"x[0, j] = j\n",
"j = 3\n",
"x[0, j] = j\n",
"j = 4\n",
"x[0, j] = j\n",
"\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 1. 2. 3. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "j2klG7B2eHqs"
},
"source": [
"which is exactly what we wanted. \n",
"\n",
"This is an incredibly common coding construct, and we will use it many times in this course. First, you initialize a variable that will be used for storing your results. Next, you use a loop to make many related calculations. The result of each of these calculations gets stored in one of the entries of your original variable. In our case, the \"calculation\" was just finding the value of `j`, but in real problems it will often be substantially more complicated. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "hyzo05o-eHqs"
},
"source": [
"As another example, suppose that we wanted to create the vector \n",
"\n",
"$$x = \\begin{pmatrix} 1 \\\\ 1.1 \\\\ 1.2 \\\\ 1.3 \\\\ 1.4 \\end{pmatrix}.$$\n",
"\n",
"Of course, we already know a shortcut with the `arange` or `linspace` functions, but suppose that we didn't have those options. We could do almost the same thing as before. (Notice that `x` is a column vector this time.)"
]
},
{
"cell_type": "code",
"metadata": {
"id": "3NInLZGoeHqs",
"outputId": "ae46fc06-2a7a-420e-df9a-052646338b29"
},
"source": [
"x = np.zeros((5, 1))\n",
"x[0, 0] = 1\n",
"x[1, 0] = 1.1\n",
"x[2, 0] = 1.2\n",
"x[3, 0] = 1.3\n",
"x[4, 0] = 1.4\n",
"\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. ]\n",
" [1.1]\n",
" [1.2]\n",
" [1.3]\n",
" [1.4]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "MXKFGBxTeHqt"
},
"source": [
"Once again, we are repeating almost the same line of code five times, so we should be able to use a loop to make this simpler. However, this problem as an extra wrinkle because there are two different numbers that change in each line: The row index of `x` (which takes the values 0, 1, 2, 3 and 4) and the value being assigned (which takes the values 1, 1.1, 1.2, 1.3 and 1.4). A loop only keeps track of one loop variable for us. This means that we will need to deal with the other value on our own. Probably the easiest way to approach this problem is to write something like: \n",
"\n",
"`x = np.zeros((5, 1))\n",
"for k in range(5):\n",
" x[k, 0] = # Some value`\n",
"\n",
"This means that the loop will keep track of the index of `x` (we get to use `k` as the index), but we will still have to figure out what \"some value\" should be at each step. A standard way to do this is to start by defining a variable as whichever value we want in the first step, then incrementing this variable at every step of the loop. That is, "
]
},
{
"cell_type": "code",
"metadata": {
"id": "NFHoi4creHqt",
"outputId": "f26856c9-2241-40c8-e151-548823763412"
},
"source": [
"x = np.zeros((5, 1))\n",
"x_value = 1;\n",
"for k in range(5):\n",
" x[k, 0] = x_value\n",
" x_value = x_value + 0.1\n",
" \n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. ]\n",
" [1.1]\n",
" [1.2]\n",
" [1.3]\n",
" [1.4]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "bzeboeCOeHqt"
},
"source": [
"The line `x_value = 1` is also called *initialization*. Here we are initializing the variable `x_value`. The line `x_value = x_value + 0.1` increments this value by 0.1 after each step. The first time through the loop, `x_value` is 1 (as desired). The second time through the loop it is 1.1, then 1.2, then 1.3, then 1.4. \n",
"\n",
"This is another very common pattern in programming. If you have a variable that changes at each step in a loop, you can initialize it to some useful starting value, then update it at the end of each loop step. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "9b3Z34bleHqu"
},
"source": [
"The incrementing approach described above is a standard approach and should almost always be your first choice. In this case, however, we have another interesting option: We can find a formula for `x_value` interms of `k`. In particular, we can use "
]
},
{
"cell_type": "code",
"metadata": {
"id": "mjj3pLbDeHqu",
"outputId": "97e0ef2f-ccfe-40f3-dc3a-d87537023f21"
},
"source": [
"x = np.zeros((5, 1))\n",
"for k in range(5):\n",
" x[k, 0] = 1 + 0.1 * k\n",
" \n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. ]\n",
" [1.1]\n",
" [1.2]\n",
" [1.3]\n",
" [1.4]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "wC4XbmYoeHqu"
},
"source": [
"You should convince yourself of why this makes exactly the same vector as in the previous example. \n",
"\n",
"It is not always easy to find a formula like this, but if you happen to notice one then this is often a good approach. It tends to be much harder to think up and possibly a little bit slower, but often doesn't accumulate as much rounding error. As with many things, unless you are extremely concerned about efficiency, which method you choose is largely a matter of taste. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "MVN4UJoleHqu"
},
"source": [
"It is worth noting that the `range(5)` part of our `for` loops can be generalized quite a bit. For example, we can replace it with the syntax `range(start, stop, step)`, like this: "
]
},
{
"cell_type": "code",
"metadata": {
"id": "NapSzZ9-eHqu",
"outputId": "3a0023a7-817c-487b-cadf-e5a00361899d"
},
"source": [
"for k in range(2, 12, 3):\n",
" print(k)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"2\n",
"5\n",
"8\n",
"11\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "1Xo9ZmWxeHqv"
},
"source": [
"This syntax makes the loop variable begin at `start` and increase in increments of `step` until just before it reaches `stop`. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "dc9bqtTQeHqv"
},
"source": [
"Actually, the `range` part of our `for` loops is not a part of the general python `for` loop. We can replace `range` with a wide variety of different statements. A discussion of what exactly `range` does or what statements are allowed to replace it is well beyond the scope of this class. That said, it is useful to know that we can replace the `range` statement with a 1D numpy array. (Technically, you can also replace it with a 2D array, but the results are probably not what you would expect.) As an example, we could write the code "
]
},
{
"cell_type": "code",
"metadata": {
"id": "_j9rf36qeHqv",
"outputId": "d2de302c-241f-499d-c6e4-4ed1c592a9ab"
},
"source": [
"x = np.exp(np.arange(3, 10))\n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[ 20.08553692 54.59815003 148.4131591 403.42879349 1096.63315843\n",
" 2980.95798704 8103.08392758]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "code",
"metadata": {
"id": "d0BBOw7feHqw",
"outputId": "2e708039-eaff-4b74-fcfe-056d97504305"
},
"source": [
"for k in x:\n",
" print(k)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"20.085536923187668\n",
"54.598150033144236\n",
"148.4131591025766\n",
"403.4287934927351\n",
"1096.6331584284585\n",
"2980.9579870417283\n",
"8103.083927575384\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "vnFGEWjNeHqw"
},
"source": [
"As you can see, when we use a 1D numpy array instead of `range`, the loop variable takes on the value of each entry of the array in turn. \n",
"\n",
"We could therefore have used the `np.arange` function in place of `range` in all of our previous loops. As an example, "
]
},
{
"cell_type": "code",
"metadata": {
"id": "M0JTrsnWeHqx",
"outputId": "7dc900b6-d42c-477c-ff97-7e13e5e96301"
},
"source": [
"x = np.zeros((1, 5))\n",
"for j in np.arange(5):\n",
" x[0, j] = j\n",
" \n",
"print(x)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[0. 1. 2. 3. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "Ezmr5pgieHqy"
},
"source": [
"This is actually usually a bad idea, but explaining why is beyond the scope of these notes. For now, just use `range` in your `for` loops whenever possible. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "zNE2HkwWeHqy"
},
"source": [
"## Nested loops\n",
"Now let's try something slightly more complicated. Intead of filling out the entries of a vector, we will fill out the entries of a matrix. For starters, we will try to create the matrix \n",
"\n",
"$$A = \\begin{pmatrix} 1 & 1 & 1 \\\\ 2 & 2 & 2 \\\\ 3 & 3 & 3 \\\\ 4 & 4 & 4 \\end{pmatrix}.$$\n",
"\n",
"If we do not want to type the whole matrix out (and once the matrix gets large that would be very impractical) then we can use a similar approach to the last section. For instance, we could type "
]
},
{
"cell_type": "code",
"metadata": {
"id": "bUNFanJTeHqy",
"outputId": "58a20248-d2f7-4fc5-89df-1a2cb2cda5de"
},
"source": [
"A = np.zeros((4, 3))\n",
"A[0, :] = 1\n",
"A[1, :] = 2\n",
"A[2, :] = 3\n",
"A[3, :] = 4\n",
"\n",
"print(A)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. 1. 1.]\n",
" [2. 2. 2.]\n",
" [3. 3. 3.]\n",
" [4. 4. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "SwofKFJteHqy"
},
"source": [
"This initializes the matrix (creates a zero matrix of the right size), then sets each row in turn. As in our previous examples, we have almost exactly repeated the same line of code several times, which means that this is a prime candidate for a loop. Following the same logic as above, we can write "
]
},
{
"cell_type": "code",
"metadata": {
"id": "D_HlKOQaeHqy",
"outputId": "0561a4e6-0b9d-4b75-d7c8-6f2c1a72059d"
},
"source": [
"A = np.zeros((4, 3))\n",
"for i in range(4):\n",
" A[i, :] = i + 1\n",
" \n",
"print(A)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[1. 1. 1.]\n",
" [2. 2. 2.]\n",
" [3. 3. 3.]\n",
" [4. 4. 4.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "G73QYhTIeHqz"
},
"source": [
"This produces the desired matrix. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "kZAEnwTPeHqz"
},
"source": [
"Now suppose that we want to make the matrix \n",
"\n",
"$$A = \\begin{pmatrix} 1 & 2 & 3 \\\\ 4 & 5 & 6 \\\\ 7 & 8 & 9 \\\\ 10 & 11 & 12 \\end{pmatrix}.$$\n",
"\n",
"Following a similar strategy, we could initialize our matrix and then fill in each value one at a time. "
]
},
{
"cell_type": "code",
"metadata": {
"id": "NQdUHcXueHqz",
"outputId": "ef5f5966-8554-497f-b6a0-04431c8ab155"
},
"source": [
"A = np.zeros((4, 3))\n",
"A[0, 0] = 1\n",
"A[0, 1] = 2\n",
"A[0, 2] = 3\n",
"\n",
"A[1, 0] = 4\n",
"A[1, 1] = 5\n",
"A[1, 2] = 6\n",
"\n",
"A[2, 0] = 7\n",
"A[2, 1] = 8\n",
"A[2, 2] = 9\n",
"\n",
"A[3, 0] = 10\n",
"A[3, 1] = 11\n",
"A[3, 2] = 12\n",
"\n",
"print(A)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[ 1. 2. 3.]\n",
" [ 4. 5. 6.]\n",
" [ 7. 8. 9.]\n",
" [10. 11. 12.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "5by5Z-HteHqz"
},
"source": [
"Once again, this seems like a good candidate for a loop, but this time we have three values that are changing: The row index, the column index and the value on the right hand side. One approach might be to have one `for k in range(12)` loop and perform each of these twelve steps, but it would end up being pretty messy to keep track of the necessary variables. A better method is to think of each block of three commands as a single piece of code. We really repeat each of those blocks three times. This means that we want something like \n",
"\n",
"`A = np.zeros((4, 3))\n",
"for i in range(4):\n",
" A[i, 0] = some_number\n",
" A[i, 1] = some_other_number\n",
" A[i, 2] = some_different_number`\n",
" \n",
"Look at the three lines inside this loop. They are really just the same line of code repeated three times with slightly different numbers, which makes them another good candidate for a `for` loop. We can therefore write \n",
"\n",
"`A = np.zeros((4, 3))\n",
"for i in range(4):\n",
" for j in range(3):\n",
" A[i, j] = some_number`\n",
" \n",
"Now all we need to do is keep track of `some_number`. Here is one of the more straightforward methods (very similar to one of the examples from the single `for` loop section): "
]
},
{
"cell_type": "code",
"metadata": {
"id": "sOeQ2Tz2eHqz",
"outputId": "e32eba18-3085-4718-8f2f-5e559d377968"
},
"source": [
"A = np.zeros((4, 3))\n",
"A_value = 1\n",
"for i in range(4):\n",
" for j in range(3):\n",
" A[i, j] = A_value\n",
" A_value = A_value + 1\n",
" \n",
"print(A)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[[ 1. 2. 3.]\n",
" [ 4. 5. 6.]\n",
" [ 7. 8. 9.]\n",
" [10. 11. 12.]]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "vsgeANideHq0"
},
"source": [
"Notice the indentation - it is very important. All the code in the first `for` loop is indented once (in this notebook, one indentation is four spaces). All the code in the second `for` loop is indented an additional time. When the loops are over, we return to the original indentation. \n",
"\n",
"You should carefully study this example until you can clearly see why it creates the matrix we are looking for and what order everything happens in. In particular, it may help to have python print out the values of `i`, `j` and `A_value` at every step. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "GgWu7AmNeHq0"
},
"source": [
"As you can see, code inside a `for` loop does not have to be just one line; it can be arbitrarilly complicated. In particular, we are allowed to put loops inside other loops. These are called *nested for loops*. The loop involving `i` (the one where the `for` is not indented) is called the *outer loop*. The loop involving `j` (the one where the `for` is indented once) is called the *inner loop*. We will often need two loops nested together like this example, but it is probably wise to avoid going any deeper than this. Python doesn't have any limit on how many loops you can nest (well, it does, but the limit is very, very large), but you will find that your code becomes both very slow and very hard to understand once you have too many nested loops. "
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "K1lP2KhheHq0"
},
"source": [
"# Fibonacci numbers\n",
"Now let's try a more interesting mathematical example. We will try to calculate some Fibonacci numbers. The Fibonacci numbers $F_n$ are defined by the following recurrence relation: \n",
"\n",
"$$F_n = F_{n-1} + F_{n-2},$$\n",
"\n",
"where $F_1 = F_2 = 1$. That is, the $n$th Fibonacci number is the sum of the previous two. We will start by calculating the first 20 Fibonacci numbers and saving them in a vector. This is the first example where we really do need a loop: There is no python shortcut like `np.arange` for calculating Fibonacci numbers. (There is actually a relatively simple formula for $F_n$, but it is not easy to derive.)\n",
"\n",
"We will use the same basic approach as before: Initialize an array for our Fibonacci numbers, then use a loop to fill in each entry of the array in turn. The basic skeleton is \n",
"\n",
"`fib = np.zeros(20)\n",
"for n in some_range:\n",
" fib(n) = # calculate the nth Fibonacci number`\n",
" \n",
"(Note that I chose to use a 1D array instead of a row or column vector for `fib`. Any of those choices would be fine.) There are quite a few variations we could use to write this code, but probably the most straightforward way is to essentially copy the definition into our loop like so: \n",
"\n",
"`fib = np.zeros(20)\n",
"fib[0] = 1\n",
"fib[1] = 1\n",
"for n in some_range:\n",
" fib[n] = fib[n - 1] + fib[n - 2]`\n",
" \n",
"Notice that we included the first two Fibonacci numbers in the initialization step. This is just part of the definition of the problem. We can now figure out what the loop variable is supposed to be. We ahve already filled out the first two values, so the index `n` should start at 2, and we want to fill out everything up to index 19 (the 20th entry), so `n` should end at 19. We therefore need"
]
},
{
"cell_type": "code",
"metadata": {
"id": "oYVq94MaeHq0",
"outputId": "138f0724-13cf-46db-ba33-23f34ee0aca5"
},
"source": [
"fib = np.zeros(20)\n",
"fib[0] = 1\n",
"fib[1] = 1\n",
"\n",
"for n in range(2, 20):\n",
" fib[n] = fib[n - 1] + fib[n - 2]\n",
" \n",
"print(fib)"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"[1.000e+00 1.000e+00 2.000e+00 3.000e+00 5.000e+00 8.000e+00 1.300e+01\n",
" 2.100e+01 3.400e+01 5.500e+01 8.900e+01 1.440e+02 2.330e+02 3.770e+02\n",
" 6.100e+02 9.870e+02 1.597e+03 2.584e+03 4.181e+03 6.765e+03]\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "EWpPNpo5eHq0"
},
"source": [
"(As a side note, the `e` in the printout is scientific notation. It stands for \"times ten to the power of\", so something like `6.765e+03` means $6.765\\times 10^3$.)\n",
"\n",
"Suppose that we decide to calculate the first 200 numbers instead. We can use almost exactly the same code, but we have to make two changes: We need to change the range from `range(2, 20)` to `range(2, 200)` and we need to change the initialization line so that `fib` is the right size. "
]
},
{
"cell_type": "code",
"metadata": {
"id": "IOXipBfeeHq1",
"outputId": "9e6a93ef-48e0-4799-b058-d0d71869421a"
},
"source": [
"fib = np.zeros(200)\n",
"fib[0] = 1\n",
"fib[1] = 1\n",
"\n",
"for n in range(2, 200):\n",
" fib[n] = fib[n - 1] + fib[n - 2]\n",
" \n",
"print(fib[-1])"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"2.8057117299251016e+41\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "7o65mKzYeHq1"
},
"source": [
"I only printed out the last value of `fib` because the entire array takes a lot of space. \n",
"\n",
"Note that forgetting to change either 20 to a 200 causes problems. If we forget to change the 20 in the `range` function, then the loop only calculates the first 20 Fibonacci numbers and the rest of the array is left as zeros. If we forget to change the 20 in the initialization step, then python will throw an error when `n` gets to 20 and we try to set the value of `fib[20]`, because `fib` does not have a 21st entry. \n",
"\n",
"Since both of these numbers are always supposed to be the same, it is a good idea to make a variable for the number of Fibonacci numbers that we are calculating. "
]
},
{
"cell_type": "code",
"metadata": {
"id": "Uy3rv8f-eHq1",
"outputId": "fd275b93-183e-44b4-eb98-9921589bf733"
},
"source": [
"total_numbers = 200\n",
"fib = np.zeros(total_numbers)\n",
"fib[0] = 1\n",
"fib[1] = 1\n",
"\n",
"for n in range(2, total_numbers):\n",
" fib[n] = fib[n - 1] + fib[n - 2]\n",
" \n",
"print(fib[-1])"
],
"execution_count": null,
"outputs": [
{
"output_type": "stream",
"text": [
"2.8057117299251016e+41\n"
],
"name": "stdout"
}
]
},
{
"cell_type": "markdown",
"metadata": {
"id": "cnIN_2_ueHq1"
},
"source": [
"This way, we only need to change the value of `total_numbers` and the rest of the code will work as desired. \n",
"\n",
"We have successfully calculated the first 20 (or 200, or any number) Fibonacci numbers. In fact, this code is pretty close to the best solution we can get for this problem. However, imagine that we modify the problem slightly. Suppose that instead of the first 20 numbers, we want to find all the Fibonacci numbers that are less than 1,000,000. Now our solution doesn't work. The issue is that we don't know beforehand how many numbers we will need, so we don't know what our loop variable should be. \n",
"\n",
"To solve this problem, we want to start calculating Fibonacci numbers like before, but tell python to stop when `fib[n]` reaches 1,000,000. To write this in code, we will need a new concept that tells python \"if `fib[n]` is bigger than 1,000,000, stop the loop.\" We will talk about this concept in the next lecture. "
]
}
]
}