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

### 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

### 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.

### 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'>

### 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']

### 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

### 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
(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:__

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.

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

$\mathbf{j}$ is the displacement one unit in the y-direction

$\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.

### 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*

__3D vector__

$\mathbf{r} = [x, y, z]$

magnitude of $\mathbf{r}$:

$|\mathbf{r}| = \sqrt{x^2 + y^2 + z^2}$

__2D vector__

$\mathbf{r} = [x, y]$

magnitude of $\mathbf{r}$:

$|\mathbf{r}| = \sqrt{x^2 + y^2}$

__ALGEBRAIC REPRESENTATION OF THE DOT PRODUCT__

The dot product of two $n$-length-vectors:

 $ \mathbf{A} = [A_1, A_2, ... A_n]$

 $ \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:

 $ \mathbf{A} = [A_x, A_y, A_z]$

 $ \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:__

 The dot product $\mathbf{A} \cdot \mathbf{B}$:

 $ \mathbf{A} = [1, 3, −5]$

 $ \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
 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?

(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:

"y**2 for y in Y"

means:
- Loop through each value in the list Y.
- With each iteration:
 - assign the value of the next element in Y to the variable y.
 - square it to get the next element in X.

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__

>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__ *
>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.

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__

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'>

### 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 Nested Lists 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`...)

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),

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 Sorting Lists.

__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.
This is known as a *formatted string* or *"f-string"*.

## 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 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:__

Refer back to:
 - Iterating Over Lists
 - Iterating Over Tuples

to remind yourself how to *iterate* over a data structure.

Using __exactly the same method__, iterate over the entries in the dictionary `room allocations` using a `for` loop.

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__

Copy and paste the code from the previous cell.

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

### 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}

### 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__

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 Sorting Lists.

```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:

"key: val for key, val in room_allocations.items() if val"

means:
If the dictionary entry has a value (val), create an entry in the new list using its:

key

value

### 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`'.

(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.

__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.

__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.

__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.

__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.

__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 Indexing for how to pick out individual letters of a string.

Jump to Iterating over lists 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.

- Create a second list with the number of days in each month (for a regular year).

- 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 Indexing Example: The dot product of two vectors 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

$\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 Indexing Example: The dot product of two vectors for how to use two vectors in a loop.
- Start by creating an empty list, `C = []`.

Add an element to the list each time the code loops using the method `C.append()`

Jump to Adding an Item to a List

__(B)__ To add two vectors, the number of elements in each vectors must be equal.

Use the function `len()` to print the length of $\mathbf{A}$ and the length of $\mathbf{B}$ before adding the two vectors.

Jump to Finding the Length of a List

__(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}$.
Refer to __Logical Operators__ (Seminar 2).

__(D)__ Check your code works by using it to try and add two vectors with:

i) the same number of elements in each vector

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 find the dot product of two vectors  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.
Jump to Removing Entries from a Dictionary
 - Sort()the items in the list stored as a value under the 'backpack' key.
Dictionaries can be sorted in the same way as other data structures e.g. Sorting Lists.
 - 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.

Jump to creating an empty dictionary.

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.

In the cell below create a dictionary:
Jump to Dictionaries
 - __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.

Jump to Removing Entries from a Dictionary

__(C)__ Add a new entry (chemical symbol and atomic number) to the dictionary and print the updated version.

Jump to Adding Entries to a Dictionary

__(D)__ Use a `for` loop to create a new dictionary:
 - __keys:__ atomic numbers
 - __values:__ chemical symbols
using your original dictionary.

 Hint: Refer to the earlier example of re-structuring to make a new dictioary.

__*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)$

i.e. a value of $x$ for which the function $F(x)$ is equal to zero.

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.

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$

F(a$_1$) $\times$ F(b$_2$) = negative

F(b$_1$) $\times$ F(b$_2$) = positive

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

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.

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}$.
Use  the function `abs()` to compute the absolute value of a number,
https://docs.python.org/3/library/functions.html#abs
 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) `.
This value is the estimate of the root.

Jump to While Loops'

__(B)__ The bisection method is only effective where F(a) and F(b) are of opposite sign.

 i.e. where F(a) $\times$ F(b) $ < 0$

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__.
Copy and paste your code from the cell above in the cel below.

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.

The `break` statement should exit the for loop __if__ abs(F(x$_{mid}$)) $< 1 \times10^{-6}$

i.e. __if__ abs(F(x$_{mid}$)) $< 1 \times10^{-6}$ the loop will stop before the maximum number of iterations is reached.

Before the command `break`, print the value of x$_{mid}$ using `print("root =", x_mid) `.
This value is the estimate of the root.

Jump to break

```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.
 >
 `git add -A`
 >
`git commit -m "A short message describing changes"`
 >
