From b1fa2d650d49fe29b3fb82228857c3c225d0568c Mon Sep 17 00:00:00 2001
From: Samuel Koovely <samuel.koovely@math.uzh.ch>
Date: Fri, 16 Sep 2022 20:07:36 +0200
Subject: [PATCH] Upload New File

---
 tutorials/ES1/ES1_Tutorial1.ipynb | 1254 +++++++++++++++++++++++++++++
 1 file changed, 1254 insertions(+)
 create mode 100644 tutorials/ES1/ES1_Tutorial1.ipynb

diff --git a/tutorials/ES1/ES1_Tutorial1.ipynb b/tutorials/ES1/ES1_Tutorial1.ipynb
new file mode 100644
index 0000000..59c61e8
--- /dev/null
+++ b/tutorials/ES1/ES1_Tutorial1.ipynb
@@ -0,0 +1,1254 @@
+{
+ "cells": [
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# Tutorial 1\n",
+    "\n",
+    "This tutorial is based on a tutorial shared on https://github.com/CambridgeUniversityPress/FirstCourseNetworkScience.\n",
+    "\n",
+    "\n",
+    "This tutorial demonstrates the features of the Python language and of Jupyter Notebook that are used in the examples and tutorials of the textbook. Special attention is given to the most important data types used in data analysis workflows, as well as common idioms and patterns employed in these use-cases. This appendix may be especially useful for readers more experienced in programming languages other than Python.\n",
+    "\n",
+    "Contents:\n",
+    "\n",
+    "1. [Jupyter Notebook](#1.-Jupyter-Notebook)\n",
+    "2. [Conditionals](#2.-Conditionals)\n",
+    "3. [Lists](#3.-Lists)\n",
+    "4. [Loops](#4.-Loops)\n",
+    "5. [Tuples](#5.-Tuples)\n",
+    "6. [Dictionaries](#6.-Dictionaries)\n",
+    "7. [Combining Data Types](#7.-Combining-Data-Types)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 1. Jupyter Notebook\n",
+    "\n",
+    "Even if you're well-versed in Python, you may not have used Jupyter Notebook before. The main idea is that we can mix text and code, and that code is executed in \"cells.\" By clicking on a cell and pressing Shift + Enter, you execute the cell and move to the next cell. Ctrl + Enter executes the cell but does not move to the next cell. You can run many cells at once by using the different options in the \"Cell\" menu.\n",
+    "\n",
+    "Try executing the code in the next cell and observe that the output is printed below the cell."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "print('Hello from Jupyter')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 1.1 Printing and inspecting variables\n",
+    "\n",
+    "In Jupyter notebooks, we have two different ways of inspecting variables. Python's `print()` function is useful as always:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "my_str = 'Hello'\n",
+    "my_int = 16\n",
+    "\n",
+    "print(my_str)\n",
+    "print(my_int)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can also just execute a cell with the name of a variable:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "my_str"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The big difference here between the two approaches is that `print()` statements can output multiple items per cell, while the latter approach will only display the last variable named. Observe:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "my_str\n",
+    "my_int"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "As opposed to the first example using `print()`, this only outputs the last value."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## Nota Bene\n",
+    "One key advantage of presenting information in this notebook format is that it allows you to change and re-run the code cells, then see how the output differs. Don't be afraid to experiment!"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 2. Conditionals\n",
+    "\n",
+    "\"Conditionals\" is a fancy word for if-statements. If you've ever done any programming, you are surely aware of the if-then-else construction. In Python it's done as follows:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "number_of_apples = 5\n",
+    "\n",
+    "if number_of_apples < 1:\n",
+    "    print('You have no apples')\n",
+    "elif number_of_apples == 1:\n",
+    "    print('You have one apple')\n",
+    "elif number_of_apples < 4:\n",
+    "    print('You have a few apples')\n",
+    "else:\n",
+    "    print('You have many apples!')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "You can change `number_of_apples` and re-run the previous cell in order to get the different possible outputs.\n"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 3. Lists\n",
+    "\n",
+    "One of Python's most versatile and ubiquitous data types is the List ([Python documentation](https://docs.python.org/3/library/stdtypes.html#list)). This is an **ordered**, **mutable**, **collection** of **non-unique** items.\n",
+    "\n",
+    "## 3.1 Ordered\n",
+    "\n",
+    "By *ordered*, we mean that the items are addressed by their *index* in the collection:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names = ['Alice', 'Bob', 'Carol', 'Dave']\n",
+    "student_names[1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Indices in Python start at zero, so the head of the list has index 0:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "scrolled": true
+   },
+   "outputs": [],
+   "source": [
+    "student_names[0]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can get the last item in a list by using negative indexing:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names[-1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Lists can also be *sliced* to get a subset of the list items:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names[0:2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names[1:3]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "When slicing from the beginning of the list, or to the end of the list, we can leave out the index:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names[:2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names[2:]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 3.2 Mutable\n",
+    "\n",
+    "By *mutable*, we mean that the list can be changed by adding or removing items. We most often add items to the end of the list with `.append()`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names.append('Esther')\n",
+    "student_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "But we can also add items at any arbitrary index with `.insert()`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names.insert(2, 'Xavier')\n",
+    "student_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can delete items with the `del` keyword:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "del student_names[2]\n",
+    "student_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 3.3 Non-unique\n",
+    "\n",
+    "Note that nothing stops us from repeatedly adding the same name to this list:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names.append('Esther')\n",
+    "student_names.append('Esther')\n",
+    "student_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "If you want a collection where uniqueness is enforced, you should look towards\n",
+    "[sets](https://docs.python.org/3/library/stdtypes.html#set)\n",
+    "or\n",
+    "[dictionaries](https://docs.python.org/3/library/stdtypes.html#dict).\n",
+    "\n",
+    "## 3.4 Collection\n",
+    "\n",
+    "A collection refers to a data type consisting of more than one values. Lists are one type of collection, but there are others such as tuples, sets, and dictionaries.\n",
+    "\n",
+    "When naming your variables that contain lists, you should use plural nouns, *e.g.* `student_names` in the previous example. In contrast, single values should be named with singular nouns, *e.g.* `my_str` in the first section. This helps you and others reading your code keep straight which variables are collections and which are single items, and also helps when writing loops as shown in the next section."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 4. Loops\n",
+    "\n",
+    "If you're coming from another programming language, you're probably aware of more than one type of loop. In Python, we focus on one type of loop in particular: the for-loop. The for-loop iterates through a collection of items, executing its code for each item:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names = ['Alice', 'Bob', 'Carol', 'Dave']\n",
+    "\n",
+    "for student_name in student_names:\n",
+    "    print('Hello ' + student_name + '!')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 4.1 Naming conventions\n",
+    "\n",
+    "Note the naming convention being used in the for-in construction:\n",
+    "\n",
+    "    for student_name in student_names:\n",
+    "    \n",
+    "By using a plural noun for the collection `student_names`, we automatically have good name for the individual items in the collection: `student_name`. The tutorials in this book use this naming convention when possible as it makes clear to the reader which variable is the \"loop variable\" that changes value between iterations of the loop body."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 4.2 Loops, lists, and conditionals\n",
+    "\n",
+    "One extremely common type of task when working with data is the *filtering task*. In abstract, this task involves looping over one collection, checking each item for some criterion, then adding items that meet the criterion to another collection.\n",
+    "\n",
+    "In the following example, we'll create a list of just the \"long\" names from the `student_names` list. Long names are those that contain more than four characters. You will often see and write code that looks like the following in this book's tutorials:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Initialize an empty list and add to it the\n",
+    "# student names containing more than four characters\n",
+    "long_names = []\n",
+    "for student_name in student_names:\n",
+    "    # This is our criterion\n",
+    "    if len(student_name) > 4:\n",
+    "        long_names.append(student_name)\n",
+    "\n",
+    "long_names"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 4.3 Nested loops\n",
+    "\n",
+    "Loops can be \"nested\" inside one another. This often occurs when we want to match up items from one collection to items from the same or another collection. Here let's create a list of all possible pairs of students:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names = ['Alice', 'Bob', 'Carol', 'Dave']\n",
+    "\n",
+    "student_pairs = []\n",
+    "for student_name_0 in student_names:\n",
+    "    for student_name_1 in student_names:\n",
+    "        student_pairs.append(\n",
+    "            (student_name_0, student_name_1)\n",
+    "        )\n",
+    "\n",
+    "student_pairs"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note here that instead of just adding names to the `student_pairs` list, we are adding *tuples* `(student_name, language)`. This means each item in the list is a 2-tuple:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_pairs[0]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We'll talk more about tuples in the next section. The second thing to notice is that we're including pairs with two of the same student. Suppose we wish to exclude those. We can accomplish this by adding an if-statement in the second for-loop to *filter* out those repeats:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_names = ['Alice', 'Bob', 'Carol', 'Dave']\n",
+    "\n",
+    "student_pairs = []\n",
+    "for student_name_0 in student_names:\n",
+    "    for student_name_1 in student_names:\n",
+    "        # This is the criterion we added\n",
+    "        if student_name_0 != student_name_1:\n",
+    "            student_pairs.append(\n",
+    "                (student_name_0, student_name_1)\n",
+    "            )\n",
+    "\n",
+    "student_pairs"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "And now the list has no repeats."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 5. Tuples\n",
+    "\n",
+    "Even experienced Python users often are confused about the difference between tuples and lists, so definitely read this short section even if you have some experience.\n",
+    "\n",
+    "Tuples ([documentation](https://docs.python.org/3/library/stdtypes.html#tuple)) are superficially similar to lists as they are an ordered collection of non-unique items:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade = ('Alice', 'Spanish', 'A-')\n",
+    "student_grade"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade[0]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 5.1 Immutable\n",
+    "\n",
+    "The big difference from lists is that tuples are **immutable**. Each of the following cells should raise an exception."
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "raises-exception"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "student_grade.append('IU Bloomington')"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "raises-exception"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "del student_grade[2]"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "raises-exception"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "student_grade[2] = 'C'"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This immutability makes tuples useful when **index matters**. In this example, the index matters semantically: index 0 is the student's name, index 1 is the course name, and index 2 is their grade in the course. The inability to insert or append items to the tuple means that we are certain that, say, the course name won't move around to a different index.\n",
+    "\n",
+    "## 5.2 Unpacking\n",
+    "\n",
+    "Tuples' immutability makes them useful for *unpacking*. At its simplest, tuple unpacking allows the following:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade = ('Alice', 'Spanish', 'A-')\n",
+    "student_name, subject, grade = student_grade\n",
+    "\n",
+    "print(student_name)\n",
+    "print(subject)\n",
+    "print(grade)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "While occasionally useful on its own, tuple unpacking is most useful when used with loops. Consider the following piece of code, which congratulates students on getting good grades:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grades = [\n",
+    "    ('Alice', 'Spanish', 'A'),\n",
+    "    ('Bob', 'French', 'C'),\n",
+    "    ('Carol', 'Italian', 'B+'),\n",
+    "    ('Dave', 'Italian', 'A-'),\n",
+    "]\n",
+    "\n",
+    "for student_name, subject, grade in student_grades:\n",
+    "    if grade.startswith('A'):\n",
+    "        print('Congratulations', student_name,\n",
+    "              'on getting an', grade,\n",
+    "              'in', subject)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Compare this to the same code using indices:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for student_grade in student_grades:\n",
+    "    if student_grade[2].startswith('A'):\n",
+    "        print('Congratulations', student_grade[0],\n",
+    "              'on getting an', student_grade[2],\n",
+    "              'in', student_grade[1])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Tuple unpacking allows us to easily refer to this structured data by semantic names instead of having to keep the indices straight. The second example, while functionally identical, is more difficult to write and harder still to read."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 6. Dictionaries\n",
+    "\n",
+    "The next type of collection is much different than the previous two, but is among the most powerful tools in Python: the dictionary ([documentation](https://docs.python.org/3/library/stdtypes.html#dict)). The dictionary is an **unordered**, **mutable**, collection of **unique** items. In other languages these are called maps, mappings, hashmaps, hashes, or associative arrays.\n",
+    "\n",
+    "## 6.1 Unordered\n",
+    "\n",
+    "By unordered, we mean that dictionary items aren't referred to by their position, or index, in the collection. Instead, dictionary items have *keys*, each of which is associated with a value. Here's a very basic example:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "foreign_languages = {\n",
+    "    'Alice': 'Spanish',\n",
+    "    'Bob': 'French',\n",
+    "    'Carol': 'Italian',\n",
+    "    'Dave': 'Italian',\n",
+    "}"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here the student names are the keys and the students' foreign language courses are the values. So to see Carol's foreign language, we use the key -- her name -- instead of an index:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "foreign_languages['Carol']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Trying to get the value for a key that does not exist in the dictionary results in a `KeyError`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {
+    "tags": [
+     "raises-exception"
+    ]
+   },
+   "outputs": [],
+   "source": [
+    "foreign_languages['Zeke']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We can check if a particular key is in a dictionary with the `in` keyword:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "'Zeke' in foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "'Alice' in foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Note that keys are case-sensitive:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "'alice' in foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 6.2 Mutable\n",
+    "\n",
+    "We can add, delete, and change entries in a dictionary:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Add an entry that doesn't exist\n",
+    "foreign_languages['Esther'] = 'French'\n",
+    "foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Delete an entry that exists\n",
+    "del foreign_languages['Bob']\n",
+    "foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "# Change an entry that does exist\n",
+    "foreign_languages['Esther'] = 'Italian'\n",
+    "foreign_languages"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 6.3 Unique\n",
+    "Note that the syntax for adding an entry that does not exist and changing an existing entry are the same. When assigning a value to a key in a dictionary, it adds the key if it doesn't exist, or else updates the value for the key if it does exist. As a consequence, keys are necessarily *unique* -- there can't be more than one element with the same key in a dictionary."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 6.4 Looping over dictionaries\n",
+    "\n",
+    "While not performed as often as with lists, it is possible to loop over entries in a dictionary. There are two ways to accomplish this task:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for key in foreign_languages:\n",
+    "    value = foreign_languages[key]\n",
+    "    print(key, 'is taking', value)"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for key, value in foreign_languages.items():\n",
+    "    print(key, 'is taking', value)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here I'm using variables named `key` and `value` to show the general principle. When you write loops over dictionaries in your own code, you should use descriptive names as opposed to `key` and `value`."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 6.5 Dictionaries as records\n",
+    "\n",
+    "In `foreign_languages` we have paired data -- every name is associated with a subject. Dictionaries are also often used to contain several different data about a single entity. To illustrate this subtle difference, let's take a look at one item from `student_grades`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade = ('Alice', 'Spanish', 'A')"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Here we know that the items in each of these tuples is a name, subject, and grade:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_name, subject, grade = student_grades[0]\n",
+    "print(student_name, 'got a grade of', grade, 'in', subject)"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "We could instead represent this data as a dictionary and use it as such. A dictionary of information describing a single item is often referred to as a *record*:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "record = {\n",
+    "    'name': 'Alice',\n",
+    "    'subject': 'Spanish',\n",
+    "    'grade': 'A',\n",
+    "}\n",
+    "print(record['name'],\n",
+    "      'got a grade of', record['grade'],\n",
+    "      'in', record['subject'])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "While the code is slightly longer, there is absolutely no ambiguity here about matching up indices and what each value represents. This is also useful in contexts where some of the fields might be optional."
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "# 7. Combining Data Types\n",
+    "\n",
+    "In most of these simple examples we've worked with collections of simple values like strings and numbers, however data analysis often involves working with complex data, where each item of interest has several data associated with it. These complex data are often represented as collections of collections, *e.g.,* lists of dictionaries.\n",
+    "\n",
+    "Choosing the appropriate data types for a given problem will make it easier for you to write bug-free code and will make your code easier for others to read, but identifying the best data types is a skill gained through experience. Some of the commonly-used combination data types are illustrated below, but this is hardly exhaustive.\n",
+    "\n",
+    "## 7.1 List of tuples\n",
+    "\n",
+    "We've actually seen this one before. Consider the `student_grades` data from the earlier example on tuple unpacking:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grades = [\n",
+    "    ('Alice', 'Spanish', 'A'),\n",
+    "    ('Bob', 'French', 'C'),\n",
+    "    ('Carol', 'Italian', 'B+'),\n",
+    "    ('Dave', 'Italian', 'A-'),\n",
+    "]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This is a list of tuples:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grades[1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "and we can work with the individual tuples as such:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grades[1][2]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 7.2 List of dictionaries\n",
+    "\n",
+    "In the section on dictionaries, we explored how dictionaries are often used to contain several data about a single entity, and each such dictionary is sometimes called a *record*. Let's convert the list of tuples `student_grades` into a list of records `student_grade_records`:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade_records = []\n",
+    "for student_name, subject, grade in student_grades:\n",
+    "    record = {\n",
+    "        'name': student_name,\n",
+    "        'subject': subject,\n",
+    "        'grade': grade,\n",
+    "    }\n",
+    "    student_grade_records.append(record)\n",
+    "    \n",
+    "student_grade_records"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now each item in the list is a dictionary:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade_records[1]"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "and we can work with the individual records as such:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_grade_records[1]['grade']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "This list-of-dicts is often used to represent data from a database or an API. Let's use this data to write our code congratulating students for good grades, as we did in the section on tuple unpacking:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "for record in student_grade_records:\n",
+    "    if record['grade'].startswith('A'):\n",
+    "        print('Congratulations', record['name'], \n",
+    "              'on getting an', record['grade'], \n",
+    "              'in', record['subject'])"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 7.3 Dictionary of dictionaries\n",
+    "\n",
+    "The list of dictionaries is very useful when dealing with non-unique data; in the previous example each student might have several grades from different classes. But sometimes we want to refer to the data by a particular name or key. In this case, we can use a dictionary whose values are records, *i.e.*, other dictionaries.\n",
+    "\n",
+    "Let's use data from `student_grades` again, but assume we just want the foreign language grade so we can use the students name as a key:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "foreign_language_grades = {}\n",
+    "for student_name, subject, grade in student_grades:\n",
+    "    record = {\n",
+    "        'subject': subject,\n",
+    "        'grade': grade,\n",
+    "    }\n",
+    "    foreign_language_grades[student_name] = record\n",
+    "    \n",
+    "foreign_language_grades"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now we can refer to these by student name:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "foreign_language_grades['Alice']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "And we can get the individual data that we care about:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "foreign_language_grades['Alice']['grade']"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 7.4 Dictionary with tuple keys\n",
+    "\n",
+    "It is occasionally useful to key dictionaries on more than one data. Dictionaries can use any immutable object as a key, which includes tuples. Continuing with our student grades example, we may want the keys to be the student name and subject:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_course_grades = {}\n",
+    "for student_name, subject, grade in student_grades:\n",
+    "    student_course_grades[student_name, subject] = grade\n",
+    "    \n",
+    "student_course_grades"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "Now we can represent all of a student's grades:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_course_grades['Alice', 'Math'] = 'A'\n",
+    "student_course_grades['Alice', 'History'] = 'B'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_course_grades"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "## 7.5 Another dictionary of dictionaries\n",
+    "\n",
+    "Let's take advantage of the fact that, for a particular student, we often want to get subject-grade pairs, *i.e.* a report card. We can create a dictionary with student names as keys and the values being dictionaries of subject-grade pairs. In this case we need to do a bit of checking; that step is commented below:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_report_cards = {}\n",
+    "for student_name, subject, grade in student_grades:\n",
+    "    # If there is no report card for a student,\n",
+    "    # we need to create a blank one\n",
+    "    if student_name not in student_report_cards:\n",
+    "        student_report_cards[student_name] = {}\n",
+    "    student_report_cards[student_name][subject] = grade"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_report_cards"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "The advantage of this extra work is that we can now easily have multiple grades per student:"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_report_cards['Alice']['Math'] = 'A'\n",
+    "student_report_cards['Alice']['History'] = 'B'"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_report_cards"
+   ]
+  },
+  {
+   "cell_type": "markdown",
+   "metadata": {},
+   "source": [
+    "And we can easily fetch a student's \"report card\":"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": [
+    "student_report_cards['Alice']"
+   ]
+  },
+  {
+   "cell_type": "code",
+   "execution_count": null,
+   "metadata": {},
+   "outputs": [],
+   "source": []
+  }
+ ],
+ "metadata": {
+  "kernelspec": {
+   "display_name": "Python 3.8.3 ('venv')",
+   "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.3"
+  },
+  "vscode": {
+   "interpreter": {
+    "hash": "3699540f6baed5bd29a193b0c2d028af3f2c80498e3cac18f2b44cdd848387e2"
+   }
+  }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 2
+}
-- 
GitLab