```python
from IPython.core.display import HTML
def css_styling():
    styles = open("./styles/custom.css", "r").read()
    return HTML(styles)
css_styling()
```




<style>

body {
 counter-reset: h2counter;
}
h1 {
 counter-reset: h2counter;
}
h2:before {
 content: counter(h2counter) ".\0000a0\0000a0";
 counter-increment: h2counter;
 counter-reset: h3counter;
}


</style>




### BEFORE YOU DO ANYTHING...
In the terminal:
1. Navigate to __inside__ your ILAS_Python repository.
2. __COMMIT__ any un-commited work on your personal computer.
3. __PULL__ any changes *you* have made using another computer.
4. __PULL__ textbook updates (including homework answers).

1. __Open Jupyter notebook:__   Start >> Programs (すべてのプログラム) >> Programming >> Anaconda3 >> JupyterNotebook
1. __Navigate to the ILAS_Python folder__. 
1. __Open today's seminar__  by clicking on 4_Data_Structures.

# Data Structures

# Lesson Goal

Learn to manipulate multiple values and variables contained in data structures.




# Objectives


- Express collections of mulitple variables using `lists`, `tuples` and `dictionaries`.  
- Use python functions to manipulate data structures.
- Use control statements for iteratation and comprehension.
- Learn to select the right data structure for an application.

Why we are studying this:

To use Python to solve more complex engineering problems you are likely to encounter involving:
 - multi-variable values (e.g. vectors)
 - large data sets (e.g. experiment results)
 - using control flow to manipulate data sets using logic (e.g. sorting and categorising)



 Lesson structure:
 
 Three data strcutures: `lists`, `tuples` and `dictionaries`. 
 
  - Lists
   - General data structure manipulations: sorting, adding, removing...
   - Indexing and enumerating
   - Iterating and comprension
  - Tuple
  - Dictionaries
  
   

In the last seminar we learnt to generate a range of numbers for use in control flow of  a program, using the function `range()`:


```python
for j in range(20):
    
    if j % 4 == 0:  # Check remainer of j/4
        continue    # continue to next value of j
        
    print(j, "is not a multiple of 4")
```

    1 is not a multiple of 4
    2 is not a multiple of 4
    3 is not a multiple of 4
    5 is not a multiple of 4
    6 is not a multiple of 4
    7 is not a multiple of 4
    9 is not a multiple of 4
    10 is not a multiple of 4
    11 is not a multiple of 4
    13 is not a multiple of 4
    14 is not a multiple of 4
    15 is not a multiple of 4
    17 is not a multiple of 4
    18 is not a multiple of 4
    19 is not a multiple of 4


## Data Structures

Often we want to manipulate data that is more meaningful than ranges of numbers.

These collections of variables might include:
 - the results of an experiment
 - a list of names
 - the components of a vector
 - a telephone directory with names and associated numbers.
 


 
 

Python has different __data structures__ that can be used to store and manipulate these values.

Like variable types (`string`, `int`,`float`...) different data structures behave in different ways.

Today we will learn to use `list`, `tuple` and dictionary (`dict`) data structures.


We will study the differences in how they behave so that you can learn to select the most suitable data structure for an application. 

Example

If we want to store the names of students in a laboratory group, 
rather than representing each students using an individual string variable, we could use a list of names. 




```python
lab_group0 = ["Sarah", "John", "Joe", "Emily"]
lab_group1 = ["Roger", "Rachel", "Amer", "Caroline", "Colin"]

print(lab_group0)
print(lab_group1)
```

    ['Sarah', 'John', 'Joe', 'Emily']
    ['Roger', 'Rachel', 'Amer', 'Caroline', 'Colin']


This is useful because we can perform operations on lists such as:
 - checking its length (number of students in a lab group)
 - sorting the names in the list into alphabetical order
 - making a list of lists (we call this a *nested list*):



```python
lab_groups = [lab_group0, lab_group1]
```

## Lists

A list is a sequence of data. 

We call each item in the sequence an *element*. 

A list is constructed using square brackets:




```python
a = [1, 2, 3]
```

A `range` can be converted to a list with the `list` function (casting).


```python
print(list(range(10)))
```

    [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]


Reminder, when a range has one arguments:
 - the first value is 0.
 - the second value is the stopping value.
 - the stopping value is not included in the range


When a range has two arguments:
 - the first value is the starting value.
 - the second value is the stoping value.
 - the stopping value is not included in the range


```python
print(list(range(10,20)))

```

    [10, 11, 12, 13, 14, 15, 16, 17, 18, 19]


You can optionally include a step:


```python
print(list(range(10, 20, 2)))
```

    [10, 12, 14, 16, 18]


A list can hold a mixture of types (`int`, `string`....).


```python
a = [1, 2.0, "three"]
```

An empty list is created by


```python
my_list = []
```

A list of length 5 with repeated values can be created by


```python
my_list = ["Hello"]*5
print(my_list)
```

    ['Hello', 'Hello', 'Hello', 'Hello', 'Hello']


We can check if an item is in a list using the function `in`:



```python
print("Hello" in my_list)
print("Goodbye" in my_list)
```

    True
    False


<a id='Indexing'></a>
### Indexing

Lists store data in order.

We can select a single element or multiple elements of a list using the __index__ of the element(s).

You are familiar with this process; it is the same as selecting individual characters of a string:


```python
word = "string"
letter = word[1]
print(letter)
```

    t



```python
lab_group0 = ["Sarah", "John", "Joe", "Emily"]

first_member = lab_group0[0]

print(first_member)
```

    Sarah


If we select multiple elements they are returned as a list:


```python
lab_group0 = ["Sarah", "John", "Joe", "Emily"]

first_members = lab_group0[0:2]

print(first_members)
```

    ['Sarah', 'John']


We can select the inidividual characters of a string using a second index.

For example to select the first letter of the second group member's name:


```python
lab_group0 = ["Sarah", "John", "Joe", "Emily"]

letter = lab_group0[1][0]

print(letter)
```

    J


### Manipulating Lists 

There are many functions for manipulating lists.

Many of these functions apply to other data structures.











<a id='Length'></a>
### Finding the Length of a List

We can find the length (number of items) of a list using the function `len()`, by including the name of the list in the brackets. 

In the example below, we find the length of the list `lab_group0`. 


```python
lab_group0 = ["Sara", "Mari", "Quang"]

size = len(lab_group0)

print("Lab group members:", lab_group0)

print("Size of lab group:", size)

print("Check the Python object type:", type(lab_group0))
```

    Lab group members: ['Sara', 'Mari', 'Quang']
    Size of lab group: 3
    Check the Python object type: <class 'list'>


<a id='SortLists'></a>
### Sorting Lists

To sort the list we use the function `sorted()`.

#### Sorting Numerically

If the list contains numerical variables, the numbers is sorted in ascending order.


```python
numbers = [7, 1, 3.0]

print(numbers)

numbers = sorted(numbers)

print(numbers)
```

    [7, 1, 3.0]
    [1, 3.0, 7]


__Note:__ We can sort a list with mixed numeric types (e.g. `float` and `int`). 

However, we cannot sort a list with types that cannot be sorted by the same ordering rule. 

(e.g. `numbers = sorted(["7", 1, 3.0])` causes an error.)


```python



```

#### Sorting Alphabetically

If the list contains strings of alphabet characters, the list is sorted by alphabetical order. 


```python
lab_group0 = ["Sara", "Mari", "Quang"]

print(lab_group0)

lab_group0 = sorted(lab_group0)

print(lab_group0)
```

    ['Sara', 'Mari', 'Quang']
    ['Mari', 'Quang', 'Sara']


As with `len()` we include the name of the list we want to sort in the brackets. 

`sort` is known as a 'method' of a `list`. 

If we suffix a list with `.sort()`, it performs an *in-place* sort.


```python
lab_group0 = ["Sara", "Mari", "Quang"]

print(lab_group0)

#lab_group0 = sorted(lab_group0)
lab_group0.sort()

print(lab_group0)
```

    ['Sara', 'Mari', 'Quang']
    ['Mari', 'Quang', 'Sara']