`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.
In particular, please complete: __Review Exercise: `while` loops (bisection)__.
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.

max_stars_repo_name: zaheirava/snake

max_issues_repo_name: zaheirava/snake

max_forks_repo_name: zaheirava/snake


text:
```python
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
from sympy import latex
from IPython.display import display, Math
plt.rcParams['figure.figsize'] = [20, 10]
```

```python
# retrived from https://www.arduino.cc/reference/en/language/functions/math/map/
def map_func(x, in_min, in_max, out_min, out_max):
    return (x - in_min) * (out_max - out_min) / (in_max - in_min) + out_min
```

```python
def adc2angle(df, shoulder_min=203, soulder_max=599, elbow_min=865, elbow_max=261,
              shoulder_min_angle =-90, shoudler_max_angle=0,
              elbow_min_angle=0, elbow_max_angle=145):
    col = df.columns # extract column names from dataframe

    # convertes first column (shoulder) to angles between -90 and 0
    df[col[0]] = df.apply(lambda x: map_func(df[col[0]], shoulder_min, soulder_max, shoulder_min_angle, shoudler_max_angle))

    # convertes second column (elbow) to angles between 0 and 160
    df[col[1]] = df.apply(lambda x: map_func(df[col[1]], elbow_min, elbow_max, elbow_min_angle, elbow_max_angle))
    return df
```

# Mapping Calibration

```python
df = pd.read_csv('./datasets/pot_cal_dataset_clean_1.csv') # dataset generated during calibration run
df # prints pot ADC values
```

      Shoulder
      Elbow

      0
      203.0
      490.0

      1
      203.0
      490.0

      2
      198.0
      489.0

      3
      202.0
      865.0

      4
      599.0
      491.0

      5
      598.0
      490.0

      6
      598.0
      489.0

- shoulder
    - min
        - angle:-90
        - ADC: 203

    - max
        - angle: 0
        - ADC: 599

- elbow
    - min
        - angle: 0
        - ADC: 865
    - max
        - angle: 90
            - extrapolated angle: 145
        - ADC: 490
            - extrapolated ADC: 261

```python
m = (865 - 490)/(0 - 90)

y_145 = m*145 + 865
y_145
```

    260.83333333333326

```python
# df['Shoulder'] = df.apply(lambda x: map_func(df['Shoulder'], 203, 599, -90, 0))
# df['Elbow'] = df.apply(lambda x: map_func(df['Elbow'], 865, 261, 0, 145))

df = adc2angle(df)
df # prints pot converted angle values
```

      Shoulder
      Elbow

      0
      -90.000000
      90.024834

      1
      -90.000000
      90.024834

      2
      -91.136364
      90.264901

      3
      -90.227273
      0.000000

      4
      0.000000
      89.784768

      5
      -0.227273
      90.024834

      6
      -0.227273
      90.264901

# Converting ADC Values of Traced Image to Angles

```python
df = pd.read_csv('./datasets/hyster_dataset_clean_1.csv') # second_trial_3.csv is the results of the traced image
df = df.dropna()
df = adc2angle(df)
df.to_csv('angles_1.csv', index=False)
```

```python
df = pd.read_csv('./datasets/hyster_dataset_clean_5.csv') # second_trial_3.csv is the results of the traced image
df = df.dropna()
df = adc2angle(df)
df.to_csv('angles_5.csv', index=False)
```

ext: ipynb


text:
```python
from sympy import *
from sympy.stats import *
from functools import *
import math as m
```

```python
def fatorial(n):
    return reduce(lambda n1,n2: n1 * n2, range(1, n + 1)) if n > 0 else 1

def euler(n):
    return 2.71828**n

def poisson(l, x):
    return ((euler(-l))*(l**x))/fatorial(x)
```

```python
1 - poisson(3, 0) - poisson(3, 1) - poisson(3, 2)
```

    0.5768090648924199

```python
X = Poisson("X", 10)
P(Eq(X, 1))
```

$\displaystyle \frac{10}{e^{10}}$

max_stars_repo_name: GrrriiiM/estudos-python

max_issues_repo_name: GrrriiiM/estudos-python

max_forks_repo_name: GrrriiiM/estudos-python


text:
```python
!pip install mxnet
!pip install gluonnlp pandas tqdm
!pip install sentencepiece
!pip install transformers==3.0.2
!pip install torch
```

    Collecting mxnet
      Downloading mxnet-1.9.0-py3-none-manylinux2014_x86_64.whl (47.3 MB)
         |████████████████████████████████| 47.3 MB 1.8 MB/s
    Collecting graphviz<0.9.0,>=0.8.1
      Downloading graphviz-0.8.4-py2.py3-none-any.whl (16 kB)
    Requirement already satisfied: numpy<2.0.0,>1.16.0 in /usr/local/lib/python3.7/dist-packages (from mxnet) (1.21.5)
    Requirement already satisfied: requests<3,>=2.20.0 in /usr/local/lib/python3.7/dist-packages (from mxnet) (2.23.0)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet) (2.10)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet) (1.24.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet) (2021.10.8)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet) (3.0.4)
    Installing collected packages: graphviz, mxnet
      Attempting uninstall: graphviz
        Found existing installation: graphviz 0.10.1
        Uninstalling graphviz-0.10.1:
          Successfully uninstalled graphviz-0.10.1
    Successfully installed graphviz-0.8.4 mxnet-1.9.0
    Collecting gluonnlp
      Downloading gluonnlp-0.10.0.tar.gz (344 kB)
         |████████████████████████████████| 344 kB 11.1 MB/s
    Requirement already satisfied: pandas in /usr/local/lib/python3.7/dist-packages (1.3.5)
    Requirement already satisfied: tqdm in /usr/local/lib/python3.7/dist-packages (4.63.0)
    Requirement already satisfied: numpy>=1.16.0 in /usr/local/lib/python3.7/dist-packages (from gluonnlp) (1.21.5)
    Requirement already satisfied: cython in /usr/local/lib/python3.7/dist-packages (from gluonnlp) (0.29.28)
    Requirement already satisfied: packaging in /usr/local/lib/python3.7/dist-packages (from gluonnlp) (21.3)
    Requirement already satisfied: python-dateutil>=2.7.3 in /usr/local/lib/python3.7/dist-packages (from pandas) (2.8.2)
    Requirement already satisfied: pytz>=2017.3 in /usr/local/lib/python3.7/dist-packages (from pandas) (2018.9)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.7.3->pandas) (1.15.0)
    Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging->gluonnlp) (3.0.7)
    Building wheels for collected packages: gluonnlp
      Building wheel for gluonnlp (setup.py) ... done
      Created wheel for gluonnlp: filename=gluonnlp-0.10.0-cp37-cp37m-linux_x86_64.whl size=595740 sha256=87beb3c2084de50a6c64b79314d4490cdf589e47ca61da0cd664106ec4d7faf8
      Stored in directory: /root/.cache/pip/wheels/be/b4/06/7f3fdfaf707e6b5e98b79c041e023acffbe395d78a527eae00
    Successfully built gluonnlp
    Installing collected packages: gluonnlp
    Successfully installed gluonnlp-0.10.0
    Collecting sentencepiece
      Downloading sentencepiece-0.1.96-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (1.2 MB)
         |████████████████████████████████| 1.2 MB 13.0 MB/s
    Installing collected packages: sentencepiece
    Successfully installed sentencepiece-0.1.96
    Collecting transformers==3.0.2
      Downloading transformers-3.0.2-py3-none-any.whl (769 kB)
         |████████████████████████████████| 769 kB 12.4 MB/s
    Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (4.63.0)
    Requirement already satisfied: requests in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (2.23.0)
    Requirement already satisfied: numpy in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (1.21.5)
    Collecting tokenizers==0.8.1.rc1
      Downloading tokenizers-0.8.1rc1-cp37-cp37m-manylinux1_x86_64.whl (3.0 MB)
         |████████████████████████████████| 3.0 MB 39.9 MB/s
    Requirement already satisfied: filelock in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (3.6.0)
    Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (2019.12.20)
    Requirement already satisfied: sentencepiece!=0.1.92 in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (0.1.96)
    Requirement already satisfied: packaging in /usr/local/lib/python3.7/dist-packages (from transformers==3.0.2) (21.3)
    Collecting sacremoses
      Downloading sacremoses-0.0.49-py3-none-any.whl (895 kB)
         |████████████████████████████████| 895 kB 44.5 MB/s
    Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging->transformers==3.0.2) (3.0.7)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests->transformers==3.0.2) (1.24.3)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests->transformers==3.0.2) (2021.10.8)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests->transformers==3.0.2) (2.10)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests->transformers==3.0.2) (3.0.4)
    Requirement already satisfied: joblib in /usr/local/lib/python3.7/dist-packages (from sacremoses->transformers==3.0.2) (1.1.0)
    Requirement already satisfied: six in /usr/local/lib/python3.7/dist-packages (from sacremoses->transformers==3.0.2) (1.15.0)
    Requirement already satisfied: click in /usr/local/lib/python3.7/dist-packages (from sacremoses->transformers==3.0.2) (7.1.2)
    Installing collected packages: tokenizers, sacremoses, transformers
    Successfully installed sacremoses-0.0.49 tokenizers-0.8.1rc1 transformers-3.0.2
    Requirement already satisfied: torch in /usr/local/lib/python3.7/dist-packages (1.10.0+cu111)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch) (3.10.0.2)

```python
!pip install qiskit
```

    Collecting qiskit
      Downloading qiskit-0.35.0.tar.gz (13 kB)
    Collecting qiskit-terra==0.20.0
      Downloading qiskit_terra-0.20.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (6.5 MB)
         |████████████████████████████████| 6.5 MB 9.7 MB/s
    Collecting qiskit-aer==0.10.3
      Downloading qiskit_aer-0.10.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (18.0 MB)
         |████████████████████████████████| 18.0 MB 97 kB/s
    Collecting qiskit-ibmq-provider==0.18.3
      Downloading qiskit_ibmq_provider-0.18.3-py3-none-any.whl (238 kB)
         |████████████████████████████████| 238 kB 46.6 MB/s
    Collecting qiskit-ignis==0.7.0
      Downloading qiskit_ignis-0.7.0-py3-none-any.whl (200 kB)
         |████████████████████████████████| 200 kB 49.4 MB/s
    Requirement already satisfied: numpy>=1.16.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.10.3->qiskit) (1.21.5)
    Requirement already satisfied: scipy>=1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-aer==0.10.3->qiskit) (1.4.1)
    Requirement already satisfied: requests>=2.19 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.18.3->qiskit) (2.23.0)
    Requirement already satisfied: urllib3>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.18.3->qiskit) (1.24.3)
    Collecting requests-ntlm>=1.1.0
      Downloading requests_ntlm-1.1.0-py2.py3-none-any.whl (5.7 kB)
    Collecting websocket-client>=1.0.1
      Downloading websocket_client-1.3.2-py3-none-any.whl (54 kB)
         |████████████████████████████████| 54 kB 2.4 MB/s
    Requirement already satisfied: python-dateutil>=2.8.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ibmq-provider==0.18.3->qiskit) (2.8.2)
    Requirement already satisfied: setuptools>=40.1.0 in /usr/local/lib/python3.7/dist-packages (from qiskit-ignis==0.7.0->qiskit) (57.4.0)
    Collecting retworkx>=0.8.0
      Downloading retworkx-0.11.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (1.6 MB)
         |████████████████████████████████| 1.6 MB 42.4 MB/s
    Requirement already satisfied: sympy>=1.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.0->qiskit) (1.7.1)
    Collecting python-constraint>=1.4
      Downloading python-constraint-1.4.0.tar.bz2 (18 kB)
    Collecting ply>=3.10
      Downloading ply-3.11-py2.py3-none-any.whl (49 kB)
         |████████████████████████████████| 49 kB 5.0 MB/s
    Requirement already satisfied: dill>=0.3 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.0->qiskit) (0.3.4)
    Collecting stevedore>=3.0.0
      Downloading stevedore-3.5.0-py3-none-any.whl (49 kB)
         |████████████████████████████████| 49 kB 4.3 MB/s
    Collecting tweedledum<2.0,>=1.1
      Downloading tweedledum-1.1.1-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (943 kB)
         |████████████████████████████████| 943 kB 35.0 MB/s
    Collecting symengine>=0.9
      Downloading symengine-0.9.2-cp37-cp37m-manylinux2010_x86_64.whl (37.5 MB)
         |████████████████████████████████| 37.5 MB 23.2 MB/s
    Collecting scipy>=1.0
      Downloading scipy-1.7.3-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (38.1 MB)
         |████████████████████████████████| 38.1 MB 1.4 MB/s
    Requirement already satisfied: psutil>=5 in /usr/local/lib/python3.7/dist-packages (from qiskit-terra==0.20.0->qiskit) (5.4.8)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil>=2.8.0->qiskit-ibmq-provider==0.18.3->qiskit) (1.15.0)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.18.3->qiskit) (3.0.4)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.18.3->qiskit) (2021.10.8)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests>=2.19->qiskit-ibmq-provider==0.18.3->qiskit) (2.10)
    Collecting ntlm-auth>=1.0.2
      Downloading ntlm_auth-1.5.0-py2.py3-none-any.whl (29 kB)
    Collecting cryptography>=1.3
      Downloading cryptography-36.0.2-cp36-abi3-manylinux_2_24_x86_64.whl (3.6 MB)
         |████████████████████████████████| 3.6 MB 37.9 MB/s
    Requirement already satisfied: cffi>=1.12 in /usr/local/lib/python3.7/dist-packages (from cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.18.3->qiskit) (1.15.0)
    Requirement already satisfied: pycparser in /usr/local/lib/python3.7/dist-packages (from cffi>=1.12->cryptography>=1.3->requests-ntlm>=1.1.0->qiskit-ibmq-provider==0.18.3->qiskit) (2.21)
    Collecting pbr!=2.1.0,>=2.0.0
      Downloading pbr-5.8.1-py2.py3-none-any.whl (113 kB)
         |████████████████████████████████| 113 kB 46.8 MB/s
    Requirement already satisfied: importlib-metadata>=1.7.0 in /usr/local/lib/python3.7/dist-packages (from stevedore>=3.0.0->qiskit-terra==0.20.0->qiskit) (4.11.3)
    Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=1.7.0->stevedore>=3.0.0->qiskit-terra==0.20.0->qiskit) (3.7.0)
    Requirement already satisfied: typing-extensions>=3.6.4 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata>=1.7.0->stevedore>=3.0.0->qiskit-terra==0.20.0->qiskit) (3.10.0.2)
    Requirement already satisfied: mpmath>=0.19 in /usr/local/lib/python3.7/dist-packages (from sympy>=1.3->qiskit-terra==0.20.0->qiskit) (1.2.1)
    Building wheels for collected packages: qiskit, python-constraint
      Building wheel for qiskit (setup.py) ... done
      Created wheel for qiskit: filename=qiskit-0.35.0-py3-none-any.whl size=11839 sha256=36064059005bd9da7d98f6c7762e1e377c80ddee196703322ca9ade0961bad06
      Stored in directory: /root/.cache/pip/wheels/1e/0a/42/93e5cc11795fb0a85fcf6686eac2791def0c9a1cad0a774045
      Building wheel for python-constraint (setup.py) ... done
      Created wheel for python-constraint: filename=python_constraint-1.4.0-py2.py3-none-any.whl size=24081 sha256=bfc2683bcb2127e2d3dce03dda60659bd97b1eaed7b24a401cdf4c4e577f7fe4
      Stored in directory: /root/.cache/pip/wheels/07/27/db/1222c80eb1e431f3d2199c12569cb1cac60f562a451fe30479
    Successfully built qiskit python-constraint
    Installing collected packages: pbr, tweedledum, symengine, stevedore, scipy, retworkx, python-constraint, ply, ntlm-auth, cryptography, websocket-client, requests-ntlm, qiskit-terra, qiskit-ignis, qiskit-ibmq-provider, qiskit-aer, qiskit
      Attempting uninstall: scipy
        Found existing installation: scipy 1.4.1
        Uninstalling scipy-1.4.1:
          Successfully uninstalled scipy-1.4.1
    ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
    albumentations 0.1.12 requires imgaug<0.2.7,>=0.2.5, but you have imgaug 0.2.9 which is incompatible.
    Successfully installed cryptography-36.0.2 ntlm-auth-1.5.0 pbr-5.8.1 ply-3.11 python-constraint-1.4.0 qiskit-0.35.0 qiskit-aer-0.10.3 qiskit-ibmq-provider-0.18.3 qiskit-ignis-0.7.0 qiskit-terra-0.20.0 requests-ntlm-1.1.0 retworkx-0.11.0 scipy-1.7.3 stevedore-3.5.0 symengine-0.9.2 tweedledum-1.1.1 websocket-client-1.3.2

```python
from google.colab import drive
drive.mount('/content/drive')
```

    Mounted at /content/drive

```python
!pip install git+https://git@github.com/SKTBrain/KoBERT.git@master
```

    Collecting git+https://****@github.com/SKTBrain/KoBERT.git@master
      Cloning https://****@github.com/SKTBrain/KoBERT.git (to revision master) to /tmp/pip-req-build-yvpq2g0m
      Running command git clone -q 'https://****@github.com/SKTBrain/KoBERT.git' /tmp/pip-req-build-yvpq2g0m
    Collecting boto3
      Downloading boto3-1.21.34-py3-none-any.whl (132 kB)
         |████████████████████████████████| 132 kB 11.7 MB/s
    Requirement already satisfied: gluonnlp>=0.6.0 in /usr/local/lib/python3.7/dist-packages (from kobert==0.2.3) (0.10.0)
    Requirement already satisfied: mxnet>=1.4.0 in /usr/local/lib/python3.7/dist-packages (from kobert==0.2.3) (1.9.0)
    Collecting onnxruntime==1.8.0
      Downloading onnxruntime-1.8.0-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl (4.5 MB)
         |████████████████████████████████| 4.5 MB 48.2 MB/s
    Requirement already satisfied: sentencepiece>=0.1.6 in /usr/local/lib/python3.7/dist-packages (from kobert==0.2.3) (0.1.96)
    Requirement already satisfied: torch>=1.7.0 in /usr/local/lib/python3.7/dist-packages (from kobert==0.2.3) (1.10.0+cu111)
    Collecting transformers>=4.8.1
      Downloading transformers-4.17.0-py3-none-any.whl (3.8 MB)
         |████████████████████████████████| 3.8 MB 46.4 MB/s
    Requirement already satisfied: protobuf in /usr/local/lib/python3.7/dist-packages (from onnxruntime==1.8.0->kobert==0.2.3) (3.17.3)
    Requirement already satisfied: numpy>=1.16.6 in /usr/local/lib/python3.7/dist-packages (from onnxruntime==1.8.0->kobert==0.2.3) (1.21.5)
    Requirement already satisfied: flatbuffers in /usr/local/lib/python3.7/dist-packages (from onnxruntime==1.8.0->kobert==0.2.3) (2.0)
    Requirement already satisfied: packaging in /usr/local/lib/python3.7/dist-packages (from gluonnlp>=0.6.0->kobert==0.2.3) (21.3)
    Requirement already satisfied: cython in /usr/local/lib/python3.7/dist-packages (from gluonnlp>=0.6.0->kobert==0.2.3) (0.29.28)
    Requirement already satisfied: graphviz<0.9.0,>=0.8.1 in /usr/local/lib/python3.7/dist-packages (from mxnet>=1.4.0->kobert==0.2.3) (0.8.4)
    Requirement already satisfied: requests<3,>=2.20.0 in /usr/local/lib/python3.7/dist-packages (from mxnet>=1.4.0->kobert==0.2.3) (2.23.0)
    Requirement already satisfied: certifi>=2017.4.17 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet>=1.4.0->kobert==0.2.3) (2021.10.8)
    Requirement already satisfied: idna<3,>=2.5 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet>=1.4.0->kobert==0.2.3) (2.10)
    Requirement already satisfied: urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet>=1.4.0->kobert==0.2.3) (1.24.3)
    Requirement already satisfied: chardet<4,>=3.0.2 in /usr/local/lib/python3.7/dist-packages (from requests<3,>=2.20.0->mxnet>=1.4.0->kobert==0.2.3) (3.0.4)
    Requirement already satisfied: typing-extensions in /usr/local/lib/python3.7/dist-packages (from torch>=1.7.0->kobert==0.2.3) (3.10.0.2)
    Requirement already satisfied: tqdm>=4.27 in /usr/local/lib/python3.7/dist-packages (from transformers>=4.8.1->kobert==0.2.3) (4.63.0)
    Requirement already satisfied: regex!=2019.12.17 in /usr/local/lib/python3.7/dist-packages (from transformers>=4.8.1->kobert==0.2.3) (2019.12.20)
    Requirement already satisfied: importlib-metadata in /usr/local/lib/python3.7/dist-packages (from transformers>=4.8.1->kobert==0.2.3) (4.11.3)
    Requirement already satisfied: sacremoses in /usr/local/lib/python3.7/dist-packages (from transformers>=4.8.1->kobert==0.2.3) (0.0.49)
    Collecting huggingface-hub<1.0,>=0.1.0
      Downloading huggingface_hub-0.5.0-py3-none-any.whl (77 kB)
         |████████████████████████████████| 77 kB 7.0 MB/s
    Requirement already satisfied: filelock in /usr/local/lib/python3.7/dist-packages (from transformers>=4.8.1->kobert==0.2.3) (3.6.0)
    Collecting tokenizers!=0.11.3,>=0.11.1
      Downloading tokenizers-0.11.6-cp37-cp37m-manylinux_2_12_x86_64.manylinux2010_x86_64.whl (6.5 MB)
         |████████████████████████████████| 6.5 MB 41.4 MB/s
    Collecting pyyaml>=5.1
      Downloading PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl (596 kB)
         |████████████████████████████████| 596 kB 40.8 MB/s
    Requirement already satisfied: pyparsing!=3.0.5,>=2.0.2 in /usr/local/lib/python3.7/dist-packages (from packaging->gluonnlp>=0.6.0->kobert==0.2.3) (3.0.7)
    Collecting s3transfer<0.6.0,>=0.5.0
      Downloading s3transfer-0.5.2-py3-none-any.whl (79 kB)
         |████████████████████████████████| 79 kB 6.6 MB/s
    Collecting botocore<1.25.0,>=1.24.34
      Downloading botocore-1.24.34-py3-none-any.whl (8.6 MB)
         |████████████████████████████████| 8.6 MB 31.8 MB/s
    Collecting jmespath<2.0.0,>=0.7.1
      Downloading jmespath-1.0.0-py3-none-any.whl (23 kB)
    Collecting urllib3!=1.25.0,!=1.25.1,<1.26,>=1.21.1
      Downloading urllib3-1.25.11-py2.py3-none-any.whl (127 kB)
         |████████████████████████████████| 127 kB 43.5 MB/s
    Requirement already satisfied: python-dateutil<3.0.0,>=2.1 in /usr/local/lib/python3.7/dist-packages (from botocore<1.25.0,>=1.24.34->boto3->kobert==0.2.3) (2.8.2)
    Requirement already satisfied: six>=1.5 in /usr/local/lib/python3.7/dist-packages (from python-dateutil<3.0.0,>=2.1->botocore<1.25.0,>=1.24.34->boto3->kobert==0.2.3) (1.15.0)
    Requirement already satisfied: zipp>=0.5 in /usr/local/lib/python3.7/dist-packages (from importlib-metadata->transformers>=4.8.1->kobert==0.2.3) (3.7.0)
    Requirement already satisfied: click in /usr/local/lib/python3.7/dist-packages (from sacremoses->transformers>=4.8.1->kobert==0.2.3) (7.1.2)
    Requirement already satisfied: joblib in /usr/local/lib/python3.7/dist-packages (from sacremoses->transformers>=4.8.1->kobert==0.2.3) (1.1.0)
    Building wheels for collected packages: kobert
      Building wheel for kobert (setup.py) ... done
      Created wheel for kobert: filename=kobert-0.2.3-py3-none-any.whl size=15674 sha256=5c2a9cb5fffed3a25a3387568d42af92b341c69a5e53c0576b51e9654db64de3
      Stored in directory: /tmp/pip-ephem-wheel-cache-gon6p60e/wheels/d3/68/ca/334747dfb038313b49cf71f84832a33372f3470d9ddfd051c0
    Successfully built kobert
    Installing collected packages: urllib3, jmespath, pyyaml, botocore, tokenizers, s3transfer, huggingface-hub, transformers, onnxruntime, boto3, kobert
      Attempting uninstall: urllib3
        Found existing installation: urllib3 1.24.3
        Uninstalling urllib3-1.24.3:
          Successfully uninstalled urllib3-1.24.3
      Attempting uninstall: pyyaml
        Found existing installation: PyYAML 3.13
        Uninstalling PyYAML-3.13:
          Successfully uninstalled PyYAML-3.13
      Attempting uninstall: tokenizers
        Found existing installation: tokenizers 0.8.1rc1
        Uninstalling tokenizers-0.8.1rc1:
          Successfully uninstalled tokenizers-0.8.1rc1
      Attempting uninstall: transformers
        Found existing installation: transformers 3.0.2
        Uninstalling transformers-3.0.2:
          Successfully uninstalled transformers-3.0.2
    ERROR: pip's dependency resolver does not currently take into account all the packages that are installed. This behaviour is the source of the following dependency conflicts.
    datascience 0.10.6 requires folium==0.2.1, but you have folium 0.8.3 which is incompatible.
    Successfully installed boto3-1.21.34 botocore-1.24.34 huggingface-hub-0.5.0 jmespath-1.0.0 kobert-0.2.3 onnxruntime-1.8.0 pyyaml-6.0 s3transfer-0.5.2 tokenizers-0.11.6 transformers-4.17.0 urllib3-1.25.11

```python
import torch
from torch import nn
import torch.nn.functional as F
import torch.optim as optim
from torch.utils.data import Dataset, DataLoader
import gluonnlp as nlp
import numpy as np
from tqdm import tqdm, tqdm_notebook
```

```python
from kobert.utils import get_tokenizer
from kobert.pytorch_kobert import get_pytorch_kobert_model
```

```python
from transformers import AdamW
from transformers.optimization import get_cosine_schedule_with_warmup
```

```python
# pytorch 에서 cpu 사용 선택
# cpu 환경에서 모델 구동시
device = torch.device("cuda:0")
```

```python
# 사전 훈련된 kobert model 불러옴
bertmodel, vocab = get_pytorch_kobert_model()
```

    /content/.cache/kobert_v1.zip[██████████████████████████████████████████████████]
    /content/.cache/kobert_news_wiki_ko_cased-1087f8699e.spiece[██████████████████████████████████████████████████]

```python
# 학습에 사용할 데이터 셋 클래스 선언
class BERTDataset(Dataset):
    def __init__(self, dataset, sent_idx, label_idx, bert_tokenizer, max_len,
                 pad, pair):
        transform = nlp.data.BERTSentenceTransform(
            bert_tokenizer, max_seq_length=max_len, pad=pad, pair=pair)

        self.sentences = [transform([i[sent_idx]]) for i in dataset]
        self.labels = [np.int32(i[label_idx]) for i in dataset]

    def __getitem__(self, i):
        return (self.sentences[i] + (self.labels[i], ))

    def __len__(self):
        return (len(self.labels))