__Try it yourself__

In the cell provided in your textbook create a list of __numeric__ or __string__ values.

Sort the list using `sorted()` __or__ `.sort()`.

Print the sorted list.

Print the length of the list using `len()`.


```python
# Sorting a list
```

### Removing an Item from a List

We can remove items from a list using the method `pop`.

We place the index of the element we wish to remove in brackets. 


```python
lab_group0 = ["Sara", "Mari", "Quang", "Sam", "Ryo"]
print(lab_group0)

# Remove the second student from the list: lab_group (remember indexing starts from 0 so 1 is the second element)

lab_group0.pop(1)
print(lab_group0)
```

    ['Sara', 'Mari', 'Quang', 'Sam', 'Ryo']
    ['Sara', 'Quang', 'Sam', 'Ryo']



```python
# By default, pop removes the last element

lab_group0.pop()
print(lab_group0)
```

    ['Sara', 'Quang', 'Sam']



```python
# Pop can by used to assign the removed value to a variable name

group_member = lab_group0.pop(1)
print(lab_group0)
print(group_member)
```

    ['Sara', 'Sam']
    Quang


### Adding an Item to a List

We can add items to a list using the method `insert`.

We place the desired index of new element in brackets. 


```python
# Add new student "Mark" to the list
lab_group0.insert(2, "Mark")
print(lab_group0)
```

    ['Sara', 'Sam', 'Mark']


We can add items at the end of a list using the method `append`.

We place the element we want to add to the end of the list in brackets. 


```python
# Add new student "Lia" at the end of the list
lab_group0.append('Lia')
print(lab_group0)
```

    ['Sara', 'Quang', 'Sam', 'Ryo', 'Lia', 1, 'Lia']


### Changing a list entry.
We can change the entry of a list using indexing.


```python
lab_group0[3] = "Am"
print(lab_group0)

# Adding and removing items from a list.
```

    ['Sara', 'Sam', 'Mark', 'Am']


__Try it yourself__

In the cell provided in your textbook.

Remove "Sara" from the list.

Print the new list.

Add a new lab group member, Tom, to the list.

Print the new list.


```python
lab_group0 = ["Sara", "Mari", "Quang", "Sam", "Ryo"]
print(lab_group0)

# Adding and removing items from a list.
```

    ['Sara', 'Mari', 'Quang', 'Sam', 'Ryo']


<a id='NestedList'></a>
### Nested Data Structures: Lists of Lists

A *nested list* is a list within a list. 

(Recall *nested loops* from the last seminar 1; Control Flow). 



To access a __single element__ we need as many indices as there are levels of nested list. 

This is more easily explained with an example:

`lab_groups` is a nested list containing the lists:
 - `lab_group0`
 - `lab_group1`
 - `lab_group2`


```python
lab_group0 = ["Sara", "Mika", "Ryo", "Am"]
lab_group1 = ["Hemma", "Miri", "Quy", "Sajid"]
lab_group2 = ["Adam", "Yukari", "Farad", "Fumitoshi"]

lab_groups = [lab_group0, lab_group1, lab_group2]
```

To select and element from `lab_group1` we:
- first give the index of `lab_group1` in th list `lab_groups`
- second give the index of the element within `lab_group1`


```python
group = lab_groups[1]
print(group)

name = lab_groups[1][2]
print(name)
```

    ['Hemma', 'Miri', 'Quy', 'Sajid']
    Quy


<a id='IteratingLists'></a>
### Iterating Over Lists

Looping over each item in a list is called *iterating*. 

To iterate over a list of the lab group we  can use a `for` loop.



In the followig example, each iteration, variable `d` takes the value of the next item in the list:


```python
for d in [1, 2.0, "three"]:    
    print("the value of d is:", d)
```

    the value of d is: 1
    the value of d is: 2.0
    the value of d is: three


We could also express this as:


```python
data = [1, 2.0, "three"]

for d in data:    
    print("the value of d is:", d)
```

    the value of d is: 1
    the value of d is: 2.0
    the value of d is: three


Iterating backwards over a list can be acheived using the built in `reversed` function.


```python
data = [1, 2.0, "three"]

for d in reversed(data):    
    print("the value of d is:", d)


```

    the value of d is: three
    the value of d is: 2.0
    the value of d is: 1


__Try it yourself__


In the cell provided in your textbook *iterate* over the list `data = [1, 2.0, "three"]`.

Each time the code loops:
1. print the value of data __cast as a string__ (Seminar 1 Data Types and Operators)
1. print the variable type<br>(to demonstrate that the variable has been cast. Note that otherwise the variable appears to remain unchanged).


```python
# Iterate over a list and cast each item as a string
data = [1, 2.0, "three"]
for n in data:
    print(n,type(n))
```

    1 <class 'int'>
    2.0 <class 'float'>
    three <class 'str'>


### Indexing when Iterating over Lists
Indexing can be useful when iterating over a list.



For example, we can select a range of elements to iterate over:


```python
lab_group0 = ["Sara", "Mari", "Quang", "Sam", "Ryo", "Nao", "Takashi"]

for member in lab_group0[2:5]:    
    print("name:", member)
```

    name: Quang
    name: Sam
    name: Ryo


A third value can used to choose a step size (similar to `range()`).

For example, if we want to choose every other lab member we use step size, 2:


```python
lab_group0 = ["Sara", "Mari", "Quang", "Sam", "Ryo", "Nao", "Takashi","ungerade"]

for member in lab_group0[::2]:    
    print("name:", member)
```

    name: Sara
    name: Quang
    name: Ryo
    name: Takashi


__Note:__<br>
Some data structures that support *iterating* but do not support *indexing*.

e.g. dictionaries, which we will learn about later. 

When possible, it is better to iterate over a list rather than use indexing.

### `enumerate()`
The function `enumerate` can be used to return the index of each element.
<br>This information is cast as a list to allow us to read it.


```python
lab_group0 = ["Sara", "Mari", "Quang", "Sam", "Ryo", "Nao", "Takashi"]
a = enumerate(lab_group0)
b = list(enumerate(lab_group0))
print(a)
print(b)


```

    <enumerate object at 0x10d219090>
    [(0, 'Sara'), (1, 'Mari'), (2, 'Quang'), (3, 'Sam'), (4, 'Ryo'), (5, 'Nao'), (6, 'Takashi')]



```python
string = "string"
a = list(enumerate(string))
print(a)
```

    [(0, 's'), (1, 't'), (2, 'r'), (3, 'i'), (4, 'n'), (5, 'g')]


### Iterating Over Multiple Lists Using `zip()`
It can be very useful to iterate through multiple lists within the same loop. than one list.

For example if we have a list of group members and a list of their scores for an assignemt, we can print the score that corresponds to each lab member:


```python
lab_group0 =  ["Sara", "Mari", "Quang", "Sam", "Ryo", "Nao", "Takashi"]

assignment1 = [72, 56, 65, 52, 71, 60]

for member, score in zip(lab_group0, assignment1):    
    print(member, ": score =", score)
```

    Sara : score = 72
    Mari : score = 56
    Quang : score = 65
    Sam : score = 52
    Ryo : score = 71
    Nao : score = 60


In this example 

`member` is the name given to the *current* value from the list `lab_group0`

`score` is the names given to the *current* value from the list `assignment1`.  


We can include any number of lists in `zip`.

For example it may be useful to print the combined score a lab member has achieved for all assigments this semester:


```python
lab_group0 =  ["Sara", "Mari", "Quang", "Sam", "Ryo", "Nao", "Takashi"]

assignment1 = [72, 56, 65, 52, 71, 60]
assignment2 = [52, 61, 73, 55, 62, 55]
assignment3 = [71, 71, 70, 66, 61, 71]

for member, score1, score2, score3 in zip(lab_group0, 
                                          assignment1, 
                                          assignment2, 
                                          assignment3):    
    print(member, ": score =", (score1 + score2 + score3))
```

    Sara : score = 195
    Mari : score = 188
    Quang : score = 208
    Sam : score = 173
    Ryo : score = 194
    Nao : score = 186