```

```python
# 모델 파라미터 설정
# 토큰의 최대 길이라고 생각
max_len = 64
# 몇 개의 샘플들을 예측해보고 가중치를 업데이트 할 지 설정
# 아래와 같이 배치 사이즈가 64인 경우 데이터 64개 마다 예측한 것을 실제 값과 비교한다
batch_size = 64
warmup_ratio = 0.1
# epoch 횟수는 모델이 전체 데이터셋을 훈련시킬 횟수를 의미한다.
num_epochs = 10
max_grad_norm = 1
log_interval = 200
# learning_rate 값이 너무 크면 원하는 값에 도달하기 힘들고, 너무 작으면 학습기간이 오래 걸린다.
learning_rate =  5e-5
```

```python
# 분류에 사용할 모델 클래스 선언
class BERTClassifier(nn.Module):
    def __init__(self,
                 bert,
                 hidden_size = 768,
                 # num_classes는 카테고리의 개수를 의미한다. (현재 데이터셋의 경우 3개의 분류로 데이터셋이 구성되어 있음)
                 num_classes=3,
                 dr_rate=None,
                 params=None):
        super(BERTClassifier, self).__init__()
        self.bert = bert
        self.dr_rate = dr_rate

        self.classifier = nn.Linear(hidden_size , num_classes)
        if dr_rate:
            self.dropout = nn.Dropout(p=dr_rate)

    def gen_attention_mask(self, token_ids, valid_length):
        attention_mask = torch.zeros_like(token_ids)
        for i, v in enumerate(valid_length):
            attention_mask[i][:v] = 1
        return attention_mask.float()

    def forward(self, token_ids, valid_length, segment_ids):
        attention_mask = self.gen_attention_mask(token_ids, valid_length)

        _, pooler = self.bert(input_ids = token_ids, token_type_ids = segment_ids.long(), attention_mask = attention_mask.float().to(token_ids.device))
        if self.dr_rate:
            out = self.dropout(pooler)
        return self.classifier(out)
```

```python
# to(Device)는 위의 torch.device("cuda:0")를 의미하며
# GPU에서 학습된 모델을 GPU로 불러올 때 사용한다.
# 또한 GPU로 학습된 모델에 데이터를 제공할 때도 to(Device)를 붙여줘야 함.
# bert모델 불러오기
model = BERTClassifier(bertmodel,  dr_rate=0.5).to(device)
```

```python
# optimizer와 schedule 설정
no_decay = ['bias', 'LayerNorm.weight']
optimizer_grouped_parameters = [
    {'params': [p for n, p in model.named_parameters() if not any(nd in n for nd in no_decay)], 'weight_decay': 0.01},
    {'params': [p for n, p in model.named_parameters() if any(nd in n for nd in no_decay)], 'weight_decay': 0.0}
]
```

```python
# optimizer = 전체 데이터 셋의 실제 결과와 모델이 예측한 값 간의 차이가 효율적으로 좁혀질 수 있도록 최적화해주는 역할
# transform에서 제공하는 AdamW optimizer 사용
optimizer = AdamW(optimizer_grouped_parameters, lr=learning_rate)
# 예측값과 실제값의 오차인 손실함수는 pytorch에서 제공하는 다중분류를 위한 대표적인 손실함수인 torch.nn.CrossEntropyLoss 사용
loss_fn = nn.CrossEntropyLoss()
```

    /usr/local/lib/python3.7/dist-packages/transformers/optimization.py:309: FutureWarning: This implementation of AdamW is deprecated and will be removed in a future version. Use the PyTorch implementation torch.optim.AdamW instead, or set `no_deprecation_warning=True` to disable this warning
      FutureWarning,

```python
# 전체 모델 불러오기(cpu 환경으로 사용할 때)
sentimentModel = torch.load('/content/drive/MyDrive/Colab Notebooks/jejuRecommand/jeju_model.pt')