### Lists Example: Vectors

__Vector:__ A quantity with magnitude and direction.

Position vectors (or displacement vectors) in 3D space can be expressed in terms of x,y, and z-directions.  





The position vector 𝒓 indicates the position of a point in 3D space.

$$
\mathbf{r} = x\mathbf{i} + y\mathbf{j} + z\mathbf{k}
$$

$\mathbf{i}$ is the displacement one unit in the x-direction<br>
$\mathbf{j}$ is the displacement one unit in the y-direction<br>
$\mathbf{k}$ is the displacement one unit in the z-direction


We can conveniently express $\mathbf{r}$ in matrix (or basis vector) form using the coefficients $x, y$ and $z$: 
$$
\mathbf{r} = [x, y, z]
$$

__...which looks a lot like a Python list!__


You will encounter 3D vectors a lot in your engineering studies.

They are used to describe many physical quantities, e.g. force.

<a id='DotProductLists'></a>

### Iterating with Zip 

#### Example: The dot product of two vectors:

The __dot product__ is a really useful algebraic operation.

It takes two equal-length *sequences of numbers* (often coordinate vectors) and returns a single number. 
 

__GEOMETRIC REPRESENTATION OF THE DOT PRODUCT__

\begin{align}
\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| cos(\theta)
\end{align}

Where:
$|\mathbf{A}|$ and $|\mathbf{B}|$ are the *magnitudes* of $\mathbf{A}$ and $\mathbf{B}$

$\theta$ is the angle between the two vectors.

__NOTATION:__ *Magnitude*

<br>__3D vector__ 
<br>$\mathbf{r} = [x, y, z]$

magnitude of $\mathbf{r}$:
<br>$|\mathbf{r}| = \sqrt{x^2 + y^2 + z^2}$



__2D vector__ 
<br>$\mathbf{r} = [x, y]$

magnitude of $\mathbf{r}$:
<br>$|\mathbf{r}| = \sqrt{x^2 + y^2}$

__ALGEBRAIC REPRESENTATION OF THE DOT PRODUCT__

The dot product of two $n$-length-vectors:
<br> $ \mathbf{A} = [A_1, A_2, ... A_n]$
<br> $ \mathbf{B} = [B_1, B_2, ... B_n]$

\begin{align}
\mathbf{A} \cdot \mathbf{B} = \sum_{i=1}^n A_i B_i
\end{align}



So the dot product of two 3D vectors:
<br> $ \mathbf{A} = [A_x, A_y, A_z]$
<br> $ \mathbf{B} = [B_x, B_y, B_z]$


\begin{align}
\mathbf{A} \cdot \mathbf{B} &= \sum_{i=1}^n A_i B_i \\
&= A_x B_x + A_y B_y + A_z B_z
\end{align}



__Example:__ 
<br> The dot product $\mathbf{A} \cdot \mathbf{B}$:
<br> $ \mathbf{A} = [1, 3, −5]$
<br> $ \mathbf{B} = [4, −2, −1]$



\begin{align}
      {\displaystyle {\begin{aligned}\ [1,3,-5]\cdot [4,-2,-1]&=(1)(4)+(3)(-2)+(-5)(-1)\\& = 4 \qquad - 6 \qquad + 5 \\&=3\end{aligned}}} 
\end{align}

We can solve this very easily using a Python `for` loop.

With each iteration of the loop we increase the value of `dot_product` (initial value = 0.0) by the product of `a` and `b`.  


```python
A = [1.0, 3.0, -5.0]
B = [4.0, -2.0, -1.0]

# Create a variable called dot_product with value, 0.0
dot_product = 0.0

for a, b in zip(A, B): 
    dot_product += a * b

print(dot_product)
```

    3.0


Example uses of the dot product:

\begin{align}
\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| cos(\theta)
\end{align}

 - Find the angle between two vectors (from its cosine).
 - Check if the angle between two vectors is:
   - acute ($\mathbf{A} \cdot \mathbf{B}>0$)
   - obtuse ($\mathbf{A} \cdot \mathbf{B}<0$)
   - right angle ($\mathbf{A} \cdot \mathbf{B}==0$)

 

\begin{align}
\mathbf{A} \cdot \mathbf{B} = |\mathbf{A}| |\mathbf{B}| cos(\theta)
\end{align}

- Find the magnitude of one vector in the direction of another.
- Find physical quantities <br> e.g. the work, W, done pushing an object distance, d, with force, F:
 
 


__Try it yourself:__ 

$\mathbf{C} = [2, 4, 3.5]$

$\mathbf{D} = [1, 2, -6]$

In the cell below find the dot product:
$\mathbf{C} \cdot \mathbf{D}$

Is the angle between the vectors obtuse or acute or are the vectors perpendicular? <br> 
(Perpendicular if $\mathbf{A} \cdot \mathbf{B}==0$, acute if $\mathbf{A} \cdot \mathbf{B}>0$, or obtuse if $\mathbf{A} \cdot \mathbf{B}<0$).
 


```python
# The dot product of C and D
dot=0
c=[2,4,3.5]
d=[1,2,-6]
for a,b in zip(c,d):
    dot+=a*b
print(dot)
```

    -11.0


## List comprehension

A list is a really useful way to build lists from other lists. 

However, it can be difficult to interpret when reading.

Look carefully at the following example.



We have a list of numbers Y.

`Y = [4, 6, 10, 11]`

We want a new list of numbers, X where every element is the square of the element of the same index in Y. 


```python
Y = [4, 6, 10, 11]

X = [y**2 for y in Y]

print(Y)
print(X)
```

    [4, 6, 10, 11]
    [16, 36, 100, 121]


#### Interpreting the comprehension:

"<font color='blue'>y**2</font> for <font color='green'>y</font> in <font color='red'>Y</font>" 

means:
- <font color='red'>Loop through each value in the list Y.</font>
- With each iteration:
 - <font color='green'>assign the value of the next element in Y to the variable y.</font>
 - <font color='blue'>square it to get the next element in X.</font>

This is equivalent to:


```python
Y = [4, 6, 10, 11]

#Create an empty list
X = []

for y in Y:
    X.append(y**2)
    
print(Y)
print(X)
```

    [4, 6, 10, 11]
    [16, 36, 100, 121]


...but the list comprehension is more succinct.



#### Comprehension: Operations on All Items in a List

A list comprehension is useful if we want to perform an operation on all items of a list. 

Consider the exercise you completed earlier:

>*__Try it yourself__<br>
>In the cell provided in your textbook *iterate* over the list `data = [1, 2.0, "three"]`.*<br>
*Each time the code loops:*
>1. *print the value of data __cast as a string__ *
>1. *print the variable type*



You probably wrote something like...


```python
data = [1, 2.0, "three"]

for d in data:    
    d = str(d)
    print(type(d))
```

    <class 'str'>
    <class 'str'>
    <class 'str'>


The original value of in `data` remain unchanged:


```python
print(type(data[0]), type(data[1]), type(data[2]))
```

    <class 'int'> <class 'float'> <class 'str'>


Using a list comprehension:


```python
data = [str(d) for d in data]

print(data)

print(type(data[0]), type(data[1]), type(data[2]))
```

    ['1', '2.0', 'three']
    <class 'str'> <class 'str'> <class 'str'>


#### Comprehensions with Conditionals

Comprehensions can be written so that elements will only be added to a list  if they satisfy a condition.

In the example below, elements remain in list, `items`, if their value is greater than 2.


```python
items = [1, 3, 5, 7, 3, 2, 1]

items = [i for i in items if i > 2]

print(items)
```

    [3, 5, 7, 3]


In the example below, elements are squared and added to new list `squares` if their value is greater than 2.


```python
items = [1, 3, 5, 7, 3, 2, 1]

squares = [i**2 for i in items if i > 2]

print(items)
print(squares)
```

    [1, 3, 5, 7, 3, 2, 1]
    [9, 25, 49, 9]


__Try it yourself__

In the cell below, create a new list `V`, of the even numbers of list `U`, multiplied by 2.


```python
U = [2, 3, 6, 8, 9, 1, 2, 4]
V=[u*2 for u in U if not u%2]
print(V)
```

    [4, 12, 16, 4, 8]


## Tuples

In many ways, tuples are similar to lists. 

However, after creatig a tuple:
 - you cannot __add__ or __remove__ elements from it without creating a new tuple (e.g. connecting two tuples). 
 - you cannot change the value of a single tuple element e.g. by indexing. 




Tuples are therefore used for values that should not change after being created.
<br>e.g. a vector of length three with fixed entries

It is 'safer' in this case since it cannot be modified accidentally in a program. 

To create a tuple, use () parentheses.


__Example__
<br>At Kyoto University, each professor is assigned an office.

Philamore-sensei is given room 32:


```python
room = ("Philamore", 32)

print("Room allocation:", room)

print("Length of entry:", len(room))

print(type(room))
```

    Room allocation: ('Philamore', 32)
    Length of entry: 2
    <class 'tuple'>


<a id='IteratingTuples'></a>
### Iterating over Tuples 

We can *iterate* over tuples in the same way as with lists,


```python
# Iterate over tuple values
for d in room:
    print(d)
```

    Philamore
    32


###  Indexing

We can index into a tuple:


```python
# Index into tuple values
print(room[1])
print(room[0])
```

    32
    Philamore


__Note__ Take care when creating a tuple of length 1...

In this example, we create a *list* of length 1:


```python
# Creating a list of length 1 
a = [1]
print(a)
print(type(a))
print(len(a))
```

    [1]
    <class 'list'>
    1


However, if we use the same process for a tuple:


```python
a = (1)
print(a)
print(type(a))
#print(len(a))
```

    1
    <class 'int'>


To create a tuple of length 1, we use a comma:


```python
a = (1,)
print(a)
print(type(a))
print(len(a))
```

    (1,)
    <class 'tuple'>
    1


For example, Endo-sensei is a visiting professor who has not been assigned a room:


```python
room = ("Endo",)
print("Room allocation:", room)
print("Length of entry:", len(room))
print(type(room))
```

    Room allocation: ('Endo',)
    Length of entry: 1
    <class 'tuple'>


### Nested Data Structures: Lists of Tuples
As part of a rooms database, we can create a list of tuples:


```python
room_allocations = [("Endo",), 
                   ("Philamore", 32), 
                   ("Matsuno", 31), 
                   ("Sawaragi", 28), 
                   ("Okino", 28), 
                   ("Kumegawa", 19)]

print(room_allocations)
```

    [('Endo',), ('Philamore', 32), ('Matsuno', 31), ('Sawaragi', 28), ('Okino', 28), ('Kumegawa', 19)]


__Try it yourself:__ 

Refer to <a href='#NestedList'>Nested Lists</a> for how to index into *nested* data structures.

In the cell below use indexing to print:
 - Matsuno-sensei's room number
 - Kumegawa-sensei's room number
 - The variable type of Kumegawa-sensei's room number


```python
# Matsuno-sensei's room number
print(room_allocations[2][1])
# Kumegawa-sensei's room number
print(room_allocations[-1][1])

# The Python variable type of Kumegawa-sensei's room number
print(type(room_allocations[-1][1]))

```

    31
    19
    <class 'int'>


### Sorting Tuples
To make it easier to look up the office number of each professor, we can __sort__ the list of tuples into an office directory.

The ordering rule is determined by the __first element__ of each tuple:

If the first element of each tuple is a __numeric__ type (`int`, `float`...)
<br>the tulpes are sorted by ascending __numerical__ order of the first element:

If the first element of each tuple is a __`string`__ (as in this case), 
<br>the tuples are sorted by alphabetical __order__ of the first element.

A tuple is sorted using the same method to sort a list.

Jump to <a href='#SortLists'>Sorting Lists</a>.

__Try it yourself__

In the cell provided below, sort the tuple, `room_allocations` by alphabetical order. 


```python
# room_allocation sorted by alphabetical order
room_allocations.sort()
print(room_allocations)
```

    [('Endo',), ('Kumegawa', 19), ('Matsuno', 31), ('Okino', 28), ('Philamore', 32), ('Sawaragi', 28)]


The office directory can be improved by excluding professors who do not have an office at Yoshida campus:


```python
for entry in room_allocations:
    
    # only professors with an office have an entry length > 1
    if len(entry) > 1:
        print(f"Name: {entry[0]}, Room: {entry[1]}")
```

    Name: Kumegawa, Room: 19
    Name: Matsuno, Room: 31
    Name: Okino, Room: 28
    Name: Philamore, Room: 32
    Name: Sawaragi, Room: 28


__Note:__ By prefixing the string you are printing with `f` you can interspace text with variables by placing the variable in { } braces. <br>This is known as a *formatted string* or *"f-string"*.

<a id='Dictionaries'></a>
## Dictionaries 

We used a list of tuples in the previous section to store room allocations. 

What if we wanted to use a program to find which room a particular professor has been allocated?



Ee would need to either:
- iterate through the list and check each name. 

> For a very large list, this might not be very efficient.

- use the index to select a specific entry of a list or tuple. 

> This works if we know the index to the entry of interest. For a very large list, this is unlikely.

A human looking would identify individuals in an office directory by name (or "keyword") rather than a continuous set of integers. 

Using a Python __dictionary__ we can build a 'map' from names (*keys*) to room numbers (*values*). 

<a id='DeclaringDict'></a>
A Python dictionary (`dict`) is declared using curly braces:


```python
empty_dictionary = {}

room_allocations = {"Endo": None, 
                   "Philamore": 32, 
                   "Matsuno": 31, 
                   "Sawaragi": 28, 
                   "Okino": 28, 
                   "Kumegawa": 19}


print(room_allocations)
print(type(room_allocations))
```

    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Okino': 28, 'Kumegawa': 19}
    <class 'dict'>


Each entry is separated by a comma. 

For each entry we have:
 - a 'key' (followed by a colon)
 - a 'value'. 
 



__Note:__ For empty values (e.g. `Endo` in the example above) we use '`None`' for the value.

`None` is a Python keyword for 'nothing' or 'empty'.


Now if we want to know which office belongs to Philamore-sensei, we can query the dictionary by key:


```python
philamore_office = room_allocations["Philamore"]
print(philamore_office)
```

    32


### Iterating over Dictionaries

We can __*iterate*__ over the keys in a dictionary as we iterated over the elements of a list or tuple:

__Try it yourself:__
<br>
Refer back to:
 - <a href='#IteratingLists'>Iterating Over Lists</a>
 - <a href='#IteratingTuples'>Iterating Over Tuples</a>
<br>to remind yourself how to *iterate* over a data structure.

<br>
Using __exactly the same method__, iterate over the entries in the dictionary `room allocations` using a `for` loop.
<br>
Each time the code loops, print the next dictionary entry. 


```python
# iterate over the dictionary, room_allocations.
for d in room_allocations:
    print(f"Name: {d}, Room: {room_allocations[d]}")
# print each entry

```

    Name: Endo, Room: None
    Name: Philamore, Room: 32
    Name: Matsuno, Room: 31
    Name: Sawaragi, Room: 28
    Name: Okino, Room: 28
    Name: Kumegawa, Room: 19


Notice that this only prints the keys.

We can access `keys` and `values` seperately by:
 - creating two variable names before `in` 
 - putting `items()` after the dictionary name


```python
for name, room_number in room_allocations.items():
    print(name, room_number)    
```

    Endo None
    Philamore 32
    Matsuno 31
    Sawaragi 28
    Okino 28
    Kumegawa 19


__Try it yourself__<br>
Copy and paste the code from the previous cell.
<br>
Edit it so that it prints the room numbers only. 

Remember you can __"comment out"__ the existing code (instead of deleting it) so that you can refer to it later.
e.g.
```python
#print(name, room_number)
```



```python
# Iterate over the dictionary, room_allocations.
for d in room_allocations:
    print(room_allocations[d])
# Print each room number.
```

    None
    32
    31
    28
    28
    19


### Look-up Keys

A dictionary stores data differently from a list or tuple. 

Lists and tuples store entries as continuous pieces of memory, which is why we can access entries by index. 

Indexing cannot be used to access the entries of a dictionary. For example:
```python
print(room_allocations[0])
```
raises an error. 

Dictionaries use a different type of storage which allows us to perform look-ups using a 'key'.


```python
print(room_allocations["Philamore"])
```

    32


<a id='DictionaryAdd'></a>
### Adding Entries to a Dictionary

We use this same code to add new entries to an existing dictionary:  


```python
print(room_allocations)

room_allocations["Fujiwara"] = 34

print(room_allocations)
```

    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Okino': 28, 'Kumegawa': 19}
    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Okino': 28, 'Kumegawa': 19, 'Fujiwara': 34}


<a id='DictionaryRemove'></a>
### Removing Entries from a Dictionary

To remove an item from a disctionary we use the command `del`.


```python
print(room_allocations)

del room_allocations["Fujiwara"]

print(room_allocations)
```

    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Okino': 28, 'Kumegawa': 19, 'Fujiwara': 34}
    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Okino': 28, 'Kumegawa': 19}


__Try it yourself__
<br>
Okino-sensei is leaving Kyoto University. 

Her office will be re-allocated to a new member of staff, Ito-sensei.

In the cell below, update the dictionary by deleting the entry for Okino-sensei and creating a new entry for Ito-sensei.

Print the new dictionary.


```python
# Remove Okino-sensei (room 28) from the dictionary.
del room_allocations["Okino"]
# Add a new entry for Ito-sensei (room 28)
room_allocations["Ito"]=28
print(room_allocations)
```

    {'Endo': None, 'Philamore': 32, 'Matsuno': 31, 'Sawaragi': 28, 'Kumegawa': 19, 'Ito': 28}


So far we have used a string variable types for the dictionary keys.

However, we can use almost any variable type as a key and we can mix types. 



### Sorting Dictionaries

A tuple is sorted using the same method to sort a list. 

Jump to <a href='#SortLists'>Sorting Lists</a>.


```python
room_allocations["Endo"]
print(room_allocations)
             
# By default, the entries are sorted by key 
print(sorted(room_allocations))

# This is the same as:
print(sorted(room_allocations.keys()))
      
# So to sort by value:
print(sorted(room_allocations.values()))
```

__Note:__ The value `None` cannot be sorted. 

This is where a __comprehension__ can be useful.

Let's produce a dictionary of the professors with offices only.


```python
occupied_rooms = {key: val for key, val in room_allocations.items() if val}

print(sorted(occupied_rooms.values()))
print(occupied_rooms)
```



room_number: name for name, room_number in room_allocation.items()

#### Interpreting the comprehension:

"<font color='purple'>key: val</font> for <font color='green'>key</font>, <font color='blue'>val</font> in room_allocations.items() if <font color='red'>val</font>"

means:
<font color='red'>If the dictionary entry has a value (val)</font>, create an <font color='purple'>entry</font> in the new list using its:
<br><font color='green'>key</font>
<br><font color='blue'>value</font>
 

<a id='DictionaryRestructure'></a>
### Re-structuring to make a new Dictionary
__Example__: We could 'invert' the room allocation dictionary to create a room-to-name map.

Let's build a new dictionary (`room_map`) by looping through the old dictionary (`room_allocation`) using a `for` loop:


```python
# Create empty dictionary
room_map = {}

# Build dictionary to map 'room number' -> name 
for name, room_number in room_allocations.items():
    
    # Insert entry into new dictionary
    room_map[room_number] = name

print(room_map)
```

    {None: 'Endo', 32: 'Philamore', 31: 'Matsuno', 28: 'Ito', 19: 'Kumegawa'}


A more efficient way is to use a comprehension to do this:


```python
room_map = {room_number: name for name, room_number in room_allocations.items() if room_number!=None}
print(room_map)
```

    {32: 'Philamore', 31: 'Matsuno', 28: 'Ito', 19: 'Kumegawa'}


We can now consult the room-to-name map to find out if a particular room is occupied and by whom.

Let's assume some rooms are unoccupied and therefore do not exist in this dictionary.




If we try to use a key that does not exist in the dictionary, e.g.

> `occupant17 = room_map[17]`

Python will give an error (raise an exception). 




If we're not sure that a __key__ is present (that a room is occupied or unocupied in this case), we can check using the function in '`in`'. 
<br>(We used this function to check wether an entry exists in a __list__.)


```python
print(19 in room_map)
print(17 in room_map)
```

    True
    False


So we know that:
 - room 19 is occupied
 - room 17 is unoccupied



The same result can be acheieved using:


```python
print(19 in room_map.keys())
print(17 in room_map.keys())
```

    True
    False


When using `in`, take care to check for the __key__ (not the value)


```python
print('Kumegawa' in room_map)
```

    False


If we instead need to check that a __value__ is present:


```python
print('Kumegawa' in room_map.values())
```

    True


#### Avoiding Errors 

It can be useful to check if a variable exists.

If your program tries to use a variable that does not exist, errors can occur. 



An example check is shown below.

After the `if` statement we can include code that is only to be executed if the room is occupied. 


```python
rooms_to_check = [17, 19]

for room in rooms_to_check:
    
    if room in room_map:
        print(f"Room {room} is occupied by {room_map[room]}-sensei")
    
    else:
        print(f"Room {room} is unoccupied.")
```

    Room 17 is unoccupied.
    Room 19 is occupied by Kumegawa-sensei


## Choosing a data structure

An important task when developing a computer program is selecting the *appropriate* data structure for a task.

Here are some examples of the suitablity of the data types we have studied for some common computing tasks.


- __Dynamically changing individual elements of a data structure.__ 

e.g. updating the occupant of a room number or adding a name to a list of group members.

__Lists and dictionaries__ allow us to do this.<br> 
__Tuples__ do not.

- __Performing an operation on every item in a sequence.__ 

e.g. checking every item in a data set against a particular condition (e.g. prime number, multiple of 5....etc), performing an algebraic operation on every item in a data set. 

__Lists and tuples__ make this simple as we can call each entry in turn using its index.
<br> 
__Dictionaries__ this is less efficient as it requires more code.

- __Selecting a single item from a data structure without knowing its position in a sequence.__  

e.g. looking up the profile of a person using their name, avoiding looping through a large data set in order to identify a single entry. 

__Dictionaries__ allow us to select a single entry by an associated (unique) key variable.
<br> 
__Lists and tuples__ make this difficult as to pick out a single value we must either i) know it's position in an ordered sequence, ii) loop through every item until we find it.


- __Protecting individual items of a data sequence from being added, removed or changed within the program.__

e.g. representing a vector of fixed length with fixed values, representing the coordintes of a fixed point. 

__Tuples__ allow us to do this.
<br> 
__Lists and dictionaries__ do not.

- __Speed__

For many numerical computations, efficiency is essential. 

More flexible data structures:
- are generally less efficient computationally. 
- require more computer memory. 

We will study the difference in speed there can be between different data structures in a later seminar.

- __Storing items in a particular sequence (so that they can be addressed by index or in a particular order)__.

e.g. representing the x, y, z coordinates of a 3D position vector, storing data collected from an experiment as a time series. 

__Lists and tuples__ allow us to do this.
<br> 
__Dictionaries__ do not.

## Review Exercises
Here are a series of engineering problems for you to practise each of the new Python skills that you have learnt today.

### Review Exercise:  Data structures.

__(A)__ In the cell below, what type of data structure is C?

__(B)__ Write a line of code that checks whether 3 exists within the data structure.

__(C)__ Write a line of code that checks whether 3.0 exists within the data structure.

__(D)__ Write a line of code that checks whether "3" exists within the data structure.



```python
C = (2, 3, 5, 6, 1, "hello")
```


```python
# Example solution 

print(type(C))

print(3 in C)

print(3.0 in C)

print("3" in C)
```

    <class 'tuple'>
    True
    True
    False


### Review Exercise:  Using a single list with a `for` loop.
In the cell below, use a `for` loop to print the first letter of each month in the list.

Jump to <a href='#Indexing'>Indexing</a> for how to pick out individual letters of a string.

Jump to <a href='#IteratingLists'>Iterating over lists</a> for how to loop through each element of a list.




```python
# Print the first letter of each month in the list

months = ["January",
         "February",
         "March",
         "April",
         "May",
         "June",
         "July",
         "August",
         "September",
         "October",
         "November",
         "December"]
for m in months:
    print(m[0])
```

    J
    F
    M
    A
    M
    J
    J
    A
    S
    O
    N
    D



```python
# Example solution 

months = ["January", "February",
         "March", "April",
         "May", "June",
         "July", "August",
         "September", "October",
         "November", "December"]

for m in months:
    print(m[0])
```

### Review Exercise:  Using multiple lists with a `for` loop - Indexing or iterating.

In the cell below:

- Create a list with the names of the months. 
<br>
- Create a second list with the number of days in each month (for a regular year). 
<br>
- Create a `for` loop that prints:

`The number of days in MONTH is XX days`

where, `MONTH` is the name of the month and `XX` is the correct number of days in that month.

Hint: Refer to <a href='#DotProductLists'>Indexing Example: The dot product of two vectors</a> for how to use two lists in a loop.



```python
# A for loop to print the number of days in each month
months = ["January",
         "February",
         "March",
         "April",
         "May",
         "June",
         "July",
         "August",
         "September",
         "October",
         "November",
         "December"]

days= [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

for d,m in zip(days,months):
    print(f"The number of days in {m} is {d} days")
```

    The number of days in January is 31 days
    The number of days in February is 28 days
    The number of days in March is 31 days
    The number of days in April is 30 days
    The number of days in May is 31 days
    The number of days in June is 30 days
    The number of days in July is 31 days
    The number of days in August is 31 days
    The number of days in September is 30 days
    The number of days in October is 31 days
    The number of days in November is 30 days
    The number of days in December is 31 days



```python
# Example solution 

months = ["January",
         "February",
         "March",
         "April",
         "May",
         "June",
         "July",
         "August",
         "September",
         "October",
         "November",
         "December"]

days= [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]

# Iterating (using zip)
for m, d in zip(months, days): 
    print(f"The number of days in {m} is {d}")

# # Indexing (without using zip)
# for i in range(12):
#     print(f"The number of days in {months[i]} is {days[i]}")
```

    The number of days in January is 31
    The number of days in February is 28
    The number of days in March is 31
    The number of days in April is 30
    The number of days in May is 31
    The number of days in June is 30
    The number of days in July is 31
    The number of days in August is 31
    The number of days in September is 30
    The number of days in October is 31
    The number of days in November is 30
    The number of days in December is 31


### Review Exercise: More Indexing or iterating.

__(A)__ In the cell below write a program that adds two vectors, $\mathbf{A}$ and $\mathbf{B}$, expressed as lists 
<br> 

$\mathbf{A} = [-2, 1, 3]$

$\mathbf{B} = [6, 2, 2]$

 $ \mathbf{C} = [C_1, 
                 C_2, ...
                 C_n] = \mathbf{A} + \mathbf{B} = [(A_1 + B_1), 
                                                           (A_2 + B_2), ... 
                                                           (A_n + B_n)]$

__Hints:__ 
- Refer to <a href='#DotProductLists'>Indexing Example: The dot product of two vectors</a> for how to use two vectors in a loop. 
- Start by creating an empty list, `C = []`. 
<br>Add an element to the list each time the code loops using the method `C.append()`
<br><a href='#Append'>Jump to Adding an Item to a List</a>
 
<br>
__(B)__ To add two vectors, the number of elements in each vectors must be equal. 
<br>Use the function `len()` to print the length of $\mathbf{A}$ and the length of $\mathbf{B}$ before adding the two vectors.
<br><a href='#Length'>Jump to Finding the Length of a List</a>

<br>
__(C)__ Use `if` and `else` statements (Seminar 2) to:
- add the two vectors __only__ if the length of $\mathbf{A}$ and the length of $\mathbf{B}$ are equal.
- otherwise print a message  (e.g. "`unequal vector lengths!`") 

Hint: Use a logical operator (`==`, `<`, `>`....) to compare the lengths of $\mathbf{A}$ and $\mathbf{B}$. <br>Refer to __Logical Operators__ (Seminar 2). 

<br>
__(D)__ Check your code works by using it to try and add two vectors with:
<br>i) the same number of elements in each vector
<br>ii) a different number of elements in each vector


```python
# Vector addition program with length check.
A=[-2,1,3]
B = [6, 2, 2]
print(len(A))
print(len(B))
if len(A)==len(B):
    C=[a+b for a,b in zip(A,B)]
    print(C)
else:
    print("error")

```

    3
    3
    [4, 3, 5]



```python
# Example solution 

A = [-2, 1, 3]

B = [6, 2, 2]

print(len(A))
print(len(B))

if (len(A) == len(B)):

    C = []
    
    # Iterating (using zip) 
    for a, b in zip(A, B):
        C.append(a * b)
      
    # Indexing (without using zip)
#     for i in range(len(A)):
#         C.append(A[i] + B[i])

    print(C)

else:
    print("unequal vector length!")
```

    3
    3
    [-12, 2, 6]


### Review Exercise: `if`, `elif` and `else` statements with a `for` loop
In the cell below, create a `for` loop to iterate over each name in the list of months. 

Within the `for` loop, use `if`, `elif` and `else` statements to do the following for each month in the list:
- print `month_name begins with M` if the name starts with the letter M. 
- print `month_name begins with J` if the name starts with the letter J. 
- print `month_name does not begin with M or J` if the name starts with a letter other than M or J. 
(In each case replace '`month_name`' with the actual month name)


```python
# Use if, elif and else within a for loop

months = ["January",
         "February",
         "March",
         "April",
         "May",
         "June",
         "July",
         "August",
         "September",
         "October",
         "November",
         "December"]
for m in months:
    if(m[0]=='M'):
        print(f"{m} begins with M")
    elif(m[0]=='J'):
        print(f"{m} begins with J")
    else:
        print(f"{m} does not begin with M or J")


```

    January begins with J
    February does not begin with M or J
    March begins with M
    April does not begin with M or J
    May begins with M
    June begins with J
    July begins with J
    August does not begin with M or J
    September does not begin with M or J
    October does not begin with M or J
    November does not begin with M or J
    December does not begin with M or J



```python
# Example solution 

months = ["January",
         "February",
         "March",
         "April",
         "May",
         "June",
         "July",
         "August",
         "September",
         "October",
         "November",
         "December"]

for m in months:
    if m[0]=='M':
        print(f'{m} begins with M')
        
    elif m[0]=='J':
        print(f'{m} begins with J')
        
    else:
        print(f'{m} does not begin with M or J')
```

### Review Exercise: `if` and `else` statements

Copy and paste the program you wrote earlier to <a href='#DotProductLists'>find the dot product of two vectors</a>  into the cell below.

Within the loop use `if`, `elif` and else `else` to make the program print:
 - "`The angle between vectors is acute`" if the dot product is positive.
 - "`The angle between vectors is obtuse`" if the dot product is negative.
 - "`The vectors are perpendicular`" if the dot product is 0.


```python
# Determinig angle types using the dot product.
```


```python
# Example solution 

A = [1.0, 3.0, -5.0]
B = [4.0, -2.0, -1.0]

# Create a variable called dot_product with value, 0.
dot_product = 0.0

# Iterating (using zip) 
for a, b in zip(A, B):
    dot_product += a * b

# Indexing (without using zip)
# for i in range(len(A)):
#     dot_product += A[i]*B[i]

print(dot_product)

if (dot_product > 0):
    print("The angle between vectors is acute")
    
elif (dot_product < 0):
    print("The angle between vectors is obtuse")
    
else:
    print("The vectors are perpendicular")
```

### Review Exercise: Dictionaries
Given the dictionary, `inventory` in the cell below, do the folowing:
 - Add a key to inventory called 'pocket'.
 - Set the value of 'pocket' to be a list consisting of the strings 'seashell', 'strange berry', and 'lint'.
 - Remove('dagger') from the list of items stored under the 'backpack' key.<br><a href='#DictionaryRemove'>Jump to Removing Entries from a Dictionary</a>
 - Sort()the items in the list stored as a value under the 'backpack' key. <br>Dictionaries can be sorted in the same way as other data structures e.g. <a href='#SortLists'>Sorting Lists</a>.
 - Add 50 to the number stored under the 'gold' key.
 
Print `inventory` to check your answer. 


```python
inventory = {
    'gold' : 500,
    'pouch' : ['flint', 'twine', 'gemstone'],
    'backpack' : ['xylophone','dagger', 'bedroll','bread loaf']
}
inventory["pocket"]=['seashell', 'strange berry',"lint"];
inventory["backpack"].pop(1);
inventory["backpack"]=inventory["backpack"].sort;
inventory["gold"]+=50
```


```python
# Example solution

inventory = {
    'gold' : 500,
    'pouch' : ['flint', 'twine', 'gemstone'],
    'backpack' : ['xylophone','dagger', 'bedroll','bread loaf']
}

# add 
inventory['pocket'] = ['seashell', 'strange berry', 'lint']

# remove 
del inventory['backpack'][1]

# sort 
# either
inventory['backpack'].sort()
# or
inventory['backpack'] = sorted(inventory['backpack'])

# edit 
inventory['gold'] += 50

print(inventory)
```

    {'gold': 550, 'pouch': ['flint', 'twine', 'gemstone'], 'backpack': ['bedroll', 'bread loaf', 'xylophone'], 'pocket': ['seashell', 'strange berry', 'lint']}


### Review Exercise: Dictionaries - Calculating stock value.
In the cell below, there is a dictionary called `stock`.

Make a second dictionary called `prices`.
- The `keys` in `prices` should be the same fruit names as in `stock`, in the same order.
- The `values` in `prices` should be the price of each fruit, expressed as numeric value; you can choose the values.

Make a third dictionary called `stock_value`. This dictionary should be empty. 
<br>Jump to <a href='#DeclaringDict'>creating an empty dictionary.</a>   



Use a `for` loop to iterate over two dictionaries; `stock` and `prices`.

For each fruit type:
1. Multiply the number of fruit in stock by the price of a single fruit to get the total stock value.
1. Add the total stock value to the dictionary `stock_value`. The `keys` in `stock_value` should be the same fruit names as in `stock` and `prices`.

Print `stock_value` to check your answer.


```python
stock = {
    "banana": 6,
    "apple": 0,
    "orange": 32,
    "pear": 15
}
prices = {
    "banana": 4,
    "apple": 2,
    "orange": 1.5,
    "pear": 3
}

stock_value={}
for s in stock:
    stock_value[s]=prices[s]*stock[s]
print(stock_value)

stock_value={s:sv*pv for s,sv,p,pv in zip(stock,stock.values(),prices,prices.values())}
print(stock_value)
    
```

    {'banana': 24, 'apple': 0, 'orange': 48.0, 'pear': 45}
    {'banana': 24, 'apple': 0, 'orange': 48.0, 'pear': 45}



```python
# Example solution

stock = {
    "banana": 6,
    "apple": 0,
    "orange": 32,
    "pear": 15
}

prices = {
    "banana": 4,
    "apple": 2,
    "orange": 1.5,
    "pear": 3
}

stock_value = {}

for s in stock:
    stock_value[s]=stock[s]*prices[s]

print(stock_value)
```

    {'banana': 24, 'apple': 0, 'orange': 48.0, 'pear': 45}


### Review Exercise: Dictionaries - The Periodic Table.



__(A)__ Choose 5 elements from the periodic table.
<br>
In the cell below create a dictionary: 
<a href='#Dictionaries'>Jump to Dictionaries</a>
 - __keys:__ chemical symbol names 
 - __values:__ atomic numbers 
 
 e.g. 
 ```python
 dictionary = {"C":6, "N":7, "O":8....}
 
 ```


__(B)__ Remove one entry from the dictionary and print the updated version.
<br><a href='#DictionaryRemove'>Jump to Removing Entries from a Dictionary</a>

__(C)__ Add a new entry (chemical symbol and atomic number) to the dictionary and print the updated version.
<br><a href='#DictionaryAdd'>Jump to Adding Entries to a Dictionary</a>

__(D)__ Use a `for` loop to create a new dictionary:  
 - __keys:__ atomic numbers 
 - __values:__ chemical symbols
using your original dictionary. 
<br> Hint: Refer to the earlier example of <a href='#DictionaryRestructure'>re-structuring to make a new dictioary.</a> 

__*Optional Extension*__

__(E)__ Print a __list__ of the chemical symbols in your dictionary, sorted into alphabetical order.
Hints:
 - Create an empty list 
 - Use a for loop to add each chemical symbol to the list 
 - Sort the list in alphabetical order 


```python
# Dictionary of periodic table items.
```


```python
# Example solution 

# (A) 
dictionary = {"C":6, "N":7, "O":8, "Li":3, "Mg":12}

# (B)
del dictionary["N"]
print(dictionary)

# (C)
dictionary["La"] = 57
print(dictionary)

# (D)
inverted_dictionary = {}

for chemical, number in dictionary.items():
    inverted_dictionary[number] = chemical
    
print(inverted_dictionary)

# (E) 
chemical_list = []

for chemical, number in dictionary.items():
    chemical_list.append(chemical)
    
chemical_list.sort()

print(chemical_list)
```

### Review Exercise: `while` loops (bisection)

Bisection is an iterative method for approximating a root of a function $y = F(x)$ 
<br>i.e. a value of $x$ for which the function $F(x)$ is equal to zero. 
<br>Therefore the roots are found where the line of the function F(x) __crosses__ the x axis (the red dot indicates the root of the function):





If we know such a __crossing point__ lies within the interval x = a and x = b we can repeatedly *bisect* this interval to narrow down the interval in which x = root must lie. 

Each iteration, x$_{mid} = \frac{a + b}{2}$ is determined and used to determine whether the crossing point is between x$_{mid}$ and a or x$_{mid}$ and b.
<br>This is used to define a new, narrower interval in which we know the crossing point lies.

      x_mid = (a + b) / 2 

      # If F(x) changes sign between F(x_mid) and F(a), 
      # the root must lie between F(x_mid) and F(a)
      
      if F(x_mid) * F(a) < 0:
          b = x_mid
          x_mid = (a + b)/2          
          
      # If F(x) changes sign between F(x_mid) and F(b), 
      # the root must lie between F(x_mid) and F(b)
      
      else:
          a = x_mid
          x_mid = (a + b)/2  

          


In the example shown, the midpoint (x$_{mid}$) of a$_1$  and b$_1$ is b$_2$ 
<br>F(a$_1$) $\times$ F(b$_2$) = negative
<br>F(b$_1$) $\times$ F(b$_2$) = positive

So the new increment is between a$_1$ and b$_2$.

<br>

By repeating this process, the value of F(x$_{mid}$) should become closer to zero with each iteration.

The process is repeated until the *absolute* value |F(x$_{mid}$)| is sufficiently small (below a predetermined value (*tolerance*)). 

We then determine x$_{mid}$ is the root of the function. 

**Task:** 

$$
F(x) = 4x^3 - 3x^2 - 25x - 6
$$



The function has one root between x = 0 and x = -0.6.

__(A)__ Use the bisection method to estimate the value of the root between x = 0 and x = -0.6.
<br>Instructions:
- Use a while loop to repeat the code above __while__ absF(x$_{mid}$) > 1 $\times10^{-6}$.
- Each time the code loops:
 - __Compute__ F(a), F(b) and F(x_mid) [Hint: Use approprate variable names that don't contain () parentheses) 
 - __Print__ F(x$_{mid}$) to check absF(x$_{mid}$)  $< 1 \times10^{-6}$. <br>Use  the function `abs()` to compute the absolute value of a number, <br>https://docs.python.org/3/library/functions.html#abs  <br> e.g. `y = abs(x)` assigns the absolute value of `x` to `y`. 
 - __Bisect__ the increment using the code shown above
- __After__ the loop print the final value of x$_{mid}$ using `print("root = ", x_mid) `. <br>This value is the estimate of the root.

<a href='#WhileLoops'>Jump to While Loops'</a>

__(B)__ The bisection method is only effective where F(a) and F(b) are of opposite sign.
<br> i.e. where F(a) $\times$ F(b) $ < 0$
<br>Add an if statement to your code so that the while loop is only run *if* the inputs a and b are of opposite sign.


```python
# Bisection while loop
a=-0.6
b=0
x=(a+b)/2;
fa=(4*a**3)-(3*a*a)-25*a-6
fb=(4*b**3)-(3*b*b)-25*b-6
fx=(4*x**3)-(3*x*x)-25*x-6
if(fa*fb<0):
    while (abs(fx)>=10E-6):
        x=(a+b)/2
        fa=(4*a**3)-(3*a*a)-25*a-6
        fb=(4*b**3)-(3*b*b)-25*b-6
        fx=(4*x**3)-(3*x*x)-25*x-6
        if (fa*fx<=0):
            b=x
        else:
            a=x
print(x)
```

    -0.2499996185302734



```python
# Example solution 

a = -0.6
b = 0
x_mid = (a + b) / 2

Fa = (4 * a**3) - (3 * a**2) - (25 * a) - 6

Fb = (4 * b**3) - (3 * b**2) - (25 * b) - 6

Fxmid = (4 * x_mid**3) - (3 * x_mid**2) - (25 * x_mid) - 6


if ((Fa * Fb) < 0): 

    while (abs(Fxmid) > 10E-6):

        Fa = (4 * a**3) - (3 * a**2) - (25 * a) - 6

        Fb = (4 * b**3) - (3 * b**2) - (25 * b) - 6

        Fxmid = (4 * x_mid**3) - (3 * x_mid**2) - (25 * x_mid) - 6
        
        print(round(Fxmid, 6))

        # If F(x) changes sign between F(x_mid) and F(a), 
        # the root must lie between F(x_mid) and F(a)
        if Fxmid * Fa < 0:
            b = x_mid
            x_mid = (a + b)/2


        # If F(x) changes sign between F(x_mid) and F(b), 
        # the root must lie between F(x_mid) and F(b)    
        else:
            a = x_mid
            x_mid = (a + b)/2  

print("root = ", round(x_mid, 3)) 
```

    1.122
    -2.331
    -0.572438
    0.28343
    -0.142421
    0.071035
    -0.035562
    0.01777
    -0.008888
    0.004443
    -0.002222
    0.001111
    -0.000555
    0.000278
    -0.000139
    6.9e-05
    -3.5e-05
    1.7e-05
    -9e-06
    root =  -0.25


 __(C)__ In the previous example you stopped the while loop when the value of the function was sufficiently small (abs(F(x$_{mid}$)) $< 1 \times10^{-6}$) that we can consider the corresponding value of x to be a root of the function.  

This time we are going to edit your code so that the loop is stopped when it reaches a __maximum number of iterations__.  <br>Copy and paste your code from the cell above in the cel below. 
<br>Replace your __while loop__ with a __for loop__ that runs the code in the loop 25 times then stops.  

__(D)__ Within the for loop, add a `break` statement.
<br>The `break` statement should exit the for loop __if__ abs(F(x$_{mid}$)) $< 1 \times10^{-6}$
<br>i.e. __if__ abs(F(x$_{mid}$)) $< 1 \times10^{-6}$ the loop will stop before the maximum number of iterations is reached.
<br>Before the command `break`, print the value of x$_{mid}$ using `print("root =", x_mid) `. <br>This value is the estimate of the root.

<a href='#Break'>Jump to break</a> 


```python
# Copy and paste your code from the cell above, here
```


```python
# Example solution 

a = -0.6
b = 0
x_mid = (a + b) / 2

Fa = (4 * a**3) - (3 * a**2) - (25 * a) -6

Fb = (4 * b**3) - (3 * b**2) - (25 * b) -6

Fxmid = (4 * x_mid**3) - (3 * x_mid**2) - (25 * x_mid) -6

# max number of iterations
nmax = 25


if ((Fa * Fb) < 0): 

    for i in range(nmax):

        print(Fxmid)
        
        if (abs(Fxmid) < 10E-6):
            print("root = ", x_mid) 
            break
        
        Fa = (4 * a**3) - (3 * a**2) - (25 * a) -6

        Fb = (4 * b**3) - (3 * b**2) - (25 * b) -6

        Fxmid = (4 * x_mid**3) - (3 * x_mid**2) - (25 * x_mid) -6


        # If F(x) changes sign between F(x_mid) and F(a), 
        # the root must lie between F(x_mid) and F(a)
        if Fxmid * Fa < 0:
            b = x_mid
            x_mid = (a + b)/2


        # If F(x) changes sign between F(x_mid) and F(b), 
        # the root must lie between F(x_mid) and F(b)    
        else:
            a = x_mid
            x_mid = (a + b)/2  
```

    1.1219999999999999
    1.1219999999999999
    -2.331
    -0.5724375000000013
    0.2834296874999991
    -0.14242089843750083
    0.07103503417968593
    -0.035561508178711954
    0.01776977348327513
    -0.008887634038925896
    0.004443130463360312
    -0.002221736904234106
    0.0011108255381691023
    -0.0005554234980964523
    0.0002777090668590887
    -0.00013885520398559947
    6.942743435445209e-05
    -3.4713759087701135e-05
    1.7356869064677483e-05
    -8.678437152909169e-06
    root =  -0.25000019073486324


# Updating your git repository

You have made several changes to your interactive textbook.

 > Save your work.
 > <br> `git add -A`
 > <br>`git commit -m "A short message describing changes"`
 > <br>`git push`
 

## Summary
 - A data structure is used to assign a collection of values to a single collection name.
 - A Python list can store multiple items of data in sequentially numbered elements (numbering starts at zero)
 - Data stored in a list element can be referenced using the list name can be referenced using the list name followed by an index number in [] square brackets.
 - The `len()` function returns the length of a specified list.
 - A Python tuple whose values can not be individually changed, removed or added to (except by adding another tuple).
 - Data stored in a tuple element can be referenced using the tuple name followed by an index number in [] square brackets.
 - A Python dictionary is a list of key: value pairs of data in which each key must be unique.
 - Data stored in a dictionary element can be referenced using the dictionary name followed by its key in [] square brackets. 

# Homework 

1. __COMPLETE__ any unfinished Review Exercises.<br>In particular, please complete: __Review Exercise: `while` loops (bisection)__.<br>You will need to refer to your answer in next week's Seminar. 
1. __PUSH__ the changes you make at home to your online repository. 



ipynb
6ea4bb350c70a9c5ebb71b12f8d3cbcfe4d3a9b3
Jupyter Notebook
50b9fbf8c64b3ed63a1544b89460428b74bef854
MIT
zaheirava/snake
4_Data_Structures.ipynb
50b9fbf8c64b3ed63a1544b89460428b74bef854
MIT
zaheirava/snake
4_Data_Structures.ipynb
50b9fbf8c64b3ed63a1544b89460428b74bef854
MIT
zaheirava/snake
4_Data_Structures.ipynb