# dict 불러오기
# model1.load_state_dict(torch.load('7emotions_model_state_dict.pt'))

# 지정한 값 불러오기
# checkpoint = torch.load('7emotions_all.tar')
# model1.load_state_dict(checkpoint['model'])
# optimizer.load_state_dict(checkpoint['optimizer'])
```

```python
# 앞에 클래스를 선언해줘야 모델 실행 가능

# kobert에서 vocab을 통해서 토큰화 진행
tokenizer = get_tokenizer()
tok = nlp.data.BERTSPTokenizer(tokenizer, vocab, lower=False)
sources = ['공천포','휴애리 자연생활공원','성산일출봉(UNESCO 세계자연유산)','본태박물관','제주올레 14코스',
           '바이나흐튼 크리스마스 박물관','세계자동차 & 피아노박물관','한국야구명예전당(야구박물관)','초콜릿박물관']
# 테스트 함수
def predict(predict_sentence):

    data = [predict_sentence, '0']
    dataset_another = [data]

    another_test = BERTDataset(dataset_another, 0, 1, tok, max_len, True, False)
    test_dataloader = torch.utils.data.DataLoader(another_test, batch_size=batch_size, num_workers=5)

    # 평가 모드
    sentimentModel.eval()

    for batch_id, (token_ids, valid_length, segment_ids, label) in enumerate(test_dataloader):
        token_ids = token_ids.long().to(device)
        segment_ids = segment_ids.long().to(device)

        valid_length= valid_length
        label = label.long().to(device)

        out = sentimentModel(token_ids, valid_length, segment_ids)

        test_eval=[]

        # 학습위해 변환한 분류값 초기 분류값으로 변환
        for i in out:
            logits=i
            logits = logits.detach().cpu().numpy()

            test_eval.append(sources[np.argmax(logits)])

        print(test_eval[0])
```

    using cached model. /content/.cache/kobert_news_wiki_ko_cased-1087f8699e.spiece

```python
sentence = input("검색문장: ")
predict(sentence)
```

    검색문장: 박물관 추천

    /usr/local/lib/python3.7/dist-packages/torch/utils/data/dataloader.py:481: UserWarning: This DataLoader will create 5 worker processes in total. Our suggested max number of worker in current system is 2, which is smaller than what this DataLoader is going to create. Please be aware that excessive worker creation might get DataLoader running slow or even freeze, lower the worker number to avoid potential slowness/freeze if necessary.
      cpuset_checked))

    초콜릿박물